/* ============================================================ GLASSGRID — CONTENT SCHEMA DEFINITIONS ⚠️ ARCHITECTURE RULE: All content is structured via these schemas. Components read schema-validated objects only. Raw HTML is NEVER stored or injected. ============================================================ */ 'use strict'; const Schema = { // ── Post ────────────────────────────────────────── Post: { id: { type: 'string', required: true }, userId: { type: 'string', required: true }, mediaUrl: { type: 'string', required: true }, // Image URL only mediaType: { type: 'enum', values: ['image'], default: 'image' }, aspectRatio: { type: 'enum', values: ['square', 'landscape', 'portrait'], default: 'square' }, caption: { type: 'string', required: false, maxLength: 2200 }, altText: { type: 'string', required: false, maxLength: 500 }, tags: { type: 'array', items: 'string', default: [] }, location: { type: 'string', required: false, maxLength: 100 }, likeCount: { type: 'number', default: 0 }, commentCount:{ type: 'number', default: 0 }, saveCount: { type: 'number', default: 0 }, createdAt: { type: 'timestamp', required: true }, isArchived: { type: 'boolean', default: false }, isFeatured: { type: 'boolean', default: false }, }, // ── User ────────────────────────────────────────── User: { id: { type: 'string', required: true }, username: { type: 'string', required: true, pattern: /^[a-z0-9_.]{3,30}$/ }, displayName: { type: 'string', required: true, maxLength: 60 }, avatarUrl: { type: 'string', required: false }, bio: { type: 'string', required: false, maxLength: 160 }, location: { type: 'string', required: false, maxLength: 100 }, website: { type: 'string', required: false }, followersCount:{ type: 'number', default: 0 }, followingCount:{ type: 'number', default: 0 }, postsCount: { type: 'number', default: 0 }, isVerified: { type: 'boolean', default: false }, isPrivate: { type: 'boolean', default: false }, role: { type: 'enum', values: ['user', 'moderator', 'admin'], default: 'user' }, joinedAt: { type: 'timestamp', required: true }, isBanned: { type: 'boolean', default: false }, }, // ── Comment ─────────────────────────────────────── Comment: { id: { type: 'string', required: true }, postId: { type: 'string', required: true }, userId: { type: 'string', required: true }, text: { type: 'string', required: true, maxLength: 500 }, likeCount: { type: 'number', default: 0 }, parentId: { type: 'string', required: false }, // For replies createdAt: { type: 'timestamp', required: true }, isHidden: { type: 'boolean', default: false }, isFlagged: { type: 'boolean', default: false }, }, // ── Like ────────────────────────────────────────── Like: { id: { type: 'string', required: true }, userId: { type: 'string', required: true }, targetId: { type: 'string', required: true }, // Post or Comment ID targetType:{ type: 'enum', values: ['post', 'comment'], required: true }, createdAt: { type: 'timestamp', required: true }, }, // ── Story ───────────────────────────────────────── Story: { id: { type: 'string', required: true }, userId: { type: 'string', required: true }, mediaUrl: { type: 'string', required: true }, expiresAt: { type: 'timestamp', required: true }, viewCount: { type: 'number', default: 0 }, isHighlight: { type: 'boolean', default: false }, }, // ── SiteConfig ──────────────────────────────────── SiteConfig: { siteName: { type: 'string', default: 'GlassGrid' }, tagline: { type: 'string', default: 'Share Your World' }, logoUrl: { type: 'string', default: '' }, activeTheme: { type: 'string', default: 'midnight' }, allowSignup: { type: 'boolean', default: true }, features: { likes: { type: 'boolean', default: true }, comments: { type: 'boolean', default: true }, stories: { type: 'boolean', default: true }, explore: { type: 'boolean', default: true }, saves: { type: 'boolean', default: true }, directMessages: { type: 'boolean', default: false }, }, moderation: { requireApproval: { type: 'boolean', default: false }, autoHideFlags: { type: 'boolean', default: true }, flagThreshold: { type: 'number', default: 3 }, allowedDomains: { type: 'array', items: 'string', default: [] }, }, limits: { maxPostsPerDay: { type: 'number', default: 20 }, maxCaptionLength:{ type: 'number', default: 2200 }, maxCommentLength:{ type: 'number', default: 500 }, maxBioLength: { type: 'number', default: 160 }, }, }, }; /** * Validate a data object against a schema definition. * Returns { valid: bool, errors: string[] } */ function validate(schemaName, data) { const schema = Schema[schemaName]; if (!schema) return { valid: false, errors: [`Unknown schema: ${schemaName}`] }; const errors = []; for (const [key, rules] of Object.entries(schema)) { if (typeof rules !== 'object' || Array.isArray(rules)) continue; const value = data[key]; const isDefined = value !== undefined && value !== null; if (rules.required && !isDefined) { errors.push(`"${key}" is required`); continue; } if (!isDefined) continue; if (rules.type === 'string' && typeof value !== 'string') errors.push(`"${key}" must be a string`); if (rules.maxLength && typeof value === 'string' && value.length > rules.maxLength) errors.push(`"${key}" exceeds max length of ${rules.maxLength}`); if (rules.type === 'enum' && !rules.values.includes(value)) errors.push(`"${key}" must be one of: ${rules.values.join(', ')}`); if (rules.type === 'number' && typeof value !== 'number') errors.push(`"${key}" must be a number`); if (rules.type === 'boolean' && typeof value !== 'boolean') errors.push(`"${key}" must be a boolean`); if (rules.pattern && typeof value === 'string' && !rules.pattern.test(value)) errors.push(`"${key}" does not match required pattern`); } return { valid: errors.length === 0, errors }; } /** * Apply defaults from schema to a data object. */ function applyDefaults(schemaName, data = {}) { const schema = Schema[schemaName]; if (!schema) return data; const result = { ...data }; for (const [key, rules] of Object.entries(schema)) { if (typeof rules !== 'object' || Array.isArray(rules)) continue; if (result[key] === undefined && rules.default !== undefined) { result[key] = typeof rules.default === 'object' ? JSON.parse(JSON.stringify(rules.default)) : rules.default; } } return result; } export { Schema, validate, applyDefaults };