Compare commits
	
		
			11 Commits
		
	
	
		
			ea672a9d91
			...
			v0.5.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 2412238236 | |||
| 6c88dd79d8 | |||
| 1638435e26 | |||
| 618d9591b4 | |||
| b2e9e82f5e | |||
| 81005c54cd | |||
| e02737359c | |||
| 827b53e630 | |||
| d4dfed9603 | |||
| 21c9fccf5e | |||
| e5e728705c | 
							
								
								
									
										33
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,9 +1,32 @@ | |||||||
| # zod-pocketbase | # zod-pocketbase-continued | ||||||
|  |  | ||||||
| Zod tooling for your Pocketbase instance. | Zod tooling for your PocketBase instance. | ||||||
|  |  | ||||||
| To see how to get started, check out the [docs](https://zod-pocketbase.vercel.app) | This repository is a continuation of [Gregory Bouteiller's zod-pocketbase](https://github.com/gbouteiller/zod-pocketbase), as the original repository was outdated for 10 months. | ||||||
|  |  | ||||||
| ## Licensing | ## Documentation | ||||||
|  |  | ||||||
| [MIT Licensed](./LICENSE). Made with ❤️ by [Gregory Bouteiller](https://github.com/gbouteiller). | [Old documentation is compatible with the current version](https://zod-pocketbase.vercel.app) | ||||||
|  |  | ||||||
|  | ## Compatibility | ||||||
|  |  | ||||||
|  | - **PocketBase**: v0.30.0 | ||||||
|  | - **PocketBase JS SDK**: v0.26.2 | ||||||
|  |  | ||||||
|  | ## Changes from Original | ||||||
|  |  | ||||||
|  | ### Project Structure | ||||||
|  | - Removed `doc/`, `playground/` and monorepo configuration | ||||||
|  | - Replaced `pnpm` and `node` in favor of `bun` | ||||||
|  | - Switched `.github/` to `.gitea/` and `gh` to `tea` | ||||||
|  |  | ||||||
|  | ### Dependencies & Code | ||||||
|  | - Migrated from `tsup` to `tsdown` | ||||||
|  | - Updated all npm dependencies (except `zod`, which was only updated to the latest 3 version) | ||||||
|  | - Removed unused dependencies | ||||||
|  | - Fixed `getPocketbase` function in `sdk.ts` to match the latest PocketBase version | ||||||
|  | - Implemented most TODOs left in `content.ts` | ||||||
|  |  | ||||||
|  | ## License | ||||||
|  |  | ||||||
|  | [MIT License](./LICENSE) | ||||||
|   | |||||||
| @@ -33,6 +33,41 @@ export const RecordModel = z.object({ | |||||||
| }); | }); | ||||||
| export type RecordModel = z.infer<typeof RecordModel>; | export type RecordModel = z.infer<typeof RecordModel>; | ||||||
|  |  | ||||||
|  | /******* JSON FIELD *******/ | ||||||
|  | export const pbJsonField = (maxSizeInBytes: number = 1048576) => { | ||||||
|  |   const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); | ||||||
|  |   const jsonSchema: z.ZodType<any> = z.lazy(() => | ||||||
|  |     z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]) | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   const stringTransform = z.string() | ||||||
|  |     .max(maxSizeInBytes, `JSON field cannot exceed ${maxSizeInBytes} bytes`) | ||||||
|  |     .transform((val: string) => { | ||||||
|  |       if (val === "true") return true; | ||||||
|  |       if (val === "false") return false; | ||||||
|  |       if (val === "null") return null; | ||||||
|  |  | ||||||
|  |       if ((val.startsWith('[') && val.endsWith(']'))||(val.startsWith('{') && val.endsWith('}'))) | ||||||
|  |         try { | ||||||
|  |           return JSON.parse(val); | ||||||
|  |         } catch { | ||||||
|  |           return val; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |       const num = Number(val); | ||||||
|  |       if (!isNaN(num) && isFinite(num) && val.trim() !== '') | ||||||
|  |         return num; | ||||||
|  |  | ||||||
|  |       if (val.startsWith('"') && val.endsWith('"')) | ||||||
|  |         return val.slice(1, -1); | ||||||
|  |  | ||||||
|  |       return val; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |   return z.union([jsonSchema, stringTransform]); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  |  | ||||||
| /******* RECORDS *******/ | /******* RECORDS *******/ | ||||||
| @@_RECORDS_@@ | @@_RECORDS_@@ | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| { | { | ||||||
|   description = "Zod-PocketBase development environment with Bun"; |   description = "Zod-PocketBase-Continue development environment with Bun"; | ||||||
|  |  | ||||||
|   inputs = { |   inputs = { | ||||||
|     nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; |     nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; | ||||||
| @@ -30,7 +30,7 @@ | |||||||
|           ]; |           ]; | ||||||
|  |  | ||||||
|           shellHook = '' |           shellHook = '' | ||||||
|             echo "🚀 Zod-PocketBase development environment (Bun-powered)" |             echo "🚀 Zod-PocketBase-Continue development environment (Bun-powered)" | ||||||
|             echo "Bun version: $(bun --version)" |             echo "Bun version: $(bun --version)" | ||||||
|             echo "Tea version: $(tea --version)" |             echo "Tea version: $(tea --version)" | ||||||
|             echo "" |             echo "" | ||||||
| @@ -52,7 +52,7 @@ | |||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         packages.default = pkgs.stdenv.mkDerivation { |         packages.default = pkgs.stdenv.mkDerivation { | ||||||
|           name = "zod-pocketbase"; |           name = "zod-pocketbase-continue"; | ||||||
|           src = ./.; |           src = ./.; | ||||||
|  |  | ||||||
|           buildInputs = [ bun ]; |           buildInputs = [ bun ]; | ||||||
|   | |||||||
							
								
								
									
										24
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,7 +1,7 @@ | |||||||
| { | { | ||||||
|   "name": "zod-pocketbase", |   "name": "zod-pocketbase-continue", | ||||||
|   "version": "0.5.0", |   "version": "0.5.0", | ||||||
|   "description": "", |   "description": "Zod tooling for your PocketBase instance.", | ||||||
|   "author": { |   "author": { | ||||||
|     "email": "garandplg@garandplg.com", |     "email": "garandplg@garandplg.com", | ||||||
|     "name": "Garand_PLG", |     "name": "Garand_PLG", | ||||||
| @@ -16,7 +16,7 @@ | |||||||
|     "type generation", |     "type generation", | ||||||
|     "zod" |     "zod" | ||||||
|   ], |   ], | ||||||
|   "homepage": "https://gitea.garandplg.com/GarandPLG/zod-pocketbase", |   "homepage": "https://gitea.garandplg.com/GarandPLG/zod-pocketbase-continue", | ||||||
|   "publishConfig": { |   "publishConfig": { | ||||||
|     "access": "public" |     "access": "public" | ||||||
|   }, |   }, | ||||||
| @@ -28,7 +28,7 @@ | |||||||
|   }, |   }, | ||||||
|   "main": "dist/index.js", |   "main": "dist/index.js", | ||||||
|   "bin": { |   "bin": { | ||||||
|     "zod-pocketbase": "dist/server/cli.js" |     "zod-pocketbase-continue": "dist/server/cli.js" | ||||||
|   }, |   }, | ||||||
|   "exports": { |   "exports": { | ||||||
|     ".": { |     ".": { | ||||||
| @@ -45,8 +45,8 @@ | |||||||
|     "assets" |     "assets" | ||||||
|   ], |   ], | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "dev": "tsup --watch", |     "dev": "tsdown --watch", | ||||||
|     "build": "tsup", |     "build": "tsdown", | ||||||
|     "changeset": "changeset", |     "changeset": "changeset", | ||||||
|     "release": "bun scripts/release.mjs" |     "release": "bun scripts/release.mjs" | ||||||
|   }, |   }, | ||||||
| @@ -54,20 +54,16 @@ | |||||||
|     "@clack/prompts": "^0.11.0", |     "@clack/prompts": "^0.11.0", | ||||||
|     "c12": "^3.3.0", |     "c12": "^3.3.0", | ||||||
|     "citty": "^0.1.6", |     "citty": "^0.1.6", | ||||||
|     "es-toolkit": "^1.39.10" |     "es-toolkit": "^1.40.0" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@changesets/cli": "^2.29.7", |     "@changesets/cli": "^2.29.7", | ||||||
|     "@types/node": "^24.6.2", |     "@types/node": "^24.7.1", | ||||||
|     "@typescript-eslint/parser": "^8.45.0", |     "@typescript-eslint/parser": "^8.46.0", | ||||||
|     "eslint": "^9.37.0", |     "eslint": "^9.37.0", | ||||||
|     "eslint-plugin-astro": "^1.3.1", |  | ||||||
|     "eslint-plugin-jsx-a11y": "^6.10.2", |  | ||||||
|     "pocketbase": "^0.26.2", |     "pocketbase": "^0.26.2", | ||||||
|     "prettier": "^3.6.2", |     "prettier": "^3.6.2", | ||||||
|     "prettier-plugin-astro": "^0.14.1", |     "tsdown": "^0.15.6", | ||||||
|     "prettier-plugin-tailwindcss": "^0.6.14", |  | ||||||
|     "tsup": "^8.5.0", |  | ||||||
|     "zod": "^3.25.76" |     "zod": "^3.25.76" | ||||||
|   }, |   }, | ||||||
|   "peerDependencies": { |   "peerDependencies": { | ||||||
|   | |||||||
| @@ -1,6 +1,4 @@ | |||||||
| /** @type {import("prettier").Config} */ | /** @type {import("prettier").Config} */ | ||||||
| export default { | export default { | ||||||
|   printWidth: 140, |   printWidth: 140, | ||||||
|   plugins: ["prettier-plugin-astro", "prettier-plugin-tailwindcss"], |  | ||||||
|   overrides: [{ files: "*.astro", options: { parser: "astro" } }], |  | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ export const defaultConfig = { | |||||||
|   nameEnumValues: (name: string) => `${name}Values`, |   nameEnumValues: (name: string) => `${name}Values`, | ||||||
|   nameRecordSchema: (name: string) => `${pascalCase(name)}Record`, |   nameRecordSchema: (name: string) => `${pascalCase(name)}Record`, | ||||||
|   nameRecordType: (name: string) => `${pascalCase(name)}Record`, |   nameRecordType: (name: string) => `${pascalCase(name)}Record`, | ||||||
|   output: "./zod-pocketbase.ts", |   output: "./zod-pocketbase-continue.ts", | ||||||
| }; | }; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|   | |||||||
							
								
								
									
										253
									
								
								src/content.ts
									
									
									
									
									
								
							
							
						
						
									
										253
									
								
								src/content.ts
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | |||||||
| import { sortBy } from "es-toolkit"; | import { sortBy } from "es-toolkit"; | ||||||
| import type { CollectionModel, CollectionField } from "pocketbase"; | import type { CollectionModel, CollectionField } from "pocketbase"; | ||||||
| import type { GenerateOpts } from "./server/utils.ts"; | import type { GenerateOpts } from "@/server/utils.ts"; | ||||||
|  |  | ||||||
| export function stringifyContent(collections: CollectionModel[], opts: GenerateOpts) { | export function stringifyContent(collections: CollectionModel[], opts: GenerateOpts) { | ||||||
|   function getCollectionSelectFields() { |   function getCollectionSelectFields() { | ||||||
| @@ -9,7 +9,7 @@ export function stringifyContent(collections: CollectionModel[], opts: GenerateO | |||||||
|         .filter((field: CollectionField) => field.type === "select") |         .filter((field: CollectionField) => field.type === "select") | ||||||
|         .map((field: CollectionField) => ({ |         .map((field: CollectionField) => ({ | ||||||
|           name: opts.nameEnumField(collection.name, field.name), |           name: opts.nameEnumField(collection.name, field.name), | ||||||
|           values: ((field as any).options?.values ?? []) as string[], |           values: ((field as any).values ?? []) as string[], | ||||||
|         })), |         })), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| @@ -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,124 @@ 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; | ||||||
|  |     switch (field.type) { | ||||||
|  |       case "bool": | ||||||
|  |         schema = "z.boolean()"; | ||||||
|  |         break; | ||||||
|  |  | ||||||
|  |       case "date": | ||||||
|  |         const minConstraintDate = field.min ? `.min(new Date("${field.min}"))` : ""; | ||||||
|  |         const maxConstraintDate = field.max ? `.max(new Date("${field.max}"))` : ""; | ||||||
|  |  | ||||||
|  |         schema = `z.string().pipe(z.coerce.date()${minConstraintDate}${maxConstraintDate})`; | ||||||
|  |         break; | ||||||
|  |  | ||||||
|  |       case "editor": | ||||||
|  |         // TODO: implement convertUrls | ||||||
|  |         schema = "z.string()"; | ||||||
|  |         break; | ||||||
|  |  | ||||||
|  |       case "email": | ||||||
|  |         const onlyDomainsConstraint = createDomainConstraint(field.onlyDomains, true, "email"); | ||||||
|  |         const exceptDomainsConstraint = createDomainConstraint(field.exceptDomains, false, "email"); | ||||||
|  |  | ||||||
|  |         schema = `z.string().email()${onlyDomainsConstraint}${exceptDomainsConstraint}`; | ||||||
|  |         break; | ||||||
|  |  | ||||||
|  |       case "file": | ||||||
|  |         const maxSelectFile: number = field.maxSelect; | ||||||
|  |         const maxSizeFile: number = field.maxSize; | ||||||
|  |         const mimeTypesArray: string[] = field.mimeTypes || []; | ||||||
|  |         // const protectedFile: boolean = field.protected; | ||||||
|  |         // const thumbsFileArray: string[] = field.thumbs || []; | ||||||
|  |  | ||||||
|  |         const fileFieldMaxSelect = maxSelectFile ? `.max(${maxSelectFile})` : ""; | ||||||
|  |         const fileFieldTypeArray = maxSelectFile === 1 ? "" : `.array()${fileFieldMaxSelect}`; | ||||||
|  |  | ||||||
|  |         let fileValidation = "z.instanceof(File)"; | ||||||
|  |  | ||||||
|  |         if (maxSizeFile) fileValidation += `.refine((file) => file.size <= ${maxSizeFile}, { message: "File size too large" })`; | ||||||
|  |  | ||||||
|  |         if (mimeTypesArray.length > 0) | ||||||
|  |           fileValidation += `.refine((file) => ${JSON.stringify(mimeTypesArray)}.includes(file.type), { message: "Invalid file type" })`; | ||||||
|  |  | ||||||
|  |         const baseFileSchema = `z.union([z.string(), ${fileValidation}])`; | ||||||
|  |         schema = `${baseFileSchema}${fileFieldTypeArray}`; | ||||||
|  |         break; | ||||||
|  |  | ||||||
|  |       case "json": | ||||||
|  |         schema = field.maxSize > 0 ? `pbJsonField(${field.maxSize})` : "pbJsonField()"; | ||||||
|  |         break; | ||||||
|  |  | ||||||
|  |       case "number": | ||||||
|  |         const maxNumber = field.maxNumber ? `.max(${field.maxNumber})` : ""; | ||||||
|  |         const minNumber = field.minNumber ? `.min(${field.minNumber})` : ""; | ||||||
|  |         const noDecimal = field.noDecimal ? ".int()" : ""; | ||||||
|  |  | ||||||
|  |         schema = `z.number()${noDecimal}${minNumber}${maxNumber}`; | ||||||
|  |         break; | ||||||
|  |  | ||||||
|  |       case "relation": | ||||||
|  |         // TODO: implement cascadeDelete, displayFields, multiple records query | ||||||
|  |         const multiple = field.maxSelect === 1 ? "" : `.array().min(${field.minSelect}).max(${field.maxSelect})`; | ||||||
|  |         const isOptional = field.required || field.maxSelect !== 1 ? `` : `.transform((id: string) => id === "" ? undefined : id)`; | ||||||
|  |  | ||||||
|  |         schema = `z.string()${isOptional}${multiple}`; | ||||||
|  |         break; | ||||||
|  |  | ||||||
|  |       case "select": | ||||||
|  |         const maxSelect = field.maxSelect === 1 ? "" : `.array().max(${field.maxSelect})`; | ||||||
|  |  | ||||||
|  |         schema = `${opts.nameEnumSchema(opts.nameEnumField(collectionName, field.name))}${maxSelect}`; | ||||||
|  |         break; | ||||||
|  |  | ||||||
|  |       case "text": | ||||||
|  |         const patternText = | ||||||
|  |           field.pattern && field.pattern.trim() !== "" ? `.regex(new RegExp("${field.pattern.replace(/"/g, '\\"')}"))` : ""; | ||||||
|  |         const maxText = field.max ? `.max(${field.max})` : ""; | ||||||
|  |         const minText = field.min ? `.min(${field.min})` : ""; | ||||||
|  |  | ||||||
|  |         schema = `z.string()${minText}${maxText}${patternText}`; | ||||||
|  |         break; | ||||||
|  |  | ||||||
|  |       case "url": | ||||||
|  |         const onlyDomainsUrlConstraint = createDomainConstraint(field.onlyDomains, true, "url"); | ||||||
|  |         const exceptDomainsUrlConstraint = createDomainConstraint(field.exceptDomains, false, "url"); | ||||||
|  |  | ||||||
|  |         schema = `z.string().url()${onlyDomainsUrlConstraint}${exceptDomainsUrlConstraint}`; | ||||||
|  |         break; | ||||||
|  |  | ||||||
|  |       case "geoPoint": | ||||||
|  |         schema = "z.object({ lat: z.number().min(-90).max(90), lon: 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()"}`; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /* Helpers */ | ||||||
|  |  | ||||||
|  |   const createDomainConstraint = (domains: string[], isWhitelist: boolean, type: "email" | "url") => { | ||||||
|  |     if (!domains?.length) return ""; | ||||||
|  |  | ||||||
|  |     const domainsList = domains.map((domain) => `"${domain}"`).join(", "); | ||||||
|  |     const messageType = isWhitelist ? "isn't one of the allowed ones" : "is one of the disallowed ones"; | ||||||
|  |     const negation = isWhitelist ? "" : "!"; | ||||||
|  |  | ||||||
|  |     const domainExtraction = type === "email" ? 'const domain = value.split("@")[1];' : "const domain = new URL(value).hostname;"; | ||||||
|  |  | ||||||
|  |     const errorHandling = type === "url" ? "try { " : ""; | ||||||
|  |     const errorCatch = type === "url" ? " } catch { return false; }" : ""; | ||||||
|  |  | ||||||
|  |     return `.refine((value: string) => { ${errorHandling}${domainExtraction} const domainsArray = [${domainsList}]; return domain && ${negation}domainsArray.includes(domain);${errorCatch} }, { message: "Invalid ${type}, domain ${messageType}" })`; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   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.ts"; | ||||||
| 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,5 +1,6 @@ | |||||||
| import { z, type AnyZodObject, type ZodTypeAny } from "zod"; | import { z } from "zod"; | ||||||
| import type { AnyZodRecord, RecordFullListOpts, RecordListOpts } from "./types.ts"; | import type { AnyZodObject, ZodTypeAny } from "zod"; | ||||||
|  | import type { AnyZodRecord, RecordFullListOpts, RecordListOpts } from "@/types.ts"; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Extends the given schema with the given expansion. |  * Extends the given schema with the given expansion. | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| import { type AnyZodObject, type objectUtil, z, type ZodEffects, type ZodObject, ZodOptional, type ZodRawShape } from "zod"; | import { z, ZodOptional, ZodEffects } from "zod"; | ||||||
| import type { AnyZodRecord, HasRequiredKeys, ZodRecordKeys } from "./types.ts"; | import type { AnyZodObject, objectUtil, ZodObject, ZodRawShape } from "zod"; | ||||||
|  | import type { AnyZodRecord, HasRequiredKeys, ZodRecordKeys } from "@/types.ts"; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Records list schema. |  * Records list schema. | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import Pocketbase from "pocketbase"; | import Pocketbase from "pocketbase"; | ||||||
| import type { Credentials } from "./config.ts"; | import type { Credentials } from "@/config.ts"; | ||||||
|  |  | ||||||
| let adminPocketbase: Pocketbase; | let adminPocketbase: Pocketbase; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,16 +1,17 @@ | |||||||
| #!/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"; | ||||||
| import { cancel, group, intro, log, outro, confirm, text, spinner, multiselect, isCancel } from "@clack/prompts"; | import { cancel, group, intro, log, outro, confirm, text, spinner, multiselect, isCancel } from "@clack/prompts"; | ||||||
| import { fetchCollections } from "../utils.ts"; | import { fetchCollections } from "@/utils.ts"; | ||||||
| import { generate } from "./utils.ts"; | import { generate } from "@/server/utils.ts"; | ||||||
| import { existsSync } from "node:fs"; | import { existsSync } from "node:fs"; | ||||||
|  |  | ||||||
| async function getConfig() { | async function getConfig() { | ||||||
|   const { config } = await loadConfig({ name: "zod-pocketbase", rcFile: false, dotenv: true }); |   const { config } = await loadConfig({ name: "zod-pocketbase-continue", rcFile: false, dotenv: true }); | ||||||
|   const { ZOD_POCKETBASE_ADMIN_EMAIL: adminEmail, ZOD_POCKETBASE_ADMIN_PASSWORD: adminPassword, ZOD_POCKETBASE_URL: url } = process.env; |   const { ZOD_POCKETBASE_ADMIN_EMAIL: adminEmail, ZOD_POCKETBASE_ADMIN_PASSWORD: adminPassword, ZOD_POCKETBASE_URL: url } = process.env; | ||||||
|   const result = Config.safeParse({ ...config, adminEmail, adminPassword, url }); |   const result = Config.safeParse({ ...config, adminEmail, adminPassword, url }); | ||||||
|   if (!result.success) { |   if (!result.success) { | ||||||
| @@ -72,7 +73,7 @@ async function setGeneratedFilePath(config: ResolvedConfig) { | |||||||
| } | } | ||||||
|  |  | ||||||
| const main = defineCommand({ | const main = defineCommand({ | ||||||
|   meta: { name: "zod-pocketbase", version: pkg.version, description: "Generate Zod schemas for your pocketbase instance." }, |   meta: { name: "zod-pocketbase-continue", version: pkg.version, description: "Generate Zod schemas for your pocketbase instance." }, | ||||||
|   run: async () => { |   run: async () => { | ||||||
|     intro(`ZOD POCKETBASE`); |     intro(`ZOD POCKETBASE`); | ||||||
|     const config = await getConfig(); |     const config = await getConfig(); | ||||||
|   | |||||||
| @@ -2,8 +2,8 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; | |||||||
| import { dirname, resolve } from "node:path"; | import { dirname, resolve } from "node:path"; | ||||||
| import { fileURLToPath } from "node:url"; | import { fileURLToPath } from "node:url"; | ||||||
| import type { CollectionModel } from "pocketbase"; | import type { CollectionModel } from "pocketbase"; | ||||||
| import { stringifyContent } from "../content.js"; | import { stringifyContent } from "@/content.js"; | ||||||
| import type { ResolvedConfig } from "../config.ts"; | import type { ResolvedConfig } from "@/config.ts"; | ||||||
|  |  | ||||||
| export async function generate(collections: CollectionModel[], opts: GenerateOpts) { | export async function generate(collections: CollectionModel[], opts: GenerateOpts) { | ||||||
|   const stub = readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), "../../assets/stubs/index.ts"), "utf-8"); |   const stub = readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), "../../assets/stubs/index.ts"), "utf-8"); | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import { sortBy } from "es-toolkit"; | import { sortBy } from "es-toolkit"; | ||||||
| import type { CollectionModel } from "pocketbase"; | import type { CollectionModel } from "pocketbase"; | ||||||
| import { getPocketbase } from "./sdk.js"; | import { getPocketbase } from "@/sdk.js"; | ||||||
| import type { Credentials } from "./config.ts"; | import type { Credentials } from "@/config.ts"; | ||||||
|  |  | ||||||
| export async function fetchCollections(credentials: Credentials): Promise<CollectionModel[]> { | export async function fetchCollections(credentials: Credentials): Promise<CollectionModel[]> { | ||||||
|   const pocketbase = await getPocketbase(credentials); |   const pocketbase = await getPocketbase(credentials); | ||||||
|   | |||||||
| @@ -46,7 +46,11 @@ | |||||||
|     // Report an error for unreachable code instead of just a warning. |     // Report an error for unreachable code instead of just a warning. | ||||||
|     "allowUnreachableCode": false, |     "allowUnreachableCode": false, | ||||||
|     // Report an error for unused labels instead of just a warning. |     // Report an error for unused labels instead of just a warning. | ||||||
|     "allowUnusedLabels": false |     "allowUnusedLabels": false, | ||||||
|  |     "baseUrl": ".", | ||||||
|  |     "paths": { | ||||||
|  |       "@/*": ["./src/*"] | ||||||
|  |     } | ||||||
|   }, |   }, | ||||||
|   "exclude": ["dist", "assets/stubs"] |   "exclude": ["dist", "assets/stubs"] | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { defineConfig } from "tsup"; | import { defineConfig } from "tsdown"; | ||||||
| import packageJson from "./package.json" with { type: "json" }; | import packageJson from "./package.json" with { type: "json" }; | ||||||
| 
 | 
 | ||||||
| export default defineConfig((options) => { | export default defineConfig((options) => { | ||||||
| @@ -7,7 +7,7 @@ export default defineConfig((options) => { | |||||||
|     entry: ["src/**/*.(ts|js)"], |     entry: ["src/**/*.(ts|js)"], | ||||||
|     format: ["esm"], |     format: ["esm"], | ||||||
|     target: "node24", |     target: "node24", | ||||||
|     bundle: true, |     unbundle: true, | ||||||
|     dts: true, |     dts: true, | ||||||
|     sourcemap: true, |     sourcemap: true, | ||||||
|     clean: true, |     clean: true, | ||||||
		Reference in New Issue
	
	Block a user