Refactor content generation to simplify field schema generation
This commit is contained in:
		
							
								
								
									
										215
									
								
								src/content.ts
									
									
									
									
									
								
							
							
						
						
									
										215
									
								
								src/content.ts
									
									
									
									
									
								
							| @@ -26,17 +26,7 @@ export function stringifyContent(collections: CollectionModel[], opts: GenerateO | |||||||
|     const schemaName = opts.nameRecordSchema(name); |     const schemaName = opts.nameRecordSchema(name); | ||||||
|     const typeName = opts.nameRecordType(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", "password", "tokenKey"]); | ||||||
|     const systemFields = new Set([ |  | ||||||
|       "id", |  | ||||||
|       "created", |  | ||||||
|       "updated", |  | ||||||
|       "collectionId", |  | ||||||
|       "collectionName", |  | ||||||
|       "expand", // inherited from BaseModel/RecordModel |  | ||||||
|       "password", |  | ||||||
|       "tokenKey", // should not be in response schema |  | ||||||
|     ]); |  | ||||||
|  |  | ||||||
|     const customFields = fields.filter((field) => !systemFields.has(field.name)); |     const customFields = fields.filter((field) => !systemFields.has(field.name)); | ||||||
|     const fieldStrings = sortBy(customFields, ["name"]).map((field) => stringifyField(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<typeof ${schemaName}>;`; |     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<typeof ${schemaName}>;`; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   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) { |   function stringifySchemasEntry({ name }: CollectionModel) { | ||||||
|     return `["${name}", ${opts.nameRecordSchema(name)}]`; |     return `["${name}", ${opts.nameRecordSchema(name)}]`; | ||||||
|   } |   } | ||||||
| @@ -171,6 +42,90 @@ export function stringifyContent(collections: CollectionModel[], opts: GenerateO | |||||||
|     return `\t\tcollection(idOrName: "${name}"): RecordService<z.input<typeof ${opts.nameRecordSchema(name)}>>;`; |     return `\t\tcollection(idOrName: "${name}"): RecordService<z.input<typeof ${opts.nameRecordSchema(name)}>>;`; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   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 { |   return { | ||||||
|     collectionNames: `[\n\t${collections.map(({ name }) => `"${name}"`).join(",\n\t")},\n]`, |     collectionNames: `[\n\t${collections.map(({ name }) => `"${name}"`).join(",\n\t")},\n]`, | ||||||
|     enums: getCollectionSelectFields().map(stringifyEnum).join("\n\n"), |     enums: getCollectionSelectFields().map(stringifyEnum).join("\n\n"), | ||||||
|   | |||||||
| @@ -1,7 +1,8 @@ | |||||||
| import type { default as Pocketbase, SendOptions } from "pocketbase"; | import type { default as Pocketbase, SendOptions } from "pocketbase"; | ||||||
| import { fullListOptionsFrom, optionsFrom } from "./options.js"; | import { fullListOptionsFrom, optionsFrom } from "./options.js"; | ||||||
| import type { AnyZodRecord, RecordFullListOpts, RecordIdRef, RecordRef, RecordSlugRef } from "./types.ts"; | 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) { | export function helpersFrom({ fetch, pocketbase }: HelpersFromOpts) { | ||||||
|   async function getRecord<C extends string, S extends AnyZodRecord>(ref: RecordSlugRef<C>, opts: GetRecordOpts<S>): Promise<S["_output"]>; |   async function getRecord<C extends string, S extends AnyZodRecord>(ref: RecordSlugRef<C>, opts: GetRecordOpts<S>): Promise<S["_output"]>; | ||||||
|   | |||||||
| @@ -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"; | import type { AnyZodRecord, RecordFullListOpts, RecordListOpts } from "./types.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"; | import type { AnyZodRecord, HasRequiredKeys, ZodRecordKeys } from "./types.ts"; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| #!/usr/bin/env bun | #!/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 pkg from "../../package.json" with { type: "json" }; | ||||||
| import { loadConfig } from "c12"; | import { loadConfig } from "c12"; | ||||||
| import { defineCommand, runMain } from "citty"; | import { defineCommand, runMain } from "citty"; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user