From e5e728705cb20c66cad5e34efab773599d02221e Mon Sep 17 00:00:00 2001 From: GarandPLG Date: Mon, 6 Oct 2025 22:21:54 +0200 Subject: [PATCH] Refactor content generation to simplify field schema generation --- src/content.ts | 215 ++++++++++++++++++---------------------------- src/helpers.ts | 3 +- src/options.ts | 3 +- src/schemas.ts | 3 +- src/server/cli.ts | 3 +- 5 files changed, 93 insertions(+), 134 deletions(-) diff --git a/src/content.ts b/src/content.ts index 48ab9db..4430893 100644 --- a/src/content.ts +++ b/src/content.ts @@ -26,17 +26,7 @@ export function stringifyContent(collections: CollectionModel[], opts: GenerateO const schemaName = opts.nameRecordSchema(name); const typeName = opts.nameRecordType(name); - // Filter out system fields that are already inherited from RecordModel or should be omitted - const systemFields = new Set([ - "id", - "created", - "updated", - "collectionId", - "collectionName", - "expand", // inherited from BaseModel/RecordModel - "password", - "tokenKey", // should not be in response schema - ]); + const systemFields = new Set(["id", "created", "updated", "collectionId", "collectionName", "expand", "password", "tokenKey"]); const customFields = fields.filter((field) => !systemFields.has(field.name)); const fieldStrings = sortBy(customFields, ["name"]).map((field) => stringifyField(field, name)); @@ -44,125 +34,6 @@ export function stringifyContent(collections: CollectionModel[], opts: GenerateO return `export const ${schemaName} = z.object({\n\t...RecordModel.omit({ expand: true }).shape,\n\tcollectionName: z.literal("${name}"),\n\t${fieldStrings.join(",\n\t")},\n});\nexport type ${typeName} = z.infer;`; } - function stringifyField(field: CollectionField, collectionName: string) { - let schema: string; - // TODO: - console.log(`${collectionName}: ${field.type}`); - switch (field.type) { - case "bool": - schema = stringifyBoolField(field); - break; - case "date": - schema = stringifyDateField(field); - break; - case "editor": - schema = stringifyEditorField(field); - break; - case "email": - schema = stringifyEmailField(field); - break; - case "file": - schema = stringifyFileField(field); - break; - case "json": - schema = stringifyJsonField(field); - break; - case "number": - schema = stringifyNumberField(field); - break; - case "relation": - schema = stringifyRelationField(field); - break; - case "select": - schema = stringifySelectField(field, collectionName); - break; - case "text": - schema = stringifyTextField(field); - break; - case "url": - schema = stringifyUrlField(field); - break; - case "geoPoint": - schema = stringifyGeoPointField(field); - break; - default: - console.warn(`Unknown field type "${field.type}" for field "${field.name}". Using z.any() as fallback.`); - schema = "z.any()"; - break; - } - return `${field.name}: ${schema}${(field as any).required ? "" : ".optional()"}`; - } - - function stringifyBoolField(_: CollectionField) { - return "z.boolean()"; - } - - function stringifyDateField(_field: CollectionField) { - // TODO: implement min and max - return "z.string().pipe(z.coerce.date())"; - } - - function stringifyEditorField(_field: CollectionField) { - // TODO: implement convertUrls - return "z.string()"; - } - - function stringifyEmailField(_field: CollectionField) { - // TODO: implement exceptDomains and onlyDomains - return "z.string().email()"; - } - - function stringifyFileField(field: CollectionField) { - const maxSelect = (field as any).options?.maxSelect; - // TODO: implement maxSize, mimeTypes, protected, thumbs - return `z.string()${maxSelect === 1 ? "" : `.array()${maxSelect ? `.max(${maxSelect})` : ""}`}`; - } - - function stringifyJsonField(_field: CollectionField) { - // TODO: implement maxSize and json schema - return "z.any()"; - } - - function stringifyNumberField(field: CollectionField) { - const options = (field as any).options || {}; - const { max, min, noDecimal } = options; - return `z.number()${noDecimal ? ".int()" : ""}${min ? `.min(${min})` : ""}${max ? `.max(${max})` : ""}`; - } - - function stringifyRelationField(field: CollectionField) { - const options = (field as any).options || {}; - const required = (field as any).required; - const { maxSelect, minSelect } = options; - // TODO: implement cascadeDelete, displayFields - const min = minSelect ? `.min(${minSelect})` : ""; - const max = maxSelect ? `.max(${maxSelect})` : ""; - const multiple = maxSelect === 1 ? "" : `.array()${min}${max}`; - const isOptional = required || maxSelect !== 1 ? `` : `.transform((id) => id === "" ? undefined : id)`; - return `z.string()${isOptional}${multiple}`; - } - - function stringifySelectField(field: CollectionField, collectionName: string) { - const maxSelect = (field as any).options?.maxSelect; - // TODO: implement values - return `${opts.nameEnumSchema(opts.nameEnumField(collectionName, field.name))}${maxSelect === 1 ? "" : `.array().max(${maxSelect})`}`; - } - - function stringifyTextField(field: CollectionField) { - const options = (field as any).options || {}; - const { max, min } = options; - // TODO: implement pattern - return `z.string()${min ? `.min(${min})` : ""}${max ? `.max(${max})` : ""}`; - } - - function stringifyUrlField(_field: CollectionField) { - // TODO: implement exceptDomains and onlyDomains - return "z.string().url()"; - } - - function stringifyGeoPointField(_field: CollectionField) { - return "z.object({ lat: z.number().min(-90).max(90), lng: z.number().min(-180).max(180) })"; - } - function stringifySchemasEntry({ name }: CollectionModel) { return `["${name}", ${opts.nameRecordSchema(name)}]`; } @@ -171,6 +42,90 @@ export function stringifyContent(collections: CollectionModel[], opts: GenerateO return `\t\tcollection(idOrName: "${name}"): RecordService>;`; } + function stringifyField(field: CollectionField, collectionName: string) { + let schema: string; + // if (collectionName === "relations") console.log(`${collectionName}:`, field); + switch (field.type) { + case "bool": + schema = "z.boolean()"; + break; + + case "date": + // TODO: implement min and max + schema = "z.string().pipe(z.coerce.date())"; + break; + + case "editor": + // TODO: implement convertUrls + schema = "z.string()"; + break; + + case "email": + // TODO: implement exceptDomains and onlyDomains + schema = "z.string().email()"; + break; + + case "file": + const maxSelectFile = field.maxSelect; + // TODO: implement maxSize, mimeTypes, protected, thumbs + schema = `z.string()${maxSelectFile === 1 ? "" : `.array()${maxSelectFile ? `.max(${maxSelectFile})` : ""}`}`; + break; + + case "json": + // TODO: implement maxSize and json schema + schema = "z.any()"; + break; + + case "number": + const maxNumber = field.maxNumber; + const minNumber = field.minNumber; + const noDecimal = field.noDecimal; + schema = `z.number()${noDecimal ? ".int()" : ""}${minNumber ? `.min(${minNumber})` : ""}${maxNumber ? `.max(${maxNumber})` : ""}`; + break; + + case "relation": + // TODO: implement cascadeDelete, displayFields, multiple records + const required = field.required; + const maxSelectRelation = field.maxSelect; + const minSelectRelation = field.minSelect; + const min = `.min(${minSelectRelation})`; + const max = `.max(${maxSelectRelation})`; + const multiple = maxSelectRelation === 1 ? "" : `.array()${min}${max}`; + const isOptional = required || maxSelectRelation !== 1 ? `` : `.transform((id) => id === "" ? undefined : id)`; + + schema = `z.string()${isOptional}${multiple}`; + break; + + case "select": + const maxSelect = field.maxSelect; + // TODO: implement values + schema = `${opts.nameEnumSchema(opts.nameEnumField(collectionName, field.name))}${maxSelect === 1 ? "" : `.array().max(${maxSelect})`}`; + break; + + case "text": + const maxText = field.max; + const minText = field.min; + // TODO: implement pattern + schema = `z.string()${minText ? `.min(${minText})` : ""}${maxText ? `.max(${maxText})` : ""}`; + break; + + case "url": + // TODO: implement exceptDomains and onlyDomains + schema = "z.string().url()"; + break; + + case "geoPoint": + schema = "z.object({ lat: z.number().min(-90).max(90), lng: z.number().min(-180).max(180) })"; + break; + + default: + console.warn(`Unknown field type "${field.type}" for field "${field.name}". Using z.any() as fallback.`); + schema = "z.any()"; + break; + } + return `${field.name}: ${schema}${(field as any).required ? "" : ".optional()"}`; + } + return { collectionNames: `[\n\t${collections.map(({ name }) => `"${name}"`).join(",\n\t")},\n]`, enums: getCollectionSelectFields().map(stringifyEnum).join("\n\n"), diff --git a/src/helpers.ts b/src/helpers.ts index 61e4f1a..f0e79b6 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,7 +1,8 @@ import type { default as Pocketbase, SendOptions } from "pocketbase"; import { fullListOptionsFrom, optionsFrom } from "./options.js"; import type { AnyZodRecord, RecordFullListOpts, RecordIdRef, RecordRef, RecordSlugRef } from "./types.ts"; -import { AnyRecordsList, type RecordsList } from "./schemas.ts"; +import { AnyRecordsList } from "./schemas.ts"; +import type { RecordsList } from "./schemas.ts"; export function helpersFrom({ fetch, pocketbase }: HelpersFromOpts) { async function getRecord(ref: RecordSlugRef, opts: GetRecordOpts): Promise; diff --git a/src/options.ts b/src/options.ts index c7b6b32..b4a718c 100644 --- a/src/options.ts +++ b/src/options.ts @@ -1,4 +1,5 @@ -import { z, type AnyZodObject, type ZodTypeAny } from "zod"; +import { z } from "zod"; +import type { AnyZodObject, ZodTypeAny } from "zod"; import type { AnyZodRecord, RecordFullListOpts, RecordListOpts } from "./types.ts"; /** diff --git a/src/schemas.ts b/src/schemas.ts index 03eb2c6..15daa2f 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -1,4 +1,5 @@ -import { type AnyZodObject, type objectUtil, z, type ZodEffects, type ZodObject, ZodOptional, type ZodRawShape } from "zod"; +import { z, ZodOptional, ZodEffects } from "zod"; +import type { AnyZodObject, objectUtil, ZodObject, ZodRawShape } from "zod"; import type { AnyZodRecord, HasRequiredKeys, ZodRecordKeys } from "./types.ts"; /** diff --git a/src/server/cli.ts b/src/server/cli.ts index fccf7e5..4469c24 100644 --- a/src/server/cli.ts +++ b/src/server/cli.ts @@ -1,6 +1,7 @@ #!/usr/bin/env bun -import { Config, type ResolvedConfig } from "../config.ts"; +import { Config } from "../config.ts"; +import type { ResolvedConfig } from "../config.ts"; import pkg from "../../package.json" with { type: "json" }; import { loadConfig } from "c12"; import { defineCommand, runMain } from "citty";