Idempotent seed for 16 craft + 20 cooperative tags by slug. Migration maps existing offering/lookingFor tags to communityConnections.topics and copies peerSupport fields without deleting originals.
213 lines
6.8 KiB
JavaScript
213 lines
6.8 KiB
JavaScript
/**
|
|
* Migration Script: Community Connections
|
|
*
|
|
* Migrates existing member peer support and tag data to the new
|
|
* communityConnections schema.
|
|
*
|
|
* What this does:
|
|
* 1. Builds a slug lookup from all cooperative tags in the database
|
|
* 2. For each member with offering.tags or lookingFor.tags:
|
|
* - Maps offering.tags → communityConnections.topics with state "help"
|
|
* - Maps lookingFor.tags → communityConnections.topics with state "seeking"
|
|
* 3. Copies peerSupport.enabled → communityConnections.offerPeerSupport
|
|
* 4. Copies peerSupport.availability, peerSupport.personalMessage,
|
|
* peerSupport.slackUsername → communityConnections.availability,
|
|
* .personalMessage, .slackHandle
|
|
* 5. Does NOT delete old fields (non-destructive)
|
|
*
|
|
* Safe to re-run: skips members whose communityConnections is already populated.
|
|
*/
|
|
|
|
import 'dotenv/config'
|
|
import mongoose from 'mongoose'
|
|
import Tag from '../models/tag.js'
|
|
import Member from '../models/member.js'
|
|
import { connectDB } from '../utils/mongoose.js'
|
|
|
|
async function buildCoopTagLookup() {
|
|
const coopTags = await Tag.find({ pool: 'cooperative', active: true }).lean()
|
|
// Maps normalized label → slug, and slug → slug (for direct slug matches)
|
|
const lookup = new Map()
|
|
for (const tag of coopTags) {
|
|
lookup.set(tag.label.toLowerCase(), tag.slug)
|
|
lookup.set(tag.slug.toLowerCase(), tag.slug)
|
|
}
|
|
return lookup
|
|
}
|
|
|
|
function resolveTagSlugs(rawTags, lookup) {
|
|
const matched = []
|
|
const unmatched = []
|
|
for (const raw of rawTags) {
|
|
const normalized = raw.toLowerCase().trim()
|
|
if (lookup.has(normalized)) {
|
|
matched.push(lookup.get(normalized))
|
|
} else {
|
|
unmatched.push(raw)
|
|
}
|
|
}
|
|
return { matched, unmatched }
|
|
}
|
|
|
|
async function migrateCommunityConnections() {
|
|
await connectDB()
|
|
|
|
console.log('Building cooperative tag lookup...')
|
|
const coopLookup = await buildCoopTagLookup()
|
|
console.log(` Loaded ${coopLookup.size / 2} cooperative tags`)
|
|
|
|
// Find members that have anything to migrate and haven't been migrated yet.
|
|
// A member is considered already migrated if communityConnections.topics has entries
|
|
// or offerPeerSupport is explicitly set.
|
|
const members = await Member.find({
|
|
$or: [
|
|
{ 'offering.tags': { $exists: true, $ne: [] } },
|
|
{ 'lookingFor.tags': { $exists: true, $ne: [] } },
|
|
{ 'peerSupport.enabled': { $exists: true } },
|
|
{ 'peerSupport.availability': { $exists: true } },
|
|
{ 'peerSupport.personalMessage': { $exists: true } },
|
|
{ 'peerSupport.slackUsername': { $exists: true } },
|
|
],
|
|
}).lean()
|
|
|
|
console.log(`\nFound ${members.length} member(s) with data to migrate`)
|
|
|
|
let migratedCount = 0
|
|
let skippedCount = 0
|
|
let totalTagsMatched = 0
|
|
const allUnmatched = []
|
|
|
|
for (const member of members) {
|
|
const label = `${member.name || member.email} (${member._id})`
|
|
|
|
// Skip if already migrated (topics array has entries or offerPeerSupport is set)
|
|
const cc = member.communityConnections || {}
|
|
const alreadyMigrated =
|
|
(cc.topics && cc.topics.length > 0) ||
|
|
cc.offerPeerSupport === true ||
|
|
cc.availability ||
|
|
cc.slackHandle ||
|
|
cc.personalMessage
|
|
|
|
if (alreadyMigrated) {
|
|
console.log(` skip ${label} — communityConnections already populated`)
|
|
skippedCount++
|
|
continue
|
|
}
|
|
|
|
const topics = []
|
|
const memberUnmatched = []
|
|
|
|
// Map offering.tags → state "help"
|
|
const offeringTags = member.offering?.tags || []
|
|
if (offeringTags.length > 0) {
|
|
const { matched, unmatched } = resolveTagSlugs(offeringTags, coopLookup)
|
|
for (const slug of matched) {
|
|
// Avoid duplicates
|
|
if (!topics.find((t) => t.tagSlug === slug)) {
|
|
topics.push({ tagSlug: slug, state: 'help' })
|
|
}
|
|
}
|
|
totalTagsMatched += matched.length
|
|
if (unmatched.length > 0) {
|
|
memberUnmatched.push(...unmatched.map((t) => `offering: "${t}"`))
|
|
}
|
|
}
|
|
|
|
// Map lookingFor.tags → state "seeking"
|
|
const lookingForTags = member.lookingFor?.tags || []
|
|
if (lookingForTags.length > 0) {
|
|
const { matched, unmatched } = resolveTagSlugs(lookingForTags, coopLookup)
|
|
for (const slug of matched) {
|
|
const existing = topics.find((t) => t.tagSlug === slug)
|
|
if (existing) {
|
|
// Upgrade "help" to "seeking" if it appears in both (or keep as-is — use seeking)
|
|
existing.state = 'seeking'
|
|
} else {
|
|
topics.push({ tagSlug: slug, state: 'seeking' })
|
|
}
|
|
}
|
|
totalTagsMatched += matched.length
|
|
if (unmatched.length > 0) {
|
|
memberUnmatched.push(...unmatched.map((t) => `lookingFor: "${t}"`))
|
|
}
|
|
}
|
|
|
|
if (memberUnmatched.length > 0) {
|
|
allUnmatched.push({ member: label, tags: memberUnmatched })
|
|
}
|
|
|
|
// Build communityConnections update
|
|
const ccUpdate = {}
|
|
|
|
if (topics.length > 0) {
|
|
ccUpdate['communityConnections.topics'] = topics
|
|
}
|
|
|
|
if (typeof member.peerSupport?.enabled === 'boolean') {
|
|
ccUpdate['communityConnections.offerPeerSupport'] = member.peerSupport.enabled
|
|
}
|
|
|
|
if (member.peerSupport?.availability) {
|
|
ccUpdate['communityConnections.availability'] = member.peerSupport.availability
|
|
}
|
|
|
|
if (member.peerSupport?.personalMessage) {
|
|
ccUpdate['communityConnections.personalMessage'] = member.peerSupport.personalMessage
|
|
}
|
|
|
|
if (member.peerSupport?.slackUsername) {
|
|
ccUpdate['communityConnections.slackHandle'] = member.peerSupport.slackUsername
|
|
}
|
|
|
|
if (Object.keys(ccUpdate).length === 0) {
|
|
console.log(` skip ${label} — nothing to migrate`)
|
|
skippedCount++
|
|
continue
|
|
}
|
|
|
|
await Member.findByIdAndUpdate(
|
|
member._id,
|
|
{ $set: ccUpdate },
|
|
{ runValidators: false }
|
|
)
|
|
|
|
console.log(
|
|
` migrated ${label}` +
|
|
(topics.length > 0 ? ` — ${topics.length} topic(s)` : '') +
|
|
(memberUnmatched.length > 0 ? ` — ${memberUnmatched.length} unmatched` : '')
|
|
)
|
|
migratedCount++
|
|
}
|
|
|
|
console.log('\n=== Migration Summary ===')
|
|
console.log(` Total candidates: ${members.length}`)
|
|
console.log(` Migrated: ${migratedCount}`)
|
|
console.log(` Skipped: ${skippedCount}`)
|
|
console.log(` Tags matched: ${totalTagsMatched}`)
|
|
|
|
if (allUnmatched.length > 0) {
|
|
console.log(`\n Unmatched tags (${allUnmatched.length} member(s)):`)
|
|
for (const { member, tags } of allUnmatched) {
|
|
console.log(` ${member}`)
|
|
for (const t of tags) {
|
|
console.log(` - ${t}`)
|
|
}
|
|
}
|
|
} else {
|
|
console.log(' Unmatched tags: none')
|
|
}
|
|
}
|
|
|
|
migrateCommunityConnections()
|
|
.then(() => {
|
|
console.log('\nMigration completed successfully')
|
|
process.exit(0)
|
|
})
|
|
.catch((err) => {
|
|
console.error('\nMigration failed:', err)
|
|
process.exit(1)
|
|
})
|
|
.finally(() => {
|
|
mongoose.connection.close()
|
|
})
|