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