mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-05-04 18:54:53 +00:00
code: prisma,development,dbal (4 files)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user