refactor(community): rename Community Connections → Community Ecology
Simplify the feature to pure discovery (filter by topic, see matching members, copy Slack handle). Drop the connection request/confirm flow entirely — Connection model, 7 API endpoints, useConnections composable, and TagInput component deleted. - Rename communityConnections → communityEcology in schema, API, pages - Delete legacy fields: offering, lookingFor, peerSupport - New /ecology page, /api/ecology/suggestions, community-ecology.patch - Nav: "Connections" → "Ecology", remove pending-count badge - Fix auth/member.get.js missing craftTags + communityEcology - Add community_ecology_updated activity log type - Expose slackHandle conditionally when offerPeerSupport is true - Add migration script at scripts/migrate-to-ecology.js (run before deploy)
This commit is contained in:
parent
9577929e0d
commit
0b3896d984
33 changed files with 1002 additions and 2635 deletions
|
|
@ -1,62 +0,0 @@
|
|||
// Migration to fix offering and lookingFor field structure
|
||||
// Run this once to convert string values to object structure
|
||||
import mongoose from "mongoose";
|
||||
import Member from "../models/member.js";
|
||||
import { connectDB } from "../utils/mongoose.js";
|
||||
|
||||
async function migrateOfferingLookingFor() {
|
||||
await connectDB();
|
||||
|
||||
console.log("Starting migration: fixing offering and lookingFor structure...");
|
||||
|
||||
try {
|
||||
// Find all members where offering or lookingFor is a string (not an object)
|
||||
const members = await Member.find({
|
||||
$or: [
|
||||
{ offering: { $type: "string" } },
|
||||
{ lookingFor: { $type: "string" } },
|
||||
],
|
||||
});
|
||||
|
||||
console.log(`Found ${members.length} members to migrate`);
|
||||
|
||||
for (const member of members) {
|
||||
const updates = {};
|
||||
|
||||
// Convert offering if it's a string
|
||||
if (typeof member.offering === "string") {
|
||||
updates.offering = {
|
||||
text: member.offering,
|
||||
tags: [],
|
||||
};
|
||||
console.log(
|
||||
`Converting offering for member ${member._id}: "${member.offering}"`,
|
||||
);
|
||||
}
|
||||
|
||||
// Convert lookingFor if it's a string
|
||||
if (typeof member.lookingFor === "string") {
|
||||
updates.lookingFor = {
|
||||
text: member.lookingFor,
|
||||
tags: [],
|
||||
};
|
||||
console.log(
|
||||
`Converting lookingFor for member ${member._id}: "${member.lookingFor}"`,
|
||||
);
|
||||
}
|
||||
|
||||
// Update the member
|
||||
if (Object.keys(updates).length > 0) {
|
||||
await Member.findByIdAndUpdate(member._id, { $set: updates });
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Migration completed successfully!");
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error("Migration failed:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
migrateOfferingLookingFor();
|
||||
|
|
@ -1,213 +0,0 @@
|
|||
/**
|
||||
* 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()
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue