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
This commit is contained in:
Jennie Robinson Faber 2026-04-14 16:21:04 +01:00
parent 19d519b153
commit 1da59021a3
4 changed files with 68 additions and 20 deletions

View file

@ -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)

View file

@ -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)

View file

@ -73,17 +73,7 @@ const memberSchema = new mongoose.Schema({
craftTags: [String], craftTags: [String],
board: { board: {
topics: [
{
tagSlug: String,
state: { type: String, enum: ['help', 'interested', 'seeking'] },
},
],
offerPeerSupport: { type: Boolean, default: false },
availability: String,
slackHandle: String, slackHandle: String,
personalMessage: String,
details: String,
}, },
// Privacy settings for profile fields // Privacy settings for profile fields

View file

@ -377,16 +377,37 @@ export const tagSuggestionSchema = z.object({
pool: z.enum(['craft', 'cooperative']) pool: z.enum(['craft', 'cooperative'])
}) })
export const boardUpdateSchema = z.object({ // --- Board post / channel schemas ---
topics: z.array(z.object({
tagSlug: z.string().min(1).max(100), export const boardPostCreateSchema = z.object({
state: z.enum(['help', 'interested', 'seeking']) title: z.string().trim().min(1).max(120),
})).max(20).optional(), seeking: z.string().max(500).optional(),
offerPeerSupport: z.boolean().optional(), offering: z.string().max(500).optional(),
availability: z.string().max(500).optional(), note: z.string().max(300).optional(),
slackHandle: z.string().max(200).optional(), tags: z.array(z.string().max(100)).optional().default([])
personalMessage: z.string().max(2000).optional(), }).refine(
details: z.string().max(300).optional() (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 --- // --- Admin alert schemas ---