From 1da59021a3e71f585c8949b38c605b8fbdb0de85 Mon Sep 17 00:00:00 2001 From: Jennie Robinson Faber Date: Tue, 14 Apr 2026 16:21:04 +0100 Subject: [PATCH] feat(board): add BoardPost + BoardChannel models and zod schemas - Add BoardPost model (author, title, seeking/offering, note, tags) with validator requiring at least one of seeking/offering - Add BoardChannel model (name, slackChannelId, tagSlugs) - Add boardPost/boardChannel create+update Zod schemas - Trim Member.board subdoc to only slackHandle (drop topics, details, offerPeerSupport, availability, personalMessage) - Remove old boardUpdateSchema --- server/models/boardChannel.js | 9 ++++++++ server/models/boardPost.js | 28 ++++++++++++++++++++++++ server/models/member.js | 10 --------- server/utils/schemas.js | 41 ++++++++++++++++++++++++++--------- 4 files changed, 68 insertions(+), 20 deletions(-) create mode 100644 server/models/boardChannel.js create mode 100644 server/models/boardPost.js diff --git a/server/models/boardChannel.js b/server/models/boardChannel.js new file mode 100644 index 0000000..d3b1255 --- /dev/null +++ b/server/models/boardChannel.js @@ -0,0 +1,9 @@ +import mongoose from 'mongoose' + +const boardChannelSchema = new mongoose.Schema({ + name: { type: String, required: true }, + slackChannelId: { type: String, required: true }, + tagSlugs: [String], +}, { timestamps: true }) + +export default mongoose.models.BoardChannel || mongoose.model('BoardChannel', boardChannelSchema) diff --git a/server/models/boardPost.js b/server/models/boardPost.js new file mode 100644 index 0000000..7ea7080 --- /dev/null +++ b/server/models/boardPost.js @@ -0,0 +1,28 @@ +import mongoose from 'mongoose' + +const boardPostSchema = new mongoose.Schema({ + author: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Member', + required: true, + }, + title: { type: String, required: true, maxlength: 120 }, + seeking: { type: String, maxlength: 500 }, + offering: { type: String, maxlength: 500 }, + note: { type: String, maxlength: 300 }, + tags: [String], +}, { timestamps: true }) + +boardPostSchema.pre('validate', function (next) { + const seeking = (this.seeking || '').trim() + const offering = (this.offering || '').trim() + if (!seeking && !offering) { + this.invalidate('seeking', 'At least one of seeking or offering must be provided') + } + next() +}) + +boardPostSchema.index({ author: 1 }) +boardPostSchema.index({ createdAt: -1 }) + +export default mongoose.models.BoardPost || mongoose.model('BoardPost', boardPostSchema) diff --git a/server/models/member.js b/server/models/member.js index 59367ab..9de5188 100644 --- a/server/models/member.js +++ b/server/models/member.js @@ -73,17 +73,7 @@ const memberSchema = new mongoose.Schema({ craftTags: [String], board: { - topics: [ - { - tagSlug: String, - state: { type: String, enum: ['help', 'interested', 'seeking'] }, - }, - ], - offerPeerSupport: { type: Boolean, default: false }, - availability: String, slackHandle: String, - personalMessage: String, - details: String, }, // Privacy settings for profile fields diff --git a/server/utils/schemas.js b/server/utils/schemas.js index f152988..018cae3 100644 --- a/server/utils/schemas.js +++ b/server/utils/schemas.js @@ -377,16 +377,37 @@ export const tagSuggestionSchema = z.object({ pool: z.enum(['craft', 'cooperative']) }) -export const boardUpdateSchema = z.object({ - topics: z.array(z.object({ - tagSlug: z.string().min(1).max(100), - state: z.enum(['help', 'interested', 'seeking']) - })).max(20).optional(), - offerPeerSupport: z.boolean().optional(), - availability: z.string().max(500).optional(), - slackHandle: z.string().max(200).optional(), - personalMessage: z.string().max(2000).optional(), - details: z.string().max(300).optional() +// --- Board post / channel schemas --- + +export const boardPostCreateSchema = z.object({ + title: z.string().trim().min(1).max(120), + seeking: z.string().max(500).optional(), + offering: z.string().max(500).optional(), + note: z.string().max(300).optional(), + tags: z.array(z.string().max(100)).optional().default([]) +}).refine( + (data) => (data.seeking || '').trim().length > 0 || (data.offering || '').trim().length > 0, + { message: 'At least one of seeking or offering must be provided', path: ['seeking'] } +) + +export const boardPostUpdateSchema = z.object({ + title: z.string().trim().min(1).max(120).optional(), + seeking: z.string().max(500).optional(), + offering: z.string().max(500).optional(), + note: z.string().max(300).optional(), + tags: z.array(z.string().max(100)).optional() +}) + +export const boardChannelCreateSchema = z.object({ + name: z.string().trim().min(1).max(200), + slackChannelId: z.string().trim().min(1).max(50).regex(/^[A-Z0-9]+$/, 'Invalid Slack channel ID'), + tagSlugs: z.array(z.string().max(100)).optional().default([]) +}) + +export const boardChannelUpdateSchema = z.object({ + name: z.string().trim().min(1).max(200).optional(), + slackChannelId: z.string().trim().min(1).max(50).regex(/^[A-Z0-9]+$/, 'Invalid Slack channel ID').optional(), + tagSlugs: z.array(z.string().max(100)).optional() }) // --- Admin alert schemas ---