Files
lifeforge/scripts/generateCollectionsSchemas.ts
melvinchia3636 52a80c7768 feat(server): implement database connection and collection validation during server starting process
Former-commit-id: 68c3a1e5867c1b34f4e439a6735e4d094c4aef08 [formerly c356ba449e4b90d8a585a4fe1d5509f3f5a9f77c] [formerly f722aab091f6d05405b65d46c9c4fbbefe856f5c [formerly 555c7a1db5910c3b0187357202a7663db72a5489]]
Former-commit-id: 6ba8b762e82e176a08e0dd1924c81ce66d4f6546 [formerly 1738ea41e2ab3e4134aee976befbef5d0f261512]
Former-commit-id: e72ccd9862d331cfc51f3d5f2564bdec134d4dc1
2025-09-09 16:52:40 +08:00

245 lines
6.6 KiB
TypeScript

/* eslint-disable no-case-declarations */
import chalk from 'chalk'
import dotenv from 'dotenv'
import fs from 'fs'
import _ from 'lodash'
import path from 'path'
import Pocketbase, { type CollectionModel } from 'pocketbase'
import prettier from 'prettier'
dotenv.config({
path: path.resolve(__dirname, '../server/env/.env.local')
})
if (!process.env.PB_HOST || !process.env.PB_EMAIL || !process.env.PB_PASSWORD) {
console.error(
'Please provide PB_HOST, PB_EMAIL, and PB_PASSWORD in your environment variables.'
)
process.exit(1)
}
const pb = new Pocketbase(process.env.PB_HOST)
try {
await pb
.collection('_superusers')
.authWithPassword(process.env.PB_EMAIL, process.env.PB_PASSWORD)
if (!pb.authStore.isSuperuser || !pb.authStore.isValid) {
console.error('Invalid credentials.')
process.exit(1)
}
} catch {
console.error('Server is not reachable or credentials are invalid.')
process.exit(1)
}
let MAIN_SCHEMA_EXPORTS = `import flattenSchemas from '@functions/utils/flattenSchema'
export const SCHEMAS = {
`
const moduleSchemas: Record<string, string> = {}
const allModules = fs.readdirSync('./server/src/lib', { withFileTypes: true })
const modulesMap: Record<string, CollectionModel[]> = {}
const allCollections = await pb.collections.getFullList()
const collections = allCollections.filter(e => !e.system)
for (const collection of collections) {
const module = allModules.find(e =>
collection.name.startsWith(_.snakeCase(e.name))
)
if (!module) {
console.log(
chalk.yellow('[WARNING]') +
` Collection ${collection.name} does not have a corresponding module.`
)
continue
}
if (!modulesMap[module.name]) {
modulesMap[module.name] = []
}
modulesMap[module.name]?.push(collection)
}
console.log(
chalk.green('[INFO]') +
` Found ${Object.values(modulesMap).flat().length} collections across ${Object.keys(modulesMap).length} modules.`
)
for (const module of allModules) {
if (!modulesMap[module.name]) {
continue
}
const collections = modulesMap[module.name]
if (!collections) {
console.warn(
chalk.yellow('[WARNING]') +
` No collections found for module ${chalk.bold(module.name)}.`
)
continue
}
const moduleName = collections[0].name.split('__')[0]
// Initialize module schema content
let moduleSchemaContent = `import { z } from 'zod/v4'
const ${_.camelCase(moduleName)}Schemas = {
`
for (const collection of collections ?? []) {
console.log(
chalk.blue('[INFO]') +
` Found ${collection.fields.length} fields in collection ${chalk.bold(
collection.name
)} in module ${chalk.bold(moduleName)}.`
)
const zodSchemaObject: Record<string, string> = {}
for (const field of collection.fields) {
if (field.name === 'id') {
// Skip fields that are auto-generated by PocketBase
continue
}
switch (field.type) {
case 'text':
zodSchemaObject[field.name] = 'z.string()'
break
case 'richtext':
zodSchemaObject[field.name] = 'z.string()'
break
case 'number':
zodSchemaObject[field.name] = 'z.number()'
break
case 'bool':
zodSchemaObject[field.name] = 'z.boolean()'
break
case 'email':
zodSchemaObject[field.name] = 'z.email()'
break
case 'url':
zodSchemaObject[field.name] = 'z.url()'
break
case 'date':
zodSchemaObject[field.name] = 'z.string()'
break
case 'autodate':
zodSchemaObject[field.name] = 'z.string()'
break
case 'select':
const value = [...field.values, ...(field.required ? [] : [''])]
zodSchemaObject[field.name] =
field.maxSelect > 1
? `z.array(z.enum(${JSON.stringify(value)}))`
: `z.enum(${JSON.stringify(value)})`
break
case 'file':
zodSchemaObject[field.name] =
field.maxSelect > 1 ? 'z.array(z.string())' : 'z.string()'
break
case 'relation':
zodSchemaObject[field.name] =
field.maxSelect > 1 ? `z.array(z.string())` : `z.string()`
break
case 'json':
zodSchemaObject[field.name] = 'z.any()'
break
case 'geoPoint':
zodSchemaObject[field.name] =
'z.object({ lat: z.number(), lon: z.number() })'
break
case 'password':
zodSchemaObject[field.name] = 'z.string()'
break
default:
console.warn(
chalk.yellow('[WARNING]') +
` Unknown field type ${field.type} for field ${field.name} in collection ${collection.name}.`
)
continue
}
}
const zodSchemaString = `z.object({\n${Object.entries(zodSchemaObject)
.map(([key, value]) => ` ${key}: ${value},`)
.join('\n')}\n}),`
moduleSchemaContent += ` ${collection.name.split('__').pop()}: ${zodSchemaString}`
console.log(
chalk.green('[INFO]') +
` Generated Zod schema for collection ${chalk.bold(
collection.name
)} in module ${chalk.bold(moduleName)}.`
)
}
moduleSchemaContent += `}
export default ${_.camelCase(moduleName)}Schemas
`
// Store the module schema content
moduleSchemas[module.name] = moduleSchemaContent
// Add import and export to main schema
MAIN_SCHEMA_EXPORTS += ` ${moduleName}: (await import('@lib/${module.name}/schema')).default,\n`
// Write individual module schema file
const moduleSchemaPath = path.resolve(
__dirname,
`../server/src/lib/${module.name}/schema.ts`
)
const formattedModuleSchema = await prettier.format(moduleSchemaContent, {
parser: 'typescript'
})
fs.writeFileSync(moduleSchemaPath, formattedModuleSchema)
console.log(
chalk.green('[SUCCESS]') +
` Created schema file for module ${chalk.bold(module.name)} at ${chalk.bold(`lib/${module.name}/schema.ts`)}.`
)
}
// Complete the main schema file
MAIN_SCHEMA_EXPORTS += `}
const COLLECTION_SCHEMAS = flattenSchemas(SCHEMAS)
export default COLLECTION_SCHEMAS
`
const formattedMainSchemaString = await prettier.format(MAIN_SCHEMA_EXPORTS, {
parser: 'typescript'
})
fs.writeFileSync(
path.resolve(__dirname, '../server/src/core/schema.ts'),
formattedMainSchemaString
)
console.log(
chalk.green('[SUCCESS]') +
` Updated main schema file at ${chalk.bold('core/schema.ts')} with imports from all modules.`
)
console.log(
chalk.green('[COMPLETED]') +
` Schema generation completed successfully! Individual schema files created for ${Object.keys(moduleSchemas).length} modules.`
)