mirror of
https://github.com/Lifeforge-app/lifeforge.git
synced 2026-06-28 06:46:24 +00:00
Former-commit-id: 94a4e04a6689a10743cebd3d7484806b8c295df6 [formerly f95ca1c6e302fa35ee64913e8208849586757d2d] [formerly 274bc78161b8dd8e200d2bc34690ecc8ba7b276d [formerly f36b9709a336fad9e278ec988fbd34b66b8c3a91]] Former-commit-id: 75a321f570ab5c146a21f27c9b5a3e8aa21a1c40 [formerly e35d658f3fac7f0690cf7a4051d3124aac36f832] Former-commit-id: 8094fe6dfd24022fe75927c9bd63bc9a5518bf19
293 lines
8.7 KiB
TypeScript
293 lines
8.7 KiB
TypeScript
import chalk from 'chalk'
|
|
import dotenv from 'dotenv'
|
|
import fs from 'fs'
|
|
import _ from 'lodash'
|
|
import path from 'path'
|
|
import { singular } from 'pluralize'
|
|
import Pocketbase, { type CollectionModel } from 'pocketbase'
|
|
|
|
const toBeWritten: Record<string, string> = {}
|
|
|
|
const CUSTOM_SCHEMAS_DELIMITER =
|
|
'// -------------------- CUSTOM SCHEMAS --------------------'
|
|
const TARGET_PATH = path.resolve(__dirname, '../shared/src/types/collections')
|
|
|
|
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)
|
|
}
|
|
|
|
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 moduleName = _.camelCase(module.name)
|
|
const collections = modulesMap[module.name]
|
|
|
|
let finalString = `/**
|
|
* This file is auto-generated. DO NOT EDIT IT MANUALLY.
|
|
* You may regenerate it by running \`bun run schema:generate:collection\` in the root directory.
|
|
* If you want to add custom schemas, you will find a dedicated space at the end of this file.
|
|
* Generated for module: ${moduleName}
|
|
* Generated at: ${new Date().toISOString()}
|
|
* Contains: ${collections?.map(e => e.name).join(', ')}
|
|
*/
|
|
|
|
import { z } from "zod/v4";
|
|
|
|
`
|
|
|
|
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 (['id', 'created', 'updated'].includes(field.name)) {
|
|
// 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
|
|
}
|
|
}
|
|
|
|
if (collection.name.endsWith('_aggregated')) {
|
|
collection.name =
|
|
singular(collection.name.replace(/_aggregated$/, '')) + '_aggregated'
|
|
} else {
|
|
collection.name = singular(collection.name)
|
|
}
|
|
|
|
collection.name = `${collection.name.split('__').pop() ?? collection.name}`
|
|
|
|
const zodSchemaString = `const ${_.upperFirst(
|
|
_.camelCase(collection.name)
|
|
)} = z.object({\n${Object.entries(zodSchemaObject)
|
|
.map(([key, value]) => ` ${key}: ${value},`)
|
|
.join('\n')}\n});`
|
|
finalString += `${zodSchemaString}\n\n`
|
|
|
|
console.log(
|
|
chalk.green('[INFO]') +
|
|
` Generated Zod schema for collection ${chalk.bold(
|
|
collection.name
|
|
)} in module ${chalk.bold(moduleName)}.`
|
|
)
|
|
}
|
|
|
|
if (!collections) {
|
|
console.warn(
|
|
chalk.yellow('[WARNING]') +
|
|
` No collections found for module ${chalk.bold(moduleName)}.`
|
|
)
|
|
continue
|
|
}
|
|
|
|
finalString += `${collections
|
|
.map(
|
|
e =>
|
|
`type I${_.upperFirst(_.camelCase(e.name))} = z.infer<typeof ${_.upperFirst(
|
|
_.camelCase(singular(e.name))
|
|
)}>;`
|
|
)
|
|
.join('\n')}\n\nexport {\n${collections
|
|
.map(e => ` ${_.upperFirst(_.camelCase(e.name))},`)
|
|
.join('\n')}\n};\n\nexport type {\n${collections
|
|
.map(e => ` I${_.upperFirst(_.camelCase(e.name))},`)
|
|
.join('\n')}\n};\n`
|
|
|
|
const outputPath = path.resolve(TARGET_PATH, `${moduleName}.schema.ts`)
|
|
|
|
const originalContent = fs.existsSync(outputPath)
|
|
? fs.readFileSync(outputPath, 'utf-8')
|
|
: ''
|
|
|
|
if (originalContent.includes(CUSTOM_SCHEMAS_DELIMITER)) {
|
|
const customCollectionsSchemas = originalContent
|
|
.split(CUSTOM_SCHEMAS_DELIMITER)
|
|
.pop()!
|
|
|
|
finalString += `\n${CUSTOM_SCHEMAS_DELIMITER}\n\n${customCollectionsSchemas
|
|
.replace(/\/\/\s*$/, '')
|
|
.replace(/^\n+/, '')
|
|
.replace(/\n+$/, '')}\n`
|
|
} else {
|
|
finalString += `\n${CUSTOM_SCHEMAS_DELIMITER}\n\n// Add your custom schemas here. They will not be overwritten by this script.\n`
|
|
}
|
|
|
|
toBeWritten[`${moduleName}.schema.ts`] = finalString
|
|
}
|
|
|
|
if (fs.existsSync(TARGET_PATH) && fs.lstatSync(TARGET_PATH).isDirectory()) {
|
|
const files = fs.readdirSync(TARGET_PATH)
|
|
for (const file of files) {
|
|
if (file.endsWith('.custom.schema.ts')) {
|
|
toBeWritten[file] = fs.readFileSync(path.join(TARGET_PATH, file), 'utf-8')
|
|
}
|
|
}
|
|
fs.rmdirSync(TARGET_PATH, { recursive: true })
|
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
}
|
|
fs.mkdirSync(TARGET_PATH, { recursive: true })
|
|
|
|
const indexString = `
|
|
/**
|
|
* This file is auto-generated. DO NOT EDIT IT MANUALLY.
|
|
* You may regenerate it by running \`npm run generate:schema:collection\`.
|
|
* This is the entry point for all schemas in the shared library.
|
|
* Generated at: ${new Date().toISOString()}
|
|
* Contains schemas for all modules.
|
|
*/
|
|
|
|
${Object.keys(toBeWritten)
|
|
.map(
|
|
moduleName =>
|
|
`export * as ${_.upperFirst(_.camelCase(moduleName.replace(/(?:\.custom)?\.schema\.ts$/, moduleName.endsWith('.custom.schema.ts') ? 'Custom' : 'Collections')))}CollectionsSchemas from './${moduleName.replace(/\.ts$/, '')}';`
|
|
)
|
|
.join('\n')}
|
|
|
|
export { SchemaWithPB } from './schemaWithPB'
|
|
export type { ISchemaWithPB } from './schemaWithPB'
|
|
`
|
|
|
|
toBeWritten['index.ts'] = indexString
|
|
|
|
toBeWritten['schemaWithPB.ts'] = `
|
|
import { z } from 'zod/v4'
|
|
|
|
const BasePBSchema = z.object({
|
|
id: z.string(),
|
|
collectionId: z.string(),
|
|
collectionName: z.string(),
|
|
created: z.string(),
|
|
updated: z.string()
|
|
})
|
|
|
|
export const SchemaWithPB = <T extends z.ZodTypeAny>(schema: T) => {
|
|
return z.intersection(schema, BasePBSchema)
|
|
}
|
|
|
|
export type ISchemaWithPB<T> = T & z.infer<typeof BasePBSchema>
|
|
`
|
|
|
|
for (const [fileName, content] of Object.entries(toBeWritten)) {
|
|
const filePath = path.resolve(TARGET_PATH, fileName)
|
|
fs.writeFileSync(filePath, content, 'utf-8')
|
|
}
|