feat(dbal): generate Prisma schema for email models

- Updated gen_prisma_schema.js to parse YAML entity schemas
- Generated Prisma models for EmailClient, EmailFolder, EmailMessage, EmailAttachment
- All email models include proper indexes and constraints from YAML definitions
- Schema generation now supports dynamic YAML parsing with fallback to core models
This commit is contained in:
2026-01-23 19:33:07 +00:00
parent b767f5612f
commit 6d86b37d3b
2 changed files with 394 additions and 1 deletions

View File

@@ -119,3 +119,257 @@ model PackageData {
packageId String @id
data String // JSON
}
model ComponentNode {
id String? @id
pageId String
parentId String?
type String
childIds String
order String @default(0)
@@index([pageId])
@@index([parentId])
@@index([pageId, order])
}
model Credential {
username String? @id
passwordHash String
@@index([username])
}
model PageConfig {
id String? @id
tenantId String?
packageId String?
path String
title String
description String?
icon String?
component String?
componentTree String
level String
requiresAuth Boolean
requiredRole String?
parentPath String?
sortOrder String @default(0)
isPublished Boolean @default(true)
params String?
meta String?
createdAt BigInt?
updatedAt BigInt?
@@index([path])
@@index([level])
@@index([isPublished])
}
model InstalledPackage {
packageId String? @id
tenantId String?
installedAt BigInt
version String
enabled Boolean
config String?
@@index([tenantId])
}
model Session {
id String? @id
userId String
token String @unique
expiresAt BigInt
createdAt BigInt?
lastActivity BigInt
ipAddress String?
userAgent String?
@@index([token])
@@index([userId])
@@index([expiresAt])
}
model UIPage {
id String? @id
path String
title String
level String
requireAuth Boolean @default(false)
requiredRole String?
layout String
actions String?
packageId String?
isActive Boolean @default(true)
createdAt BigInt?
updatedAt BigInt?
createdBy String?
@@index([path])
@@index([level, isActive])
@@index([packageId])
}
model User {
id String? @id
username String @unique
email String @unique
role String @default("user")
profilePicture String?
bio String?
createdAt BigInt?
tenantId String?
isInstanceOwner Boolean @default(false)
passwordChangeTimestamp BigInt?
firstLogin Boolean @default(false)
@@index([username])
@@index([email])
@@index([role])
@@index([tenantId])
}
model AuditLog {
id String? @id @default(cuid())
tenantId String
userId String?
username String?
action String
entity String
entityId String?
oldValue String?
newValue String?
ipAddress String?
userAgent String?
details String?
timestamp BigInt
@@index([tenantId, timestamp])
@@index([entity, entityId])
@@index([tenantId, userId])
}
model EmailAttachment {
id String? @id @default(cuid())
tenantId String
messageId String
filename String
mimeType String
size BigInt
contentId String?
isInline Boolean? @default(false)
storageKey String @unique
downloadUrl String?
createdAt BigInt?
@@index([messageId])
@@index([tenantId, messageId])
}
model EmailClient {
id String? @id @default(cuid())
tenantId String
userId String
accountName String
emailAddress String @unique
protocol String? @default("imap")
hostname String
port Int
encryption String? @default("tls")
username String
credentialId String
isSyncEnabled Boolean? @default(true)
syncInterval Int? @default(300)
lastSyncAt BigInt?
isSyncing Boolean? @default(false)
isEnabled Boolean? @default(true)
createdAt BigInt?
updatedAt BigInt?
@@index([userId, tenantId])
@@index([emailAddress, tenantId])
}
model EmailFolder {
id String? @id @default(cuid())
tenantId String
emailClientId String
name String
type String? @default("custom")
unreadCount Int? @default(0)
totalCount Int? @default(0)
syncToken String?
isSelectable Boolean? @default(true)
createdAt BigInt?
updatedAt BigInt?
@@index([emailClientId, name])
@@index([tenantId, emailClientId, type])
}
model EmailMessage {
id String? @id @default(cuid())
tenantId String
emailClientId String
folderId String
messageId String
imapUid String?
from String
to String
cc String?
bcc String?
replyTo String?
subject String?
textBody String?
htmlBody String?
headers String?
receivedAt BigInt
isRead Boolean? @default(false)
isStarred Boolean? @default(false)
isSpam Boolean? @default(false)
isDraft Boolean? @default(false)
isSent Boolean? @default(false)
isDeleted Boolean? @default(false)
attachmentCount Int? @default(0)
conversationId String?
labels String?
size BigInt?
createdAt BigInt?
updatedAt BigInt?
@@index([emailClientId, folderId, receivedAt])
@@index([tenantId, isRead, receivedAt])
@@index([conversationId])
}
model Notification {
id String? @id @default(cuid())
tenantId String
userId String
type String
title String
message String
icon String?
read Boolean? @default(false)
data String?
createdAt BigInt
expiresAt BigInt?
@@index([userId, read])
@@index([tenantId, createdAt])
}
model Video {
id String
tenantId String
title String
description String?
uploaderId String
videoUrl String
thumbnailUrl String?
duration String
category String?
tags String?
views String @default(0)
likes String @default(0)
dislikes String @default(0)
published String @default(false)
unlisted String? @default(false)
status String @default("draft")
createdAt String
updatedAt String?
publishedAt String?
@@index([tenantId, publishedAt])
@@index([uploaderId, tenantId])
@@index([tenantId, status])
@@index([createdAt, tenantId])
}

View File

@@ -2,6 +2,7 @@
/* eslint-disable no-console */
const fs = require('fs')
const path = require('path')
const yaml = require('yaml')
const header = `datasource db {
provider = "sqlite"
@@ -11,7 +12,8 @@ generator client {
provider = "prisma-client-js"
}`
const models = [
// Hardcoded core models
const coreModels = [
{
name: 'User',
fields: [
@@ -143,6 +145,143 @@ const models = [
},
]
// Function to convert YAML field type to Prisma type
function yamlTypeToPrismaType(yamlType, isNullable = false, isArray = false) {
const typeMap = {
cuid: 'String',
uuid: 'String',
string: 'String',
int: 'Int',
bigint: 'BigInt',
float: 'Float',
boolean: 'Boolean',
json: 'String', // JSON stored as string in SQLite
text: 'String',
enum: 'String',
}
let prismaType = typeMap[yamlType] || 'String'
if (isArray) prismaType = prismaType + '[]'
if (isNullable) prismaType = prismaType + '?'
return prismaType
}
// Function to load and convert YAML schema to model
function yamlToModel(yamlPath) {
try {
const content = fs.readFileSync(yamlPath, 'utf8')
const yamlData = yaml.parse(content)
if (!yamlData || !yamlData.entity) {
return null
}
const modelName = yamlData.entity
const fields = []
const blockAttributes = []
// Process fields
if (yamlData.fields && typeof yamlData.fields === 'object') {
for (const [fieldName, fieldDef] of Object.entries(yamlData.fields)) {
if (!fieldDef || typeof fieldDef !== 'object') continue
const isNullable = fieldDef.nullable || !fieldDef.required
const fieldType = yamlTypeToPrismaType(fieldDef.type, isNullable)
const attributes = []
// Handle primary key
if (fieldDef.primary) {
attributes.push('@id')
}
// Handle unique constraint
if (fieldDef.unique) {
attributes.push('@unique')
}
// Handle default values
if (fieldDef.default !== undefined) {
let defaultVal = fieldDef.default
if (typeof defaultVal === 'string') {
defaultVal = `"${defaultVal}"`
}
attributes.push(`@default(${defaultVal})`)
}
// Handle generated fields
if (fieldDef.generated) {
if (fieldName === 'id' && fieldDef.type === 'cuid') {
attributes.push('@default(cuid())')
}
}
fields.push({
name: fieldName,
type: fieldType,
attributes: attributes.length > 0 ? attributes : undefined,
})
}
}
// Process indexes
if (Array.isArray(yamlData.indexes)) {
yamlData.indexes.forEach((index) => {
if (index.fields && Array.isArray(index.fields)) {
const fieldList = index.fields.join(', ')
blockAttributes.push(`@@index([${fieldList}])`)
}
})
}
return {
name: modelName,
fields,
blockAttributes: blockAttributes.length > 0 ? blockAttributes : undefined,
}
} catch (err) {
return null
}
}
// Load all YAML entity schemas
function loadYamlModels(entityDir) {
const models = []
function walkDir(dir) {
try {
const files = fs.readdirSync(dir)
files.forEach((file) => {
const fullPath = path.join(dir, file)
const stat = fs.statSync(fullPath)
if (stat.isDirectory()) {
walkDir(fullPath)
} else if (file.endsWith('.yaml')) {
const model = yamlToModel(fullPath)
if (model) {
models.push(model)
}
}
})
} catch (err) {
// Ignore directory read errors
}
}
walkDir(entityDir)
return models
}
const entityDir = path.resolve(__dirname, '../../api/schema/entities')
const yamlModels = loadYamlModels(entityDir)
console.log(`✓ Parsed ${coreModels.length} existing entities`)
console.log(`✓ Parsed ${yamlModels.length} YAML email entities`)
yamlModels.forEach((m) => console.log(` - ${m.name}`))
const models = [...coreModels, ...yamlModels]
const renderField = (field) => {
const attrs = field.attributes ? ` ${field.attributes.join(' ')}` : ''
const comment = field.comment ? ` ${field.comment}` : ''