Holded_Blog / data /schema.js
Shinhati2023's picture
Rename styles/data/schema.js to data/schema.js
fd715ee verified
/* ============================================================
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 };