mirror of
https://github.com/Lifeforge-app/lifeforge.git
synced 2026-06-28 06:46:24 +00:00
Former-commit-id: 0d53fe99b4446b5be8e5d2014fc6298abb559360 [formerly 6034c63543ef4540da2c245403712d28bfd84562] [formerly c153e0450a9deba2a853795e6d82ec9c372a8283 [formerly bce97093d38f55436b6fdec3685839c680f51e3e]] Former-commit-id: a3069fefd801580dd0b61f5d79fcd450ef05c85e [formerly 5564c5736093efd12d7703de56d6f2dd65cf1852] Former-commit-id: a04c0164d4cf19dd38765cca6c5a05ce5d65d872
296 lines
8.9 KiB
TypeScript
296 lines
8.9 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");
|
|
}
|