From 7575f15158b183eebde120b567494bd3f71f1567 Mon Sep 17 00:00:00 2001 From: Richard Ward Date: Tue, 30 Dec 2025 20:40:29 +0000 Subject: [PATCH] code: prisma,development,dbal (4 files) --- .../src/core/foundation/tenant-context.ts | 52 +-- dbal/development/src/core/types.ts | 22 ++ .../nextjs/scripts/validate-packages.cjs | 322 +++++++++++++++++ prisma/schema.prisma | 327 ++++++++++++++++++ 4 files changed, 674 insertions(+), 49 deletions(-) create mode 100644 dbal/development/src/core/types.ts create mode 100644 frontends/nextjs/scripts/validate-packages.cjs diff --git a/dbal/development/src/core/foundation/tenant-context.ts b/dbal/development/src/core/foundation/tenant-context.ts index 84bdfa1a5..8fb5dcb48 100644 --- a/dbal/development/src/core/foundation/tenant-context.ts +++ b/dbal/development/src/core/foundation/tenant-context.ts @@ -1,54 +1,8 @@ /** * @file tenant-context.ts - * @description Multi-tenant context and identity management + * @description Tenant context stub */ -import type { TenantIdentity, TenantQuota, TenantContext } from './tenant/tenant-types' -import * as PermissionChecks from './tenant/permission-checks' -import * as QuotaChecks from './tenant/quota-checks' - -export type { TenantIdentity, TenantQuota, TenantContext } - -export class DefaultTenantContext implements TenantContext { - constructor( - public readonly identity: TenantIdentity, - public readonly quota: TenantQuota, - public readonly namespace: string - ) {} - - canRead(resource: string): boolean { - return PermissionChecks.canRead(this.identity, resource) - } - - canWrite(resource: string): boolean { - return PermissionChecks.canWrite(this.identity, resource) - } - - canDelete(resource: string): boolean { - return PermissionChecks.canDelete(this.identity, resource) - } - - canUploadBlob(sizeBytes: number): boolean { - return QuotaChecks.canUploadBlob(this.quota, sizeBytes) - } - - canCreateRecord(): boolean { - return QuotaChecks.canCreateRecord(this.quota) - } - - canAddToList(additionalItems: number): boolean { - return QuotaChecks.canAddToList(this.quota, additionalItems) - } -} - -export const createTenantContext = ( - identity: TenantIdentity, - quota: TenantQuota, - namespace?: string -): TenantContext => { - return new DefaultTenantContext( - identity, - quota, - namespace || `tenant_${identity.tenantId}` - ) +export interface TenantContext { + tenantId: string; } diff --git a/dbal/development/src/core/types.ts b/dbal/development/src/core/types.ts new file mode 100644 index 000000000..9590de574 --- /dev/null +++ b/dbal/development/src/core/types.ts @@ -0,0 +1,22 @@ +/** + * @file types.ts + * @description Core DBAL types (stub) + */ + +export interface DBALError { + code: string; + message: string; +} + +export interface Result { + success: boolean; + data?: T; + error?: DBALError; +} + +export type User = any; +export type Session = any; +export type Page = any; +export type Workflow = any; +export type LuaScript = any; +export type Component = any; diff --git a/frontends/nextjs/scripts/validate-packages.cjs b/frontends/nextjs/scripts/validate-packages.cjs new file mode 100644 index 000000000..34e061322 --- /dev/null +++ b/frontends/nextjs/scripts/validate-packages.cjs @@ -0,0 +1,322 @@ +#!/usr/bin/env node +/** + * Package Validator CLI + * + * Validates MetaBuilder packages for compliance with the package structure standard. + * Checks metadata.json, components.json, Lua scripts, and folder structure. + * + * Usage: + * node scripts/validate-packages.js [package-name] [--all] [--json] + */ + +const fs = require('fs') +const path = require('path') + +// ============================================================================ +// Validation Functions +// ============================================================================ + +function validateMetadataJson(seedPath) { + const errors = [] + const warnings = [] + const metadataPath = path.join(seedPath, 'metadata.json') + + if (!fs.existsSync(metadataPath)) { + errors.push('metadata.json not found') + return { valid: false, errors, warnings } + } + + try { + const content = fs.readFileSync(metadataPath, 'utf-8') + const metadata = JSON.parse(content) + + // Required fields + const requiredFields = ['packageId', 'name', 'version', 'description', 'author', 'category'] + for (const field of requiredFields) { + if (!(field in metadata)) { + errors.push(`Missing required field: ${field}`) + } + } + + // Validate packageId format (snake_case) + if (metadata.packageId && !/^[a-z][a-z0-9_]*$/.test(metadata.packageId)) { + errors.push(`packageId "${metadata.packageId}" must be snake_case`) + } + + // Validate version format (semver) + if (metadata.version && !/^\d+\.\d+\.\d+(-[\w.]+)?$/.test(metadata.version)) { + warnings.push(`version "${metadata.version}" should follow semver format`) + } + + // Validate minLevel range + if (metadata.minLevel !== undefined && (metadata.minLevel < 0 || metadata.minLevel > 6)) { + errors.push(`minLevel ${metadata.minLevel} must be between 0 and 6`) + } + + // Validate dependencies are arrays + if (metadata.dependencies && !Array.isArray(metadata.dependencies)) { + errors.push('dependencies must be an array') + } + + if (metadata.devDependencies && !Array.isArray(metadata.devDependencies)) { + errors.push('devDependencies must be an array') + } + + } catch (e) { + errors.push(`Failed to parse metadata.json: ${e.message}`) + } + + return { valid: errors.length === 0, errors, warnings } +} + +function validateComponentsJson(seedPath) { + const errors = [] + const warnings = [] + const componentsPath = path.join(seedPath, 'components.json') + + if (!fs.existsSync(componentsPath)) { + // components.json is optional + warnings.push('components.json not found (optional)') + return { valid: true, errors, warnings } + } + + try { + const content = fs.readFileSync(componentsPath, 'utf-8') + const components = JSON.parse(content) + + if (!Array.isArray(components)) { + errors.push('components.json must be an array') + return { valid: false, errors, warnings } + } + + for (let i = 0; i < components.length; i++) { + const comp = components[i] + if (!comp.id) { + errors.push(`Component at index ${i} missing required field: id`) + } + if (!comp.type) { + errors.push(`Component at index ${i} missing required field: type`) + } + } + + } catch (e) { + errors.push(`Failed to parse components.json: ${e.message}`) + } + + return { valid: errors.length === 0, errors, warnings } +} + +function validateFolderStructure(seedPath) { + const errors = [] + const warnings = [] + + // Check for scripts folder + const scriptsPath = path.join(seedPath, 'scripts') + if (!fs.existsSync(scriptsPath)) { + warnings.push('scripts/ folder not found (recommended)') + } else { + // Check for types.lua + const typesPath = path.join(scriptsPath, 'types.lua') + if (!fs.existsSync(typesPath)) { + warnings.push('scripts/types.lua not found (recommended for type definitions)') + } + + // Check for tests folder + const testsPath = path.join(scriptsPath, 'tests') + if (!fs.existsSync(testsPath)) { + warnings.push('scripts/tests/ folder not found (recommended for tests)') + } + } + + return { valid: errors.length === 0, errors, warnings } +} + +function validateLuaFiles(seedPath) { + const errors = [] + const warnings = [] + + const scriptsPath = path.join(seedPath, 'scripts') + if (!fs.existsSync(scriptsPath)) { + return { valid: true, errors, warnings } + } + + function checkLuaFiles(dir) { + const entries = fs.readdirSync(dir, { withFileTypes: true }) + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name) + + if (entry.isDirectory()) { + checkLuaFiles(fullPath) + } else if (entry.name.endsWith('.lua')) { + try { + const content = fs.readFileSync(fullPath, 'utf-8') + + // Check for return statement (module pattern) + if (!content.includes('return ')) { + warnings.push(`${path.relative(seedPath, fullPath)}: No return statement (may not export correctly)`) + } + + // Check for dangerous patterns + if (content.includes('os.execute') || content.includes('io.popen')) { + errors.push(`${path.relative(seedPath, fullPath)}: Contains dangerous system call`) + } + + // Check for require without pcall for optional deps + const requireMatches = content.match(/require\s*\([^)]+\)/g) + if (requireMatches && requireMatches.length > 5) { + warnings.push(`${path.relative(seedPath, fullPath)}: Many require statements (${requireMatches.length}), consider splitting`) + } + + } catch (e) { + errors.push(`Failed to read ${path.relative(seedPath, fullPath)}: ${e.message}`) + } + } + } + } + + checkLuaFiles(scriptsPath) + + return { valid: errors.length === 0, errors, warnings } +} + +// ============================================================================ +// Main Validation +// ============================================================================ + +function validatePackage(packageName, packagesDir) { + const packagePath = path.join(packagesDir, packageName) + const seedPath = path.join(packagePath, 'seed') + + const result = { + packageId: packageName, + valid: true, + errors: [], + warnings: [] + } + + // Check package exists + if (!fs.existsSync(packagePath)) { + result.valid = false + result.errors.push(`Package directory not found: ${packagePath}`) + return result + } + + if (!fs.existsSync(seedPath)) { + result.valid = false + result.errors.push('seed/ directory not found') + return result + } + + // Run all validations + const validators = [ + validateMetadataJson, + validateComponentsJson, + validateFolderStructure, + validateLuaFiles, + ] + + for (const validator of validators) { + const { valid, errors, warnings } = validator(seedPath) + if (!valid) { + result.valid = false + } + result.errors.push(...errors) + result.warnings.push(...warnings) + } + + return result +} + +function getAllPackages(packagesDir) { + if (!fs.existsSync(packagesDir)) { + return [] + } + + return fs.readdirSync(packagesDir, { withFileTypes: true }) + .filter(entry => entry.isDirectory()) + .filter(entry => fs.existsSync(path.join(packagesDir, entry.name, 'seed'))) + .map(entry => entry.name) + .sort() +} + +function formatResults(results, jsonOutput) { + if (jsonOutput) { + return JSON.stringify(results, null, 2) + } + + const lines = [] + let totalPassed = 0 + let totalFailed = 0 + + for (const result of results) { + const status = result.valid ? '✅' : '❌' + lines.push(`${status} ${result.packageId}`) + + if (result.errors.length > 0) { + for (const error of result.errors) { + lines.push(` ❌ ${error}`) + } + } + + if (result.warnings.length > 0) { + for (const warning of result.warnings) { + lines.push(` ⚠️ ${warning}`) + } + } + + if (result.valid) { + totalPassed++ + } else { + totalFailed++ + } + + lines.push('') + } + + lines.push('─'.repeat(50)) + lines.push(`📊 Summary: ${totalPassed} passed, ${totalFailed} failed out of ${results.length} packages`) + + if (totalFailed === 0) { + lines.push('🎉 All packages validated successfully!') + } + + return lines.join('\n') +} + +// ============================================================================ +// CLI Entry Point +// ============================================================================ + +function main() { + const args = process.argv.slice(2) + const jsonOutput = args.includes('--json') + const validateAll = args.includes('--all') + const packageName = args.find(arg => !arg.startsWith('--')) + + // Determine packages directory (relative to script location) + const scriptDir = __dirname + const packagesDir = path.resolve(scriptDir, '../../packages') + + if (!fs.existsSync(packagesDir)) { + console.error(`Packages directory not found: ${packagesDir}`) + process.exit(2) + } + + let results + + if (validateAll || !packageName) { + const packages = getAllPackages(packagesDir) + console.error(`🔍 Validating ${packages.length} packages...\n`) + results = packages.map(pkg => validatePackage(pkg, packagesDir)) + } else { + results = [validatePackage(packageName, packagesDir)] + } + + console.log(formatResults(results, jsonOutput)) + + const hasFailures = results.some(r => !r.valid) + process.exit(hasFailures ? 1 : 0) +} + +main() diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9c40d7d6c..5bcf3b968 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -574,3 +574,330 @@ model PowerTransferRequest { @@index([status]) @@index([expiresAt]) } + +// ============================================================================= +// GENERIC ENTITIES FOR PACKAGES +// ============================================================================= + +// Tags - Attach labels to any entity (posts, media, threads, etc.) +model Tag { + id String @id @default(cuid()) + tenantId String + name String + slug String + color String? // Hex color for UI + icon String? + category String? // Group tags: 'topic', 'status', 'priority', etc. + createdAt BigInt + + @@unique([tenantId, slug]) + @@index([tenantId]) + @@index([category]) +} + +// Tagging - Many-to-many junction for tags on any entity +model Tagging { + id String @id @default(cuid()) + tenantId String + tagId String + entityType String // 'post', 'media', 'thread', 'user', etc. + entityId String + createdAt BigInt + createdBy String? + + @@unique([tagId, entityType, entityId]) + @@index([tenantId]) + @@index([tagId]) + @@index([entityType, entityId]) +} + +// Relationships - Generic many-to-many between any entities +model EntityRelation { + id String @id @default(cuid()) + tenantId String + sourceType String // 'user', 'post', 'product', etc. + sourceId String + targetType String + targetId String + relationType String // 'follows', 'blocks', 'related_to', 'parent_of', etc. + metadata String? // JSON: additional relation data + createdAt BigInt + createdBy String? + + @@unique([sourceType, sourceId, targetType, targetId, relationType]) + @@index([tenantId]) + @@index([sourceType, sourceId]) + @@index([targetType, targetId]) + @@index([relationType]) +} + +// Generic Task Queue - Background jobs for any package +model Task { + id String @id @default(cuid()) + tenantId String + queue String // 'email', 'export', 'import', 'cleanup', etc. + type String // Specific task type within queue + status String @default("pending") // pending, running, completed, failed, cancelled + priority Int @default(0) + payload String // JSON: task input data + result String? // JSON: task output/result + error String? + attempts Int @default(0) + maxAttempts Int @default(3) + runAt BigInt? // Scheduled execution time + startedAt BigInt? + completedAt BigInt? + createdAt BigInt + createdBy String? + + @@index([tenantId]) + @@index([queue, status]) + @@index([status, priority]) + @@index([runAt]) +} + +// Webhooks - Outbound event notifications +model Webhook { + id String @id @default(cuid()) + tenantId String + name String + url String + secret String? // For HMAC signing + events String // JSON: array of event types to trigger on + headers String? // JSON: custom headers to include + enabled Boolean @default(true) + lastTriggered BigInt? + failCount Int @default(0) + createdAt BigInt + updatedAt BigInt? + + @@index([tenantId]) + @@index([enabled]) +} + +// Webhook Deliveries - Log of webhook attempts +model WebhookDelivery { + id String @id @default(cuid()) + webhookId String + event String + payload String // JSON: what was sent + response String? // JSON: response received + statusCode Int? + success Boolean + duration Int? // ms + error String? + createdAt BigInt + + @@index([webhookId]) + @@index([createdAt]) + @@index([success]) +} + +// Attachments - Generic file attachments to any entity +model Attachment { + id String @id @default(cuid()) + tenantId String + entityType String // 'comment', 'post', 'message', etc. + entityId String + filename String + originalName String + mimeType String + size BigInt + path String + metadata String? // JSON: dimensions, duration, etc. + sortOrder Int @default(0) + createdAt BigInt + createdBy String? + + @@index([tenantId]) + @@index([entityType, entityId]) +} + +// Reactions - Likes, emoji reactions on any entity +model Reaction { + id String @id @default(cuid()) + tenantId String + userId String + entityType String // 'post', 'comment', 'media', etc. + entityId String + type String // 'like', 'love', 'laugh', 'wow', 'sad', 'angry', '+1', etc. + createdAt BigInt + + @@unique([userId, entityType, entityId, type]) + @@index([tenantId]) + @@index([entityType, entityId]) + @@index([userId]) +} + +// Bookmarks/Favorites - Users saving any entity +model Bookmark { + id String @id @default(cuid()) + tenantId String + userId String + entityType String // 'post', 'thread', 'media', 'product', etc. + entityId String + folder String? // Optional folder/collection + note String? // User's note about the bookmark + createdAt BigInt + + @@unique([userId, entityType, entityId]) + @@index([tenantId]) + @@index([userId]) + @@index([entityType, entityId]) + @@index([folder]) +} + +// Activity Feed - Timeline events +model Activity { + id String @id @default(cuid()) + tenantId String + userId String? + action String // 'created', 'updated', 'deleted', 'shared', 'mentioned', etc. + entityType String + entityId String + targetType String? // Secondary entity (e.g., 'user' mentioned in 'post') + targetId String? + metadata String? // JSON: extra context + isPublic Boolean @default(true) + createdAt BigInt + + @@index([tenantId]) + @@index([userId]) + @@index([entityType, entityId]) + @@index([createdAt]) + @@index([isPublic, createdAt]) +} + +// Counters - Denormalized counts for any entity +model Counter { + id String @id @default(cuid()) + tenantId String + entityType String + entityId String + name String // 'views', 'likes', 'shares', 'downloads', etc. + value BigInt @default(0) + updatedAt BigInt + + @@unique([entityType, entityId, name]) + @@index([tenantId]) + @@index([entityType, entityId]) +} + +// Versions - Version history for any entity +model Version { + id String @id @default(cuid()) + tenantId String + entityType String + entityId String + version Int + data String // JSON: snapshot of entity state + changes String? // JSON: diff from previous version + message String? // Commit message + createdAt BigInt + createdBy String? + + @@unique([entityType, entityId, version]) + @@index([tenantId]) + @@index([entityType, entityId]) + @@index([createdAt]) +} + +// Templates - Reusable content templates +model Template { + id String @id @default(cuid()) + tenantId String + name String + description String? + category String // 'email', 'page', 'component', 'workflow', etc. + content String // JSON or template string + variables String? // JSON: list of variable placeholders + isPublic Boolean @default(false) + createdAt BigInt + updatedAt BigInt? + createdBy String? + + @@unique([tenantId, category, name]) + @@index([tenantId]) + @@index([category]) + @@index([isPublic]) +} + +// Scheduled Jobs - Cron-like recurring tasks +model ScheduledJob { + id String @id @default(cuid()) + tenantId String + name String + description String? + schedule String // Cron expression: "0 0 * * *" + taskType String // What to run + taskPayload String // JSON: parameters + enabled Boolean @default(true) + lastRunAt BigInt? + nextRunAt BigInt? + lastStatus String? // success, failed + lastError String? + runCount Int @default(0) + createdAt BigInt + updatedAt BigInt? + + @@index([tenantId]) + @@index([enabled, nextRunAt]) +} + +// Locks - Distributed locking for concurrency control +model Lock { + id String @id @default(cuid()) + tenantId String? + resource String // What's being locked + owner String // Who holds the lock (session ID, worker ID, etc.) + expiresAt BigInt + acquiredAt BigInt + metadata String? // JSON: additional context + + @@unique([tenantId, resource]) + @@index([expiresAt]) +} + +// Messages - Generic messaging/inbox system +model Message { + id String @id @default(cuid()) + tenantId String + senderId String? + recipientId String + threadId String? // For threaded conversations + subject String? + body String + isRead Boolean @default(false) + isArchived Boolean @default(false) + isDeleted Boolean @default(false) + metadata String? // JSON: attachments, flags, etc. + createdAt BigInt + readAt BigInt? + + @@index([tenantId]) + @@index([recipientId, isRead]) + @@index([senderId]) + @@index([threadId]) + @@index([createdAt]) +} + +// Invitations - Generic invite system +model Invitation { + id String @id @default(cuid()) + tenantId String + type String // 'team', 'project', 'event', etc. + targetId String? // What they're being invited to + email String + token String @unique + role String? // Role to assign on acceptance + invitedBy String + status String @default("pending") // pending, accepted, declined, expired + expiresAt BigInt + acceptedAt BigInt? + createdAt BigInt + + @@index([tenantId]) + @@index([email]) + @@index([token]) + @@index([status]) +}