Files
lifeforge/scripts/generateCollectionsSchemas.ts
Melvin Chia 2fd67a416e 25w30
Former-commit-id: 586191279315595e220678ceecf0874411939166 [formerly 1f55a3206ba247b80a76df22e32af6ec0779b60a] [formerly 1bacdd219527be876d47e6f9c90bd5d02827436d [formerly 88124105327ab95503b159bc610935e16b84fbfa]]
Former-commit-id: 1a634282b836db0ad2b88ee143c170e955c42f37 [formerly 0f5f331fa9846a66196c04deda637f08315e4d46]
Former-commit-id: 3558fed9aa9dc3016906a086a63cb462e9fc8866
2025-07-23 18:11:30 +08:00

200 lines
5.4 KiB
TypeScript

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)
let SCHEMA_STRING = `
import flattenSchemas from '@functions/utils/flattenSchema'
import { z } from 'zod/v4'
export const SCHEMAS = {
`
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)
}
const allModules = [
...fs.readdirSync('./server/src/apps', { withFileTypes: true }),
...fs.readdirSync('./server/src/core/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]
SCHEMA_STRING += ` ${moduleName}: {\n`
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}),`
SCHEMA_STRING += ` ${collection.name.split('__').pop()}: ${zodSchemaString}\n`
console.log(
chalk.green('[INFO]') +
` Generated Zod schema for collection ${chalk.bold(
collection.name
)} in module ${chalk.bold(moduleName)}.`
)
}
SCHEMA_STRING += ` },\n`
}
SCHEMA_STRING += `}
const COLLECTION_SCHEMAS = flattenSchemas(SCHEMAS)
export default COLLECTION_SCHEMAS
`
const formattedSchemaString = await prettier.format(SCHEMA_STRING, {
parser: 'typescript'
})
fs.writeFileSync(
path.resolve(__dirname, '../server/src/core/schema.ts'),
formattedSchemaString
)