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