code: prisma,development,dbal (4 files)

This commit is contained in:
Richard Ward
2025-12-30 20:40:29 +00:00
parent 92f5aaf22b
commit 7575f15158
4 changed files with 674 additions and 49 deletions
@@ -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;
}
+22
View File
@@ -0,0 +1,22 @@
/**
* @file types.ts
* @description Core DBAL types (stub)
*/
export interface DBALError {
code: string;
message: string;
}
export interface Result<T> {
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;
@@ -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()
+327
View File
@@ -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])
}