Compare commits
16 Commits
e895487743
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| dc0ba32801 | |||
| 4d1c06c5be | |||
| 8b1e9d1cf6 | |||
| 2412238236 | |||
| 6c88dd79d8 | |||
| 1638435e26 | |||
| 618d9591b4 | |||
| b2e9e82f5e | |||
| 81005c54cd | |||
| e02737359c | |||
| 827b53e630 | |||
| d4dfed9603 | |||
| 21c9fccf5e | |||
| e5e728705c | |||
| ea672a9d91 | |||
| ec9ef486c4 |
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
|
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
|
||||||
"changelog": "@changesets/cli/changelog",
|
"changelog": "@changesets/cli/changelog",
|
||||||
"commit": false,
|
"commit": false,
|
||||||
"fixed": [],
|
"fixed": [],
|
||||||
"linked": [],
|
"linked": [],
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"baseBranch": "main",
|
"baseBranch": "main",
|
||||||
"updateInternalDependencies": "patch"
|
"updateInternalDependencies": "patch"
|
||||||
}
|
}
|
||||||
|
|||||||
9
.zed/settings.json
Normal file
9
.zed/settings.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"lsp": {
|
||||||
|
"biome": {
|
||||||
|
"settings": {
|
||||||
|
"config_path": "./biome.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
README.md
38
README.md
@@ -1,9 +1,37 @@
|
|||||||
# 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
|
||||||
|
- Added `flake.nix` for **NixOS** development environment.
|
||||||
|
- Removed `doc/`, `playground/` and monorepo configuration
|
||||||
|
- Replaced `pnpm` and `node` in favor of `bun`
|
||||||
|
- Switched `.github/` to `.gitea/` and `gh` to `tea`
|
||||||
|
- Switched from `eslint` and `prettier` to `biome`.
|
||||||
|
|
||||||
|
### Dependencies & Code
|
||||||
|
- Migrated from `tsup` to `tsdown`
|
||||||
|
- Updated all npm dependencies
|
||||||
|
- Removed unused dependencies
|
||||||
|
- Fixed `getPocketbase` function in `sdk.ts` to match the latest PocketBase version
|
||||||
|
- Implemented most TODOs left in `content.ts`
|
||||||
|
|
||||||
|
### Not working (yet)
|
||||||
|
- Expanding `Relation` field with type `Multiple` is not working for now. Try to avoid using it.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT License](./LICENSE)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export type BaseModel = z.infer<typeof BaseModel>;
|
|||||||
export const AdminModel = z.object({
|
export const AdminModel = z.object({
|
||||||
...BaseModel.shape,
|
...BaseModel.shape,
|
||||||
avatar: z.string(),
|
avatar: z.string(),
|
||||||
email: z.string().email(),
|
email: z.email(),
|
||||||
});
|
});
|
||||||
export type AdminModel = z.infer<typeof AdminModel>;
|
export type AdminModel = z.infer<typeof AdminModel>;
|
||||||
|
|
||||||
@@ -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(z.string(), 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 (!Number.isNaN(num) && Number.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_@@
|
||||||
|
|
||||||
|
|||||||
41
biome.json
Normal file
41
biome.json
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/2.2.6/schema.json",
|
||||||
|
"vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false },
|
||||||
|
"files": {
|
||||||
|
"ignoreUnknown": false,
|
||||||
|
"includes": ["**", "!**/node_modules", "!**/assets/stubs/index.ts", "!**/dist"]
|
||||||
|
},
|
||||||
|
"formatter": {
|
||||||
|
"enabled": true,
|
||||||
|
"formatWithErrors": false,
|
||||||
|
"indentStyle": "tab",
|
||||||
|
"indentWidth": 2,
|
||||||
|
"lineEnding": "lf",
|
||||||
|
"lineWidth": 100,
|
||||||
|
"attributePosition": "auto",
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"expand": "auto",
|
||||||
|
"useEditorconfig": true
|
||||||
|
},
|
||||||
|
"linter": { "enabled": true, "rules": { "recommended": true } },
|
||||||
|
"javascript": {
|
||||||
|
"formatter": {
|
||||||
|
"jsxQuoteStyle": "double",
|
||||||
|
"quoteProperties": "asNeeded",
|
||||||
|
"trailingCommas": "all",
|
||||||
|
"semicolons": "asNeeded",
|
||||||
|
"arrowParentheses": "always",
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"quoteStyle": "single",
|
||||||
|
"attributePosition": "auto",
|
||||||
|
"bracketSpacing": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"html": { "formatter": { "selfCloseVoidElements": "always" } },
|
||||||
|
"overrides": [{ "includes": ["**/*.ts", "**/*.tsx"] }],
|
||||||
|
"assist": {
|
||||||
|
"enabled": true,
|
||||||
|
"actions": { "source": { "organizeImports": "on" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import astro from "eslint-plugin-astro";
|
|
||||||
|
|
||||||
export default [...astro.configs["flat/recommended"], ...astro.configs["flat/jsx-a11y-strict"]];
|
|
||||||
35
flake.nix
35
flake.nix
@@ -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";
|
||||||
@@ -17,7 +17,6 @@
|
|||||||
let
|
let
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
bun = pkgs.bun;
|
bun = pkgs.bun;
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
devShells.default = pkgs.mkShell {
|
devShells.default = pkgs.mkShell {
|
||||||
@@ -26,19 +25,22 @@
|
|||||||
git
|
git
|
||||||
nodejs_24
|
nodejs_24
|
||||||
tea
|
tea
|
||||||
# nodePackages.node-inspector
|
|
||||||
];
|
];
|
||||||
|
|
||||||
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 ""
|
||||||
echo "Available commands:"
|
echo "Available commands:"
|
||||||
echo " bun install - Install dependencies"
|
echo " bun install - Install dependencies"
|
||||||
echo " bun run dev - Start development"
|
echo " bun run dev - Start development (build with --watch flag)"
|
||||||
echo " bun build - Build with Bun's bundler"
|
echo " bun run build - Build library"
|
||||||
echo " tea releases create - Create Gitea release"
|
echo " bun run lint - Run Biome check"
|
||||||
|
echo " bun run lint:w - Run Biome check (with --write)"
|
||||||
|
echo " bun run format - Run Biome format"
|
||||||
|
echo " bun run format:w - Run Biome format (with --write)"
|
||||||
|
echo " bun run release - Run release script"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if [ ! -d "node_modules" ]; then
|
if [ ! -d "node_modules" ]; then
|
||||||
@@ -50,25 +52,6 @@
|
|||||||
NODE_ENV = "development";
|
NODE_ENV = "development";
|
||||||
BUN_RUNTIME = "bun";
|
BUN_RUNTIME = "bun";
|
||||||
};
|
};
|
||||||
|
|
||||||
packages.default = pkgs.stdenv.mkDerivation {
|
|
||||||
name = "zod-pocketbase";
|
|
||||||
src = ./.;
|
|
||||||
|
|
||||||
buildInputs = [ bun ];
|
|
||||||
|
|
||||||
buildPhase = ''
|
|
||||||
export HOME=$TMPDIR
|
|
||||||
bun install --frozen-lockfile
|
|
||||||
bun run build
|
|
||||||
'';
|
|
||||||
|
|
||||||
installPhase = ''
|
|
||||||
mkdir -p $out
|
|
||||||
cp -r dist $out/
|
|
||||||
cp package.json $out/
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
138
package.json
138
package.json
@@ -1,77 +1,65 @@
|
|||||||
{
|
{
|
||||||
"name": "zod-pocketbase",
|
"name": "zod-pocketbase-continue",
|
||||||
"version": "0.5.0",
|
"version": "0.6.1",
|
||||||
"description": "",
|
"description": "Zod tooling for your PocketBase instance.",
|
||||||
"author": {
|
"author": {
|
||||||
"email": "garandplg@garandplg.com",
|
"email": "garandplg@garandplg.com",
|
||||||
"name": "Garand_PLG",
|
"name": "Garand_PLG",
|
||||||
"url": "https://gitea.garandplg.com"
|
"url": "https://gitea.garandplg.com"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"keywords": [
|
"keywords": ["pocketbase", "schemas", "typescript", "typegen", "type generation", "zod"],
|
||||||
"pocketbase",
|
"homepage": "https://gitea.garandplg.com/GarandPLG/zod-pocketbase-continue",
|
||||||
"schemas",
|
"publishConfig": {
|
||||||
"typescript",
|
"access": "public"
|
||||||
"typegen",
|
},
|
||||||
"type generation",
|
"type": "module",
|
||||||
"zod"
|
"sideEffects": false,
|
||||||
],
|
"packageManager": "bun@1.2.23",
|
||||||
"homepage": "https://gitea.garandplg.com/GarandPLG/zod-pocketbase",
|
"engines": {
|
||||||
"publishConfig": {
|
"node": ">=24.9.0"
|
||||||
"access": "public"
|
},
|
||||||
},
|
"main": "dist/index.js",
|
||||||
"type": "module",
|
"bin": {
|
||||||
"sideEffects": false,
|
"zod-pocketbase-continue": "dist/server/cli.js"
|
||||||
"packageManager": "bun@1.2.23",
|
},
|
||||||
"engines": {
|
"exports": {
|
||||||
"node": ">=24.9.0"
|
".": {
|
||||||
},
|
"types": "./dist/index.d.ts",
|
||||||
"main": "dist/index.js",
|
"default": "./dist/index.js"
|
||||||
"bin": {
|
},
|
||||||
"zod-pocketbase": "dist/server/cli.js"
|
"./server": {
|
||||||
},
|
"types": "./dist/server/index.d.ts",
|
||||||
"exports": {
|
"default": "./dist/server/index.js"
|
||||||
".": {
|
}
|
||||||
"types": "./dist/index.d.ts",
|
},
|
||||||
"default": "./dist/index.js"
|
"files": ["dist", "assets"],
|
||||||
},
|
"scripts": {
|
||||||
"./server": {
|
"dev": "tsdown --watch",
|
||||||
"types": "./dist/server/index.d.ts",
|
"build": "tsdown",
|
||||||
"default": "./dist/server/index.js"
|
"changeset": "changeset",
|
||||||
}
|
"release": "bun scripts/release.mjs",
|
||||||
},
|
"lint": "biome check .",
|
||||||
"files": [
|
"lint:w": "biome check --write .",
|
||||||
"dist",
|
"format": "biome format .",
|
||||||
"assets"
|
"format:w": "biome format --write ."
|
||||||
],
|
},
|
||||||
"scripts": {
|
"dependencies": {
|
||||||
"dev": "tsup --watch",
|
"@clack/prompts": "^0.11.0",
|
||||||
"build": "tsup",
|
"c12": "^3.3.1",
|
||||||
"changeset": "changeset",
|
"citty": "^0.1.6",
|
||||||
"release": "bun scripts/release.mjs"
|
"es-toolkit": "^1.40.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"devDependencies": {
|
||||||
"@clack/prompts": "^0.9.0",
|
"@biomejs/biome": "2.2.6",
|
||||||
"c12": "^2.0.1",
|
"@changesets/cli": "^2.29.7",
|
||||||
"citty": "^0.1.6",
|
"@types/node": "^24.8.1",
|
||||||
"es-toolkit": "^1.30.1"
|
"pocketbase": "^0.26.2",
|
||||||
},
|
"tsdown": "^0.15.8",
|
||||||
"devDependencies": {
|
"zod": "^4.1.12"
|
||||||
"@changesets/cli": "^2.27.11",
|
},
|
||||||
"@types/node": "^22.10.2",
|
"peerDependencies": {
|
||||||
"@typescript-eslint/parser": "^8.18.1",
|
"pocketbase": "^0.26.2",
|
||||||
"eslint": "^9.17.0",
|
"zod": "^4.1.12"
|
||||||
"eslint-plugin-astro": "^1.3.1",
|
}
|
||||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
||||||
"pocketbase": "<0.22.0",
|
|
||||||
"prettier": "^3.4.2",
|
|
||||||
"prettier-plugin-astro": "^0.14.1",
|
|
||||||
"prettier-plugin-tailwindcss": "^0.6.9",
|
|
||||||
"tsup": "^8.3.5",
|
|
||||||
"zod": "^3.24.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"pocketbase": "<0.22.0",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
/** @type {import("prettier").Config} */
|
|
||||||
export default {
|
|
||||||
printWidth: 140,
|
|
||||||
plugins: ["prettier-plugin-astro", "prettier-plugin-tailwindcss"],
|
|
||||||
overrides: [{ files: "*.astro", options: { parser: "astro" } }],
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { spawn } from "node:child_process";
|
import { spawn } from 'node:child_process'
|
||||||
import { resolve } from "node:path";
|
import { resolve } from 'node:path'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -9,41 +9,41 @@ import { resolve } from "node:path";
|
|||||||
* @returns {Promise<string>}
|
* @returns {Promise<string>}
|
||||||
*/
|
*/
|
||||||
const run = async (command, ...args) => {
|
const run = async (command, ...args) => {
|
||||||
const cwd = resolve();
|
const cwd = resolve()
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const cmd = spawn(command, args, {
|
const cmd = spawn(command, args, {
|
||||||
stdio: ["inherit", "pipe", "pipe"], // Inherit stdin, pipe stdout, pipe stderr
|
stdio: ['inherit', 'pipe', 'pipe'], // Inherit stdin, pipe stdout, pipe stderr
|
||||||
shell: true,
|
shell: true,
|
||||||
cwd,
|
cwd,
|
||||||
});
|
})
|
||||||
|
|
||||||
let output = "";
|
let output = ''
|
||||||
|
|
||||||
cmd.stdout.on("data", (data) => {
|
cmd.stdout.on('data', (data) => {
|
||||||
process.stdout.write(data.toString());
|
process.stdout.write(data.toString())
|
||||||
output += data.toString();
|
output += data.toString()
|
||||||
});
|
})
|
||||||
|
|
||||||
cmd.stderr.on("data", (data) => {
|
cmd.stderr.on('data', (data) => {
|
||||||
process.stderr.write(data.toString());
|
process.stderr.write(data.toString())
|
||||||
});
|
})
|
||||||
|
|
||||||
cmd.on("close", () => {
|
cmd.on('close', () => {
|
||||||
resolve(output);
|
resolve(output)
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
await run("bun changeset version");
|
await run('bun changeset version')
|
||||||
await run("git add .");
|
await run('git add .')
|
||||||
await run('git commit -m "chore: update version"');
|
await run('git commit -m "chore: update version"')
|
||||||
await run("git push");
|
await run('git push')
|
||||||
await run("bun run build");
|
await run('bun run build')
|
||||||
await run("bun changeset publish");
|
await run('bun changeset publish')
|
||||||
await run("git push --follow-tags");
|
await run('git push --follow-tags')
|
||||||
const tag = (await run("git describe --abbrev=0")).replace("\n", "");
|
const tag = (await run('git describe --abbrev=0')).replace('\n', '')
|
||||||
await run(`tea releases create --tag ${tag} --title "${tag}"`);
|
await run(`tea releases create --tag ${tag} --title "${tag}"`)
|
||||||
};
|
}
|
||||||
|
|
||||||
main();
|
main()
|
||||||
|
|||||||
124
src/config.ts
124
src/config.ts
@@ -1,66 +1,88 @@
|
|||||||
import { pascalCase, snakeCase } from "es-toolkit";
|
import { pascalCase, snakeCase } from 'es-toolkit'
|
||||||
import { z } from "zod";
|
import { z } from 'zod'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default config values.
|
* Default config values.
|
||||||
*/
|
*/
|
||||||
export const defaultConfig = {
|
export const defaultConfig = {
|
||||||
ignore: [],
|
ignore: [],
|
||||||
nameEnum: (name: string) => snakeCase(name).toUpperCase(),
|
nameEnum: (name: string) => snakeCase(name).toUpperCase(),
|
||||||
nameEnumField: (collectionName: string, fieldName: string) => `${collectionName}${pascalCase(fieldName)}`,
|
nameEnumField: (collectionName: string, fieldName: string) =>
|
||||||
nameEnumSchema: (name: string) => pascalCase(name),
|
`${collectionName}${pascalCase(fieldName)}`,
|
||||||
nameEnumType: (name: string) => pascalCase(name),
|
nameEnumSchema: (name: string) => pascalCase(name),
|
||||||
nameEnumValues: (name: string) => `${name}Values`,
|
nameEnumType: (name: string) => pascalCase(name),
|
||||||
nameRecordSchema: (name: string) => `${pascalCase(name)}Record`,
|
nameEnumValues: (name: string) => `${name}Values`,
|
||||||
nameRecordType: (name: string) => `${pascalCase(name)}Record`,
|
nameRecordSchema: (name: string) => `${pascalCase(name)}Record`,
|
||||||
output: "./zod-pocketbase.ts",
|
nameRecordType: (name: string) => `${pascalCase(name)}Record`,
|
||||||
};
|
output: './zod-pocketbase-continue.ts',
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schema for the PocketBase credentials.
|
* Schema for the PocketBase credentials.
|
||||||
*/
|
*/
|
||||||
export const Credentials = z.object({
|
export const Credentials = z.object({
|
||||||
adminEmail: z.string().email(),
|
adminEmail: z.email(),
|
||||||
adminPassword: z.string(),
|
adminPassword: z.string(),
|
||||||
url: z.string().url(),
|
url: z.url(),
|
||||||
});
|
})
|
||||||
export type Credentials = z.infer<typeof Credentials>;
|
export type Credentials = z.infer<typeof Credentials>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schema for the config file.
|
* Schema for the config file.
|
||||||
*/
|
*/
|
||||||
export const Config = z.object({
|
export const Config = z.object({
|
||||||
...Credentials.partial().shape,
|
...Credentials.partial().shape,
|
||||||
ignore: z.string().array().default(defaultConfig.ignore),
|
ignore: z.string().array().default(defaultConfig.ignore),
|
||||||
nameEnum: z
|
nameEnum: z
|
||||||
.function(z.tuple([z.string()]), z.string())
|
.function({
|
||||||
.optional()
|
input: [z.string()],
|
||||||
.transform((f) => f ?? defaultConfig.nameEnum),
|
output: z.string(),
|
||||||
nameEnumField: z
|
})
|
||||||
.function(z.tuple([z.string(), z.string()]), z.string())
|
.optional()
|
||||||
.optional()
|
.transform((f) => f ?? defaultConfig.nameEnum),
|
||||||
.transform((f) => f ?? defaultConfig.nameEnumField),
|
nameEnumField: z
|
||||||
nameEnumSchema: z
|
.function({
|
||||||
.function(z.tuple([z.string()]), z.string())
|
input: [z.string(), z.string()],
|
||||||
.optional()
|
output: z.string(),
|
||||||
.transform((f) => f ?? defaultConfig.nameEnumSchema),
|
})
|
||||||
nameEnumType: z
|
.optional()
|
||||||
.function(z.tuple([z.string()]), z.string())
|
.transform((f) => f ?? defaultConfig.nameEnumField),
|
||||||
.optional()
|
nameEnumSchema: z
|
||||||
.transform((f) => f ?? defaultConfig.nameEnumType),
|
.function({
|
||||||
nameEnumValues: z
|
input: [z.string()],
|
||||||
.function(z.tuple([z.string()]), z.string())
|
output: z.string(),
|
||||||
.optional()
|
})
|
||||||
.transform((f) => f ?? defaultConfig.nameEnumValues),
|
.optional()
|
||||||
nameRecordSchema: z
|
.transform((f) => f ?? defaultConfig.nameEnumSchema),
|
||||||
.function(z.tuple([z.string()]), z.string())
|
nameEnumType: z
|
||||||
.optional()
|
.function({
|
||||||
.transform((f) => f ?? defaultConfig.nameRecordSchema),
|
input: [z.string()],
|
||||||
nameRecordType: z
|
output: z.string(),
|
||||||
.function(z.tuple([z.string()]), z.string())
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.transform((f) => f ?? defaultConfig.nameRecordType),
|
.transform((f) => f ?? defaultConfig.nameEnumType),
|
||||||
output: z.string().default(defaultConfig.output),
|
nameEnumValues: z
|
||||||
});
|
.function({
|
||||||
export type Config = z.input<typeof Config>;
|
input: [z.string()],
|
||||||
export type ResolvedConfig = z.infer<typeof Config>;
|
output: z.string(),
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.transform((f) => f ?? defaultConfig.nameEnumValues),
|
||||||
|
nameRecordSchema: z
|
||||||
|
.function({
|
||||||
|
input: [z.string()],
|
||||||
|
output: z.string(),
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.transform((f) => f ?? defaultConfig.nameRecordSchema),
|
||||||
|
nameRecordType: z
|
||||||
|
.function({
|
||||||
|
input: [z.string()],
|
||||||
|
output: z.string(),
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.transform((f) => f ?? defaultConfig.nameRecordType),
|
||||||
|
output: z.string().default(defaultConfig.output),
|
||||||
|
})
|
||||||
|
export type Config = z.input<typeof Config>
|
||||||
|
export type ResolvedConfig = z.infer<typeof Config>
|
||||||
|
|||||||
281
src/content.ts
281
src/content.ts
@@ -1,120 +1,203 @@
|
|||||||
import { sortBy } from "es-toolkit";
|
import { sortBy } from 'es-toolkit'
|
||||||
import type { CollectionModel, SchemaField } from "pocketbase";
|
import type { CollectionField, CollectionModel } 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() {
|
||||||
return collections.flatMap((collection) =>
|
return collections.flatMap((collection) =>
|
||||||
collection.schema
|
collection.fields
|
||||||
.filter((field) => field.type === "select")
|
.filter((field: CollectionField) => field.type === 'select')
|
||||||
.map((field) => ({ name: opts.nameEnumField(collection.name, field.name), values: (field.options.values ?? []) as string[] })),
|
.map((field: CollectionField) => ({
|
||||||
);
|
name: opts.nameEnumField(collection.name, field.name),
|
||||||
}
|
values: ((field as any).values ?? []) as string[],
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function stringifyEnum({ name, values }: SelectField) {
|
function stringifyEnum({ name, values }: SelectField) {
|
||||||
const valuesName = opts.nameEnumValues(name);
|
const valuesName = opts.nameEnumValues(name)
|
||||||
const schemaName = opts.nameEnumSchema(name);
|
const schemaName = opts.nameEnumSchema(name)
|
||||||
const enumName = opts.nameEnum(name);
|
const enumName = opts.nameEnum(name)
|
||||||
const typeName = opts.nameEnumType(name);
|
const typeName = opts.nameEnumType(name)
|
||||||
return `export const ${valuesName} = [\n\t${values.map((value) => `"${value}"`).join(",\n\t")},\n] as const;\nexport const ${schemaName} = z.enum(${valuesName});\nexport type ${typeName} = z.infer<typeof ${schemaName}>;\nexport const ${enumName} = ${schemaName}.enum;`;
|
return `export const ${valuesName} = [\n\t${values.map((value) => `"${value}"`).join(',\n\t')},\n] as const;\nexport const ${schemaName} = z.enum(${valuesName});\nexport type ${typeName} = z.infer<typeof ${schemaName}>;\nexport const ${enumName} = ${schemaName}.enum;`
|
||||||
}
|
}
|
||||||
|
|
||||||
function stringifyRecord({ name, schema }: CollectionModel) {
|
function stringifyRecord({ name, fields }: CollectionModel) {
|
||||||
const schemaName = opts.nameRecordSchema(name);
|
const schemaName = opts.nameRecordSchema(name)
|
||||||
const typeName = opts.nameRecordType(name);
|
const typeName = opts.nameRecordType(name)
|
||||||
const fields = sortBy(schema, ["name"]).map((field) => stringifyField(field, name));
|
|
||||||
return `export const ${schemaName} = z.object({\n\t...RecordModel.omit({ expand: true }).shape,\n\tcollectionName: z.literal("${name}"),\n\t${fields.join(",\n\t")},\n});\nexport type ${typeName} = z.infer<typeof ${schemaName}>;`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function stringifyField(field: SchemaField, collectionName: string) {
|
const systemFields = new Set([
|
||||||
let schema: string | undefined;
|
'id',
|
||||||
if (field.type === "bool") schema = stringifyBoolField(field);
|
'created',
|
||||||
else if (field.type === "date") schema = stringifyDateField(field);
|
'updated',
|
||||||
else if (field.type === "editor") schema = stringifyEditorField(field);
|
'collectionId',
|
||||||
else if (field.type === "email") schema = stringifyEmailField(field);
|
'collectionName',
|
||||||
else if (field.type === "file") schema = stringifyFileField(field);
|
'expand',
|
||||||
else if (field.type === "json") schema = stringifyJsonField(field);
|
'password',
|
||||||
else if (field.type === "number") schema = stringifyNumberField(field);
|
'tokenKey',
|
||||||
else if (field.type === "relation") schema = stringifyRelationField(field);
|
])
|
||||||
else if (field.type === "select") schema = stringifySelectField(field, collectionName);
|
|
||||||
else if (field.type === "text") schema = stringifyTextField(field);
|
|
||||||
else if (field.type === "url") schema = stringifyUrlField(field);
|
|
||||||
// TODO: manage unknown field type
|
|
||||||
return `${field.name}: ${schema}${field.required ? "" : ".optional()"}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function stringifyBoolField(_: SchemaField) {
|
const customFields = fields.filter((field) => !systemFields.has(field.name))
|
||||||
return "z.boolean()";
|
const fieldStrings = sortBy(customFields, ['name']).map((field) => stringifyField(field, name))
|
||||||
}
|
|
||||||
|
|
||||||
function stringifyDateField(_field: SchemaField) {
|
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}>;`
|
||||||
// TODO: implement min and max
|
}
|
||||||
return "z.string().pipe(z.coerce.date())";
|
|
||||||
}
|
|
||||||
|
|
||||||
function stringifyEditorField(_field: SchemaField) {
|
function stringifySchemasEntry({ name }: CollectionModel) {
|
||||||
// TODO: implement convertUrls
|
return `["${name}", ${opts.nameRecordSchema(name)}]`
|
||||||
return "z.string()";
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function stringifyEmailField(_field: SchemaField) {
|
function stringifyService({ name }: CollectionModel) {
|
||||||
// TODO: implement exceptDomains and onlyDomains
|
return `\t\tcollection(idOrName: "${name}"): RecordService<z.input<typeof ${opts.nameRecordSchema(name)}>>;`
|
||||||
return "z.string().email()";
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function stringifyFileField({ options: { maxSelect } }: SchemaField) {
|
function stringifyField(field: CollectionField, collectionName: string) {
|
||||||
// TODO: implement maxSize, mimeTypes, protected, thumbs
|
let schema: string
|
||||||
return `z.string()${maxSelect === 1 ? "" : `.array().max(${maxSelect})`}`;
|
switch (field.type) {
|
||||||
}
|
case 'bool':
|
||||||
|
schema = 'z.boolean()'
|
||||||
|
break
|
||||||
|
|
||||||
function stringifyJsonField(_field: SchemaField) {
|
case 'date': {
|
||||||
// TODO: implement maxSize and json schema
|
const minConstraintDate = field.min ? `.min(new Date("${field.min}"))` : ''
|
||||||
return "z.any()";
|
const maxConstraintDate = field.max ? `.max(new Date("${field.max}"))` : ''
|
||||||
}
|
|
||||||
|
|
||||||
function stringifyNumberField({ options: { max, min, noDecimal } }: SchemaField) {
|
schema = `z.coerce.date()${minConstraintDate}${maxConstraintDate}`
|
||||||
return `z.number()${noDecimal ? ".int()" : ""}${min ? `.min(${min})` : ""}${max ? `.max(${max})` : ""}`;
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
function stringifyRelationField({ options, required }: SchemaField) {
|
case 'editor':
|
||||||
const { maxSelect, minSelect } = options;
|
// TODO: implement convertUrls
|
||||||
// TODO: implement cascadeDelete, displayFields
|
schema = 'z.string()'
|
||||||
const min = minSelect ? `.min(${minSelect})` : "";
|
break
|
||||||
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({ name, options: { maxSelect } }: SchemaField, collectionName: string) {
|
case 'email': {
|
||||||
// TODO: implement values
|
const onlyDomainsConstraint = createDomainConstraint(field.onlyDomains, true, 'email')
|
||||||
return `${opts.nameEnumSchema(opts.nameEnumField(collectionName, name))}${maxSelect === 1 ? "" : `.array().max(${maxSelect})`}`;
|
const exceptDomainsConstraint = createDomainConstraint(field.exceptDomains, false, 'email')
|
||||||
}
|
|
||||||
|
|
||||||
function stringifyTextField({ options: { max, min } }: SchemaField) {
|
schema = `z.email()${onlyDomainsConstraint}${exceptDomainsConstraint}`
|
||||||
// TODO: implement pattern
|
break
|
||||||
return `z.string()${min ? `.min(${min})` : ""}${max ? `.max(${max})` : ""}`;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function stringifyUrlField(_field: SchemaField) {
|
case 'file': {
|
||||||
// TODO: implement exceptDomains and onlyDomains
|
const maxSelectFile: number = field.maxSelect
|
||||||
return "z.string().url()";
|
const maxSizeFile: number = field.maxSize
|
||||||
}
|
const mimeTypesArray: string[] = field.mimeTypes || []
|
||||||
|
// const protectedFile: boolean = field.protected;
|
||||||
|
// const thumbsFileArray: string[] = field.thumbs || [];
|
||||||
|
|
||||||
function stringifySchemasEntry({ name }: CollectionModel) {
|
const fileFieldMaxSelect = maxSelectFile ? `.max(${maxSelectFile})` : ''
|
||||||
return `["${name}", ${opts.nameRecordSchema(name)}]`;
|
const fileFieldTypeArray = maxSelectFile === 1 ? '' : `.array()${fileFieldMaxSelect}`
|
||||||
}
|
|
||||||
|
|
||||||
function stringifyService({ name }: CollectionModel) {
|
const fileMaxSizeFile = maxSizeFile ? `.max(${maxSizeFile})` : ''
|
||||||
return `\t\tcollection(idOrName: "${name}"): RecordService<z.input<typeof ${opts.nameRecordSchema(name)}>>;`;
|
const fileMimeTypesArray =
|
||||||
}
|
mimeTypesArray.length > 0 ? `.mime(${JSON.stringify(mimeTypesArray)})` : ''
|
||||||
|
|
||||||
return {
|
const baseFileSchema = `z.union([z.string(), z.file()${fileMaxSizeFile}${fileMimeTypesArray}])`
|
||||||
collectionNames: `[\n\t${collections.map(({ name }) => `"${name}"`).join(",\n\t")},\n]`,
|
schema = `${baseFileSchema}${fileFieldTypeArray}`
|
||||||
enums: getCollectionSelectFields().map(stringifyEnum).join("\n\n"),
|
break
|
||||||
records: `${collections.map(stringifyRecord).join("\n\n")}\n\nexport const records = new Map<Collection, z.AnyZodObject>([\n\t${collections.map(stringifySchemasEntry).join(",\n\t")},\n]);`,
|
}
|
||||||
services: collections.map(stringifyService).join("\n"),
|
|
||||||
};
|
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.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 {
|
||||||
|
collectionNames: `[\n\t${collections.map(({ name }) => `"${name}"`).join(',\n\t')},\n]`,
|
||||||
|
enums: getCollectionSelectFields().map(stringifyEnum).join('\n\n'),
|
||||||
|
records: `${collections.map(stringifyRecord).join('\n\n')}\n\nexport const records = new Map<Collection, z.ZodObject>([\n\t${collections.map(stringifySchemasEntry).join(',\n\t')},\n]);`,
|
||||||
|
services: collections.map(stringifyService).join('\n'),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SelectField = { name: string; values: string[] };
|
export type SelectField = { name: string; values: string[] }
|
||||||
|
|||||||
@@ -1,29 +1,45 @@
|
|||||||
import type { default as Pocketbase, SendOptions } from "pocketbase";
|
import type { default as Pocketbase, SendOptions } from 'pocketbase'
|
||||||
import { fullListOptionsFrom, optionsFrom } from "./options.js";
|
import type { ZodObject } from 'zod'
|
||||||
import type { AnyZodRecord, RecordFullListOpts, RecordIdRef, RecordRef, RecordSlugRef } from "./types.ts";
|
import { fullListOptionsFrom, optionsFrom } from '@/options.ts'
|
||||||
import { AnyRecordsList, type RecordsList } from "./schemas.ts";
|
import type { RecordsList } from '@/schemas.ts'
|
||||||
|
import { AnyRecordsList } from '@/schemas.ts'
|
||||||
|
import type { RecordFullListOpts, RecordIdRef, RecordRef, RecordSlugRef } from '@/types.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 ZodObject>(
|
||||||
async function getRecord<C extends string, S extends AnyZodRecord>(ref: RecordIdRef<C>, opts: GetRecordOpts<S>): Promise<S["_output"]>;
|
ref: RecordSlugRef<C>,
|
||||||
async function getRecord<C extends string, S extends AnyZodRecord>(ref: RecordRef<C>, opts: GetRecordOpts<S>) {
|
opts: GetRecordOpts<S>,
|
||||||
const { schema } = opts;
|
): Promise<S['_output']>
|
||||||
const sdkOpts = { ...optionsFrom(schema), ...(fetch ? { fetch } : {}) };
|
async function getRecord<C extends string, S extends ZodObject>(
|
||||||
const unsafeRecord = await ("id" in ref
|
ref: RecordIdRef<C>,
|
||||||
? pocketbase.collection(ref.collection).getOne(ref.id, sdkOpts)
|
opts: GetRecordOpts<S>,
|
||||||
: pocketbase.collection(ref.collection).getFirstListItem(`slug = "${ref.slug}"`, sdkOpts));
|
): Promise<S['_output']>
|
||||||
return schema.parseAsync(unsafeRecord);
|
async function getRecord<C extends string, S extends ZodObject>(
|
||||||
}
|
ref: RecordRef<C>,
|
||||||
|
opts: GetRecordOpts<S>,
|
||||||
|
) {
|
||||||
|
const { schema } = opts
|
||||||
|
const sdkOpts = { ...optionsFrom(schema), ...(fetch ? { fetch } : {}) }
|
||||||
|
const unsafeRecord = await ('id' in ref
|
||||||
|
? pocketbase.collection(ref.collection).getOne(ref.id, sdkOpts)
|
||||||
|
: pocketbase.collection(ref.collection).getFirstListItem(`slug = "${ref.slug}"`, sdkOpts))
|
||||||
|
return schema.parseAsync(unsafeRecord)
|
||||||
|
}
|
||||||
|
|
||||||
async function getRecords<C extends string, S extends AnyZodRecord>(collection: C, opts: GetRecordsOpts<S>): Promise<RecordsList<S>> {
|
async function getRecords<C extends string, S extends ZodObject>(
|
||||||
const { schema, ...otherOpts } = opts;
|
collection: C,
|
||||||
const sdkOpts = { ...fullListOptionsFrom(schema, otherOpts), ...(fetch ? { fetch } : {}) };
|
opts: GetRecordsOpts<S>,
|
||||||
const recordsList = await pocketbase.collection(collection).getList(sdkOpts.page, sdkOpts.perPage, sdkOpts);
|
): Promise<RecordsList<S>> {
|
||||||
return AnyRecordsList.extend({ items: schema.array() }).parseAsync(recordsList);
|
const { schema, ...otherOpts } = opts
|
||||||
}
|
const sdkOpts = { ...fullListOptionsFrom(schema, otherOpts), ...(fetch ? { fetch } : {}) }
|
||||||
|
const recordsList = await pocketbase
|
||||||
|
.collection(collection)
|
||||||
|
.getList(sdkOpts.page, sdkOpts.perPage, sdkOpts)
|
||||||
|
return AnyRecordsList.extend({ items: schema.array() }).parseAsync(recordsList)
|
||||||
|
}
|
||||||
|
|
||||||
return { getRecord, getRecords };
|
return { getRecord, getRecords }
|
||||||
}
|
}
|
||||||
export type GetRecordOpts<S extends AnyZodRecord> = { schema: S };
|
export type GetRecordOpts<S extends ZodObject> = { schema: S }
|
||||||
export type GetRecordsOpts<S extends AnyZodRecord> = RecordFullListOpts<S> & { schema: S };
|
export type GetRecordsOpts<S extends ZodObject> = RecordFullListOpts<S> & { schema: S }
|
||||||
export type HelpersFromOpts = { fetch?: SendOptions["fetch"]; pocketbase: Pocketbase };
|
export type HelpersFromOpts = { fetch?: SendOptions['fetch']; pocketbase: Pocketbase }
|
||||||
|
|||||||
12
src/index.ts
12
src/index.ts
@@ -1,6 +1,6 @@
|
|||||||
export * from "./config.js";
|
export * from '@/config.js'
|
||||||
export * from "./helpers.js";
|
export * from '@/helpers.js'
|
||||||
export * from "./options.js";
|
export * from '@/options.js'
|
||||||
export * from "./schemas.js";
|
export * from '@/schemas.js'
|
||||||
export * from "./types.ts";
|
export * from '@/types.ts'
|
||||||
export * from "./utils.js";
|
export * from '@/utils.js'
|
||||||
|
|||||||
154
src/options.ts
154
src/options.ts
@@ -1,5 +1,6 @@
|
|||||||
import { z, type AnyZodObject, type ZodTypeAny } from "zod";
|
import type { ZodObject } from 'zod'
|
||||||
import type { AnyZodRecord, RecordFullListOpts, RecordListOpts } from "./types.ts";
|
import { z } from 'zod'
|
||||||
|
import type { RecordFullListOpts, RecordListOpts } from '@/types.ts'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extends the given schema with the given expansion.
|
* Extends the given schema with the given expansion.
|
||||||
@@ -7,10 +8,10 @@ import type { AnyZodRecord, RecordFullListOpts, RecordListOpts } from "./types.t
|
|||||||
* @param shape - The shape of the expansion
|
* @param shape - The shape of the expansion
|
||||||
* @returns A new schema extended with the given expansion
|
* @returns A new schema extended with the given expansion
|
||||||
*/
|
*/
|
||||||
export function expandFrom<S extends AnyZodRecord>(schema: S) {
|
export function expandFrom<S extends ZodObject<any, any>>(schema: S) {
|
||||||
return expandFromRec(schema)
|
return expandFromRec(schema)
|
||||||
.sort((k1, k2) => (k1 < k2 ? -1 : 1))
|
.sort((k1, k2) => (k1 < k2 ? -1 : 1))
|
||||||
.join(",");
|
.join(',')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,10 +20,10 @@ export function expandFrom<S extends AnyZodRecord>(schema: S) {
|
|||||||
* @param shape - The shape of the expansion
|
* @param shape - The shape of the expansion
|
||||||
* @returns A new schema extended with the given expansion
|
* @returns A new schema extended with the given expansion
|
||||||
*/
|
*/
|
||||||
export function fieldsFrom<S extends AnyZodRecord>(schema: S) {
|
export function fieldsFrom<S extends ZodObject<any, any>>(schema: S) {
|
||||||
return fieldsFromRec(schema)
|
return fieldsFromRec(schema)
|
||||||
.sort((k1, k2) => (k1 < k2 ? -1 : 1))
|
.sort((k1, k2) => (k1 < k2 ? -1 : 1))
|
||||||
.join(",");
|
.join(',')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,8 +32,8 @@ export function fieldsFrom<S extends AnyZodRecord>(schema: S) {
|
|||||||
* @param shape - The shape of the expansion
|
* @param shape - The shape of the expansion
|
||||||
* @returns A new schema extended with the given expansion
|
* @returns A new schema extended with the given expansion
|
||||||
*/
|
*/
|
||||||
export function optionsFrom<S extends AnyZodRecord>(schema: S) {
|
export function optionsFrom<S extends ZodObject<any, any>>(schema: S) {
|
||||||
return { expand: expandFrom(schema), fields: fieldsFrom(schema) };
|
return { expand: expandFrom(schema), fields: fieldsFrom(schema) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,9 +42,9 @@ export function optionsFrom<S extends AnyZodRecord>(schema: S) {
|
|||||||
* @param shape - The shape of the expansion
|
* @param shape - The shape of the expansion
|
||||||
* @returns A new schema extended with the given expansion
|
* @returns A new schema extended with the given expansion
|
||||||
*/
|
*/
|
||||||
export function listOptionsFrom<S extends AnyZodRecord>(schema: S, opts: RecordListOpts<S>) {
|
export function listOptionsFrom<S extends ZodObject<any, any>>(schema: S, opts: RecordListOpts<S>) {
|
||||||
const { page = 1, perPage = 30, ...rest } = opts;
|
const { page = 1, perPage = 30, ...rest } = opts
|
||||||
return { ...optionsFrom(schema), page, perPage, ...rest };
|
return { ...optionsFrom(schema), page, perPage, ...rest }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,46 +53,101 @@ export function listOptionsFrom<S extends AnyZodRecord>(schema: S, opts: RecordL
|
|||||||
* @param shape - The shape of the expansion
|
* @param shape - The shape of the expansion
|
||||||
* @returns A new schema extended with the given expansion
|
* @returns A new schema extended with the given expansion
|
||||||
*/
|
*/
|
||||||
export function fullListOptionsFrom<S extends AnyZodRecord>(schema: S, opts: RecordFullListOpts<S>) {
|
export function fullListOptionsFrom<S extends ZodObject<any, any>>(
|
||||||
const { page = 1, perPage = 200, skipTotal = true, ...rest } = opts;
|
schema: S,
|
||||||
return listOptionsFrom(schema, { page, perPage, skipTotal, ...rest });
|
opts: RecordFullListOpts<S>,
|
||||||
|
) {
|
||||||
|
const { page = 1, perPage = 200, skipTotal = true, ...rest } = opts
|
||||||
|
return listOptionsFrom(schema, { page, perPage, skipTotal, ...rest })
|
||||||
}
|
}
|
||||||
|
|
||||||
function expandFromRec<S extends ZodTypeAny>(schema: S, prefix = "") {
|
function expandFromRec<S>(schema: S, prefix = '') {
|
||||||
let expands: string[] = [];
|
let expands: string[] = []
|
||||||
const shape = getObjectSchemaDescendant(schema)?.shape;
|
const shape = getObjectSchemaDescendant(schema)?.shape
|
||||||
if (!shape || !("expand" in shape)) return [];
|
if (!shape || !('expand' in shape)) return []
|
||||||
for (const [key, value] of Object.entries(getObjectSchemaDescendant(shape.expand)!.shape)) {
|
for (const [key, value] of Object.entries(getObjectSchemaDescendant(shape.expand)!.shape)) {
|
||||||
expands = [...expands, `${prefix}${key}`];
|
expands = [...expands, `${prefix}${key}`]
|
||||||
if (hasObjectSchemaDescendant(value)) expands = [...expands, ...expandFromRec(getObjectSchemaDescendant(value)!, `${prefix}${key}.`)];
|
if (hasObjectSchemaDescendant(value))
|
||||||
}
|
expands = [
|
||||||
return expands;
|
...expands,
|
||||||
|
...expandFromRec(getObjectSchemaDescendant(value)!, `${prefix}${key}.`),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return expands
|
||||||
}
|
}
|
||||||
|
|
||||||
function fieldsFromRec<S extends z.ZodTypeAny>(schema: S, prefix = "") {
|
function fieldsFromRec<S>(schema: S, prefix = '') {
|
||||||
let fields: string[] = [];
|
let fields: string[] = []
|
||||||
const shape = getObjectSchemaDescendant(schema)?.shape;
|
const shape = getObjectSchemaDescendant(schema)?.shape
|
||||||
if (!shape) return [];
|
if (!shape) return []
|
||||||
for (const [key, value] of Object.entries(shape)) {
|
for (const [key, value] of Object.entries(shape)) {
|
||||||
fields = [
|
fields = [
|
||||||
...fields,
|
...fields,
|
||||||
...(hasObjectSchemaDescendant(value) ? fieldsFromRec(getObjectSchemaDescendant(value)!, `${prefix}${key}.`) : [`${prefix}${key}`]),
|
...(hasObjectSchemaDescendant(value)
|
||||||
];
|
? fieldsFromRec(getObjectSchemaDescendant(value)!, `${prefix}${key}.`)
|
||||||
}
|
: [`${prefix}${key}`]),
|
||||||
return fields.sort((k1, k2) => (k1 < k2 ? -1 : 1));
|
]
|
||||||
|
}
|
||||||
|
return fields.sort((k1, k2) => (k1 < k2 ? -1 : 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasObjectSchemaDescendant(value: unknown): value is z.ZodTypeAny {
|
function hasObjectSchemaDescendant(value: unknown): value is any {
|
||||||
if (value instanceof z.ZodEffects) return hasObjectSchemaDescendant(value.innerType());
|
// Handle ZodPipe
|
||||||
if (value instanceof z.ZodArray) return hasObjectSchemaDescendant(value.element);
|
if (value && typeof value === 'object' && 'constructor' in value) {
|
||||||
if (value instanceof z.ZodOptional) return hasObjectSchemaDescendant(value.unwrap());
|
const valueConstructor = value.constructor
|
||||||
return value instanceof z.ZodObject;
|
if (valueConstructor?.name === 'ZodPipe') {
|
||||||
|
const inputSchema = (value as any)._def?.in
|
||||||
|
if (inputSchema) return hasObjectSchemaDescendant(inputSchema)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle ZodTransform
|
||||||
|
if (value && typeof value === 'object' && 'constructor' in value) {
|
||||||
|
const valueConstructor = value.constructor
|
||||||
|
if (valueConstructor?.name === 'ZodTransform') {
|
||||||
|
const innerSchema =
|
||||||
|
(value as any)._def?.input || (value as any)._def?.schema || (value as any).sourceType
|
||||||
|
if (innerSchema) return hasObjectSchemaDescendant(innerSchema)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle ZodArray
|
||||||
|
if (value instanceof z.ZodArray) return hasObjectSchemaDescendant((value as any).element)
|
||||||
|
|
||||||
|
// Handle ZodOptional
|
||||||
|
if (value instanceof z.ZodOptional) return hasObjectSchemaDescendant((value as any).unwrap())
|
||||||
|
|
||||||
|
return value instanceof z.ZodObject
|
||||||
}
|
}
|
||||||
|
|
||||||
function getObjectSchemaDescendant<S extends z.ZodTypeAny>(schema: S): AnyZodObject | undefined {
|
function getObjectSchemaDescendant<S>(schema: S): ZodObject | undefined {
|
||||||
if (schema instanceof z.ZodEffects) return getObjectSchemaDescendant(schema.innerType());
|
// Handle ZodPipe
|
||||||
if (schema instanceof z.ZodArray) return getObjectSchemaDescendant(schema.element);
|
if (schema && typeof schema === 'object' && 'constructor' in schema) {
|
||||||
if (schema instanceof z.ZodOptional) return getObjectSchemaDescendant(schema.unwrap());
|
const schemaConstructor = schema.constructor
|
||||||
if (schema instanceof z.ZodObject) return schema;
|
if (schemaConstructor?.name === 'ZodPipe') {
|
||||||
return;
|
const inputSchema = (schema as any)._def?.in
|
||||||
|
if (inputSchema) return getObjectSchemaDescendant(inputSchema)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle ZodTransform
|
||||||
|
if (schema && typeof schema === 'object' && 'constructor' in schema) {
|
||||||
|
const schemaConstructor = schema.constructor
|
||||||
|
if (schemaConstructor?.name === 'ZodTransform') {
|
||||||
|
const innerSchema =
|
||||||
|
(schema as any)._def?.input || (schema as any)._def?.schema || (schema as any).sourceType
|
||||||
|
if (innerSchema) return getObjectSchemaDescendant(innerSchema)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle ZodArray
|
||||||
|
if (schema instanceof z.ZodArray) return getObjectSchemaDescendant((schema as any).element)
|
||||||
|
|
||||||
|
// Handle ZodOptional
|
||||||
|
if (schema instanceof z.ZodOptional) return getObjectSchemaDescendant((schema as any).unwrap())
|
||||||
|
|
||||||
|
// Handle ZodObject
|
||||||
|
if (schema instanceof z.ZodObject) return schema as ZodObject
|
||||||
|
|
||||||
|
return undefined
|
||||||
}
|
}
|
||||||
|
|||||||
102
src/schemas.ts
102
src/schemas.ts
@@ -1,16 +1,17 @@
|
|||||||
import { type AnyZodObject, type objectUtil, z, type ZodEffects, type ZodObject, ZodOptional, type ZodRawShape } from "zod";
|
import type { ZodObject, ZodOptional, ZodRawShape } from 'zod'
|
||||||
import type { AnyZodRecord, HasRequiredKeys, ZodRecordKeys } from "./types.ts";
|
import { z } from 'zod'
|
||||||
|
import type { HasRequiredKeys, ZodRecordKeys } from '@/types.ts'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Records list schema.
|
* Records list schema.
|
||||||
*/
|
*/
|
||||||
export const AnyRecordsList = z.object({
|
export const AnyRecordsList = z.object({
|
||||||
items: z.any().array(),
|
items: z.any().array(),
|
||||||
page: z.number().int().min(1),
|
page: z.int().min(1),
|
||||||
perPage: z.number().int().min(1),
|
perPage: z.int().min(1),
|
||||||
totalItems: z.number().int().min(-1),
|
totalItems: z.int().min(-1),
|
||||||
totalPages: z.number().int().min(-1),
|
totalPages: z.int().min(-1),
|
||||||
});
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extends the given schema with the given expansion.
|
* Extends the given schema with the given expansion.
|
||||||
@@ -18,11 +19,19 @@ export const AnyRecordsList = z.object({
|
|||||||
* @param shape - The shape of the expansion
|
* @param shape - The shape of the expansion
|
||||||
* @returns A new schema extended with the given expansion
|
* @returns A new schema extended with the given expansion
|
||||||
*/
|
*/
|
||||||
export function expand<S extends AnyZodObject, E extends ZodRawShape>(schema: S, shape: E) {
|
export function expand<S extends ZodObject<any, any>, E extends ZodRawShape>(schema: S, shape: E) {
|
||||||
const isExpandOptional = Object.entries(shape).every(([, value]) => value instanceof z.ZodOptional);
|
const isExpandOptional = Object.entries(shape).every(
|
||||||
return z
|
([, value]) => value instanceof z.ZodOptional,
|
||||||
.object({ ...schema.shape, expand: isExpandOptional ? z.object(shape).optional() : z.object(shape) })
|
)
|
||||||
.transform(({ expand, ...rest }) => ({ ...rest, ...(expand ?? {}) })) as ZodObjectExpand<S, E>;
|
return z
|
||||||
|
.object({
|
||||||
|
...schema.shape,
|
||||||
|
expand: isExpandOptional ? z.object(shape).optional() : z.object(shape),
|
||||||
|
})
|
||||||
|
.transform(({ expand, ...rest }) => ({
|
||||||
|
...rest,
|
||||||
|
...(expand ?? {}),
|
||||||
|
})) as ZodObjectExpand<S, E>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,8 +40,11 @@ export function expand<S extends AnyZodObject, E extends ZodRawShape>(schema: S,
|
|||||||
* @param keys - The keys to keep
|
* @param keys - The keys to keep
|
||||||
* @returns A new schema with only the given keys
|
* @returns A new schema with only the given keys
|
||||||
*/
|
*/
|
||||||
export function pick<S extends AnyZodObject, K extends ZodRecordKeys<S>[]>(schema: S, keys: K) {
|
export function pick<S extends ZodObject<any, any>, K extends ZodRecordKeys<S>[]>(
|
||||||
return schema.pick(Object.fromEntries(keys.map((key) => [key, true]))) as ZodObjectPick<S, K>;
|
schema: S,
|
||||||
|
keys: K,
|
||||||
|
) {
|
||||||
|
return schema.pick(Object.fromEntries(keys.map((key) => [key, true]))) as ZodObjectPick<S, K>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,30 +54,44 @@ export function pick<S extends AnyZodObject, K extends ZodRecordKeys<S>[]>(schem
|
|||||||
* @param shape - The shape of the expansion
|
* @param shape - The shape of the expansion
|
||||||
* @returns A new schema with only the given keys
|
* @returns A new schema with only the given keys
|
||||||
*/
|
*/
|
||||||
export function select<S extends AnyZodObject, K extends ZodRecordKeys<S>[], E extends ZodRawShape>(
|
export function select<
|
||||||
schema: S,
|
S extends ZodObject<any, any>,
|
||||||
keys: K,
|
K extends ZodRecordKeys<S>[],
|
||||||
shape: E,
|
E extends ZodRawShape,
|
||||||
): ZodObjectExpand<ZodObjectPick<S, K>, E>;
|
>(schema: S, keys: K, shape: E): ZodObjectExpand<ZodObjectPick<S, K>, E>
|
||||||
export function select<S extends AnyZodObject, K extends ZodRecordKeys<S>[]>(schema: S, keys: K): ZodObjectPick<S, K>;
|
export function select<S extends ZodObject<any, any>, K extends ZodRecordKeys<S>[]>(
|
||||||
export function select<S extends AnyZodObject, K extends ZodRecordKeys<S>[], E extends ZodRawShape | undefined>(
|
schema: S,
|
||||||
schema: S,
|
keys: K,
|
||||||
keys: K,
|
): ZodObjectPick<S, K>
|
||||||
shape?: E,
|
export function select<
|
||||||
) {
|
S extends ZodObject<any, any>,
|
||||||
return shape ? expand(pick(schema, keys), shape) : pick(schema, keys);
|
K extends ZodRecordKeys<S>[],
|
||||||
|
E extends ZodRawShape | undefined,
|
||||||
|
>(schema: S, keys: K, shape?: E) {
|
||||||
|
return shape ? expand(pick(schema, keys), shape) : pick(schema, keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ZodObjectExpand<S extends AnyZodObject, E extends ZodRawShape> =
|
export type ZodObjectExpand<
|
||||||
S extends ZodObject<infer T, infer U, infer C>
|
S extends ZodObject<any, any>,
|
||||||
? ZodEffects<
|
E extends ZodRawShape,
|
||||||
ZodObject<objectUtil.extendShape<T, { expand: HasRequiredKeys<E> extends true ? ZodObject<E> : ZodOptional<ZodObject<E>> }>, U, C>,
|
> = S extends ZodObject<infer T, infer U>
|
||||||
ZodObject<objectUtil.extendShape<T, E>, U, C>["_output"]
|
? z.ZodPipe<
|
||||||
>
|
ZodObject<
|
||||||
: never;
|
T & {
|
||||||
|
expand: HasRequiredKeys<E> extends true ? ZodObject<E> : ZodOptional<ZodObject<E>>
|
||||||
|
},
|
||||||
|
U
|
||||||
|
>,
|
||||||
|
z.ZodTransform<T & E>
|
||||||
|
>
|
||||||
|
: never
|
||||||
|
|
||||||
export type ZodObjectPick<S extends AnyZodObject, K extends ZodRecordKeys<S>[]> =
|
export type ZodObjectPick<
|
||||||
S extends ZodObject<infer T, infer U, infer C> ? ZodObject<Pick<T, K[number]>, U, C> : never;
|
S extends ZodObject<any, any>,
|
||||||
|
K extends ZodRecordKeys<S>[],
|
||||||
|
> = S extends ZodObject<infer T, infer U> ? ZodObject<Pick<T, K[number]>, U> : never
|
||||||
|
|
||||||
export type AnyRecordsList = z.infer<typeof AnyRecordsList>;
|
export type AnyRecordsList = z.infer<typeof AnyRecordsList>
|
||||||
export type RecordsList<S extends AnyZodRecord> = Omit<AnyRecordsList, "items"> & { items: S["_output"][] };
|
export type RecordsList<S extends ZodObject<any, any>> = Omit<AnyRecordsList, 'items'> & {
|
||||||
|
items: S['_output'][]
|
||||||
|
}
|
||||||
|
|||||||
18
src/sdk.ts
18
src/sdk.ts
@@ -1,13 +1,13 @@
|
|||||||
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
|
||||||
|
|
||||||
export async function getPocketbase({ adminEmail, adminPassword, url }: Credentials) {
|
export async function getPocketbase({ adminEmail, adminPassword, url }: Credentials) {
|
||||||
if (!adminPocketbase) {
|
if (!adminPocketbase) {
|
||||||
const pocketbase = new Pocketbase(url);
|
const pocketbase = new Pocketbase(url)
|
||||||
await pocketbase.collection("_superusers").authWithPassword(adminEmail, adminPassword);
|
await pocketbase.collection('_superusers').authWithPassword(adminEmail, adminPassword)
|
||||||
adminPocketbase = pocketbase;
|
adminPocketbase = pocketbase
|
||||||
}
|
}
|
||||||
return adminPocketbase;
|
return adminPocketbase
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,91 +1,125 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
import { Config, type ResolvedConfig } from "../config.ts";
|
import { existsSync } from 'node:fs'
|
||||||
import pkg from "../../package.json" with { type: "json" };
|
import {
|
||||||
import { loadConfig } from "c12";
|
cancel,
|
||||||
import { defineCommand, runMain } from "citty";
|
confirm,
|
||||||
import { cancel, group, intro, log, outro, confirm, text, spinner, multiselect, isCancel } from "@clack/prompts";
|
group,
|
||||||
import { fetchCollections } from "../utils.ts";
|
intro,
|
||||||
import { generate } from "./utils.ts";
|
isCancel,
|
||||||
import { existsSync } from "node:fs";
|
log,
|
||||||
|
multiselect,
|
||||||
|
outro,
|
||||||
|
spinner,
|
||||||
|
text,
|
||||||
|
} from '@clack/prompts'
|
||||||
|
import { loadConfig } from 'c12'
|
||||||
|
import { defineCommand, runMain } from 'citty'
|
||||||
|
import type { ResolvedConfig } from '@/config.ts'
|
||||||
|
import { Config } from '@/config.ts'
|
||||||
|
import { generate } from '@/server/utils.ts'
|
||||||
|
import { fetchCollections } from '@/utils.ts'
|
||||||
|
import pkg from '%/package.json' with { type: 'json' }
|
||||||
|
|
||||||
async function getConfig() {
|
async function getConfig() {
|
||||||
const { config } = await loadConfig({ name: "zod-pocketbase", rcFile: false, dotenv: true });
|
const { config } = await loadConfig({
|
||||||
const { ZOD_POCKETBASE_ADMIN_EMAIL: adminEmail, ZOD_POCKETBASE_ADMIN_PASSWORD: adminPassword, ZOD_POCKETBASE_URL: url } = process.env;
|
name: 'zod-pocketbase',
|
||||||
const result = Config.safeParse({ ...config, adminEmail, adminPassword, url });
|
rcFile: false,
|
||||||
if (!result.success) {
|
dotenv: true,
|
||||||
log.error("Invalid fields in your config file.");
|
})
|
||||||
onCancel();
|
const {
|
||||||
}
|
ZOD_POCKETBASE_ADMIN_EMAIL: adminEmail,
|
||||||
return result.data!;
|
ZOD_POCKETBASE_ADMIN_PASSWORD: adminPassword,
|
||||||
|
ZOD_POCKETBASE_URL: url,
|
||||||
|
} = process.env
|
||||||
|
const result = Config.safeParse({ ...config, adminEmail, adminPassword, url })
|
||||||
|
if (!result.success) {
|
||||||
|
log.error('Invalid fields in your config file.')
|
||||||
|
onCancel()
|
||||||
|
}
|
||||||
|
return result.data!
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCancel() {
|
function onCancel() {
|
||||||
cancel("Operation cancelled.");
|
cancel('Operation cancelled.')
|
||||||
process.exit(0);
|
process.exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectCollections(config: ResolvedConfig) {
|
async function selectCollections(config: ResolvedConfig) {
|
||||||
const credentialPrompts = {
|
const credentialPrompts = {
|
||||||
url: () => text({ message: "What is the url of your pocketbase instance?", initialValue: config.url ?? "" }),
|
url: () =>
|
||||||
adminEmail: () => text({ message: "What is your admin's email?", initialValue: config.adminEmail ?? "" }),
|
text({
|
||||||
adminPassword: () => text({ message: "What is your admin's password?", initialValue: config.adminPassword ?? "" }),
|
message: 'What is the url of your pocketbase instance?',
|
||||||
};
|
initialValue: config.url ?? '',
|
||||||
const credentials = await group(credentialPrompts, { onCancel });
|
}),
|
||||||
const s = spinner();
|
adminEmail: () =>
|
||||||
s.start("Fetching collections...");
|
text({ message: "What is your admin's email?", initialValue: config.adminEmail ?? '' }),
|
||||||
try {
|
adminPassword: () =>
|
||||||
const allCollections = await fetchCollections(credentials);
|
text({ message: "What is your admin's password?", initialValue: config.adminPassword ?? '' }),
|
||||||
s.stop("Successfully fetched collections.");
|
}
|
||||||
const collectionNames = await multiselect({
|
const credentials = await group(credentialPrompts, { onCancel })
|
||||||
message: "What collections do you want to generate schemas for?",
|
const s = spinner()
|
||||||
options: allCollections.map(({ name: value }) => ({ value })),
|
s.start('Fetching collections...')
|
||||||
initialValues: allCollections.filter(({ name }) => !config.ignore.includes(name)).map(({ name }) => name),
|
try {
|
||||||
});
|
const allCollections = await fetchCollections(credentials)
|
||||||
if (isCancel(collectionNames)) onCancel();
|
s.stop('Successfully fetched collections.')
|
||||||
return allCollections.filter(({ name }) => (collectionNames as string[]).includes(name));
|
const collectionNames = await multiselect({
|
||||||
} catch {
|
message: 'What collections do you want to generate schemas for?',
|
||||||
s.stop("Failed to fetch collections.Please check your credentials and try again.");
|
options: allCollections.map(({ name: value }) => ({ value })),
|
||||||
return selectCollections(config);
|
initialValues: allCollections
|
||||||
}
|
.filter(({ name }) => !config.ignore.includes(name))
|
||||||
|
.map(({ name }) => name),
|
||||||
|
})
|
||||||
|
if (isCancel(collectionNames)) onCancel()
|
||||||
|
return allCollections.filter(({ name }) => (collectionNames as string[]).includes(name))
|
||||||
|
} catch {
|
||||||
|
s.stop('Failed to fetch collections.Please check your credentials and try again.')
|
||||||
|
return selectCollections(config)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setGeneratedFilePath(config: ResolvedConfig) {
|
async function setGeneratedFilePath(config: ResolvedConfig) {
|
||||||
const output = await text({
|
const output = await text({
|
||||||
message: "What is the generated file path?",
|
message: 'What is the generated file path?',
|
||||||
initialValue: config.output,
|
initialValue: config.output,
|
||||||
validate: (value) => {
|
validate: (value) => {
|
||||||
if (!value) return "Please enter a path.";
|
if (!value) return 'Please enter a path.'
|
||||||
if (value[0] !== ".") return "Please enter a relative path.";
|
if (value[0] !== '.') return 'Please enter a relative path.'
|
||||||
return;
|
return
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
if (isCancel(output)) onCancel();
|
if (isCancel(output)) onCancel()
|
||||||
|
|
||||||
if (existsSync(output as string)) {
|
if (existsSync(output as string)) {
|
||||||
const confirmed = await confirm({ message: "The file already exists, would you like to overwrite it?" });
|
const confirmed = await confirm({
|
||||||
if (isCancel(confirmed)) onCancel();
|
message: 'The file already exists, would you like to overwrite it?',
|
||||||
if (!confirmed) return setGeneratedFilePath(config);
|
})
|
||||||
}
|
if (isCancel(confirmed)) onCancel()
|
||||||
|
if (!confirmed) return setGeneratedFilePath(config)
|
||||||
|
}
|
||||||
|
|
||||||
return output as string;
|
return output as string
|
||||||
}
|
}
|
||||||
|
|
||||||
const main = defineCommand({
|
const main = defineCommand({
|
||||||
meta: { name: "zod-pocketbase", version: pkg.version, description: "Generate Zod schemas for your pocketbase instance." },
|
meta: {
|
||||||
run: async () => {
|
name: 'zod-pocketbase-continue',
|
||||||
intro(`ZOD POCKETBASE`);
|
version: pkg.version,
|
||||||
const config = await getConfig();
|
description: 'Generate Zod schemas for your pocketbase instance.',
|
||||||
const collections = await selectCollections(config);
|
},
|
||||||
const output = await setGeneratedFilePath(config);
|
run: async () => {
|
||||||
|
intro(`ZOD POCKETBASE`)
|
||||||
|
const config = await getConfig()
|
||||||
|
const collections = await selectCollections(config)
|
||||||
|
const output = await setGeneratedFilePath(config)
|
||||||
|
|
||||||
const s = spinner();
|
const s = spinner()
|
||||||
s.start("Generating your schemas...");
|
s.start('Generating your schemas...')
|
||||||
await generate(collections, { ...config, output });
|
await generate(collections, { ...config, output })
|
||||||
s.stop("Schemas successfully generated.");
|
s.stop('Schemas successfully generated.')
|
||||||
|
|
||||||
outro("Operation completed.");
|
outro('Operation completed.')
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
runMain(main);
|
runMain(main)
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export * from "./utils.js";
|
export * from '@/server/utils.js'
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
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 type { ResolvedConfig } from '@/config.ts'
|
||||||
import type { ResolvedConfig } from "../config.ts";
|
import { stringifyContent } from '@/content.js'
|
||||||
|
|
||||||
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(
|
||||||
const { collectionNames, enums, records, services } = stringifyContent(collections, opts);
|
resolve(dirname(fileURLToPath(import.meta.url)), '../../assets/stubs/index.ts'),
|
||||||
const content = stub
|
'utf-8',
|
||||||
.replace("@@_COLLECTION_NAMES_@@", collectionNames)
|
)
|
||||||
.replace("@@_ENUMS_@@", enums)
|
const { collectionNames, enums, records, services } = stringifyContent(collections, opts)
|
||||||
.replace("@@_RECORDS_@@", records)
|
const content = stub
|
||||||
.replace("@@_SERVICES_@@", services);
|
.replace('@@_COLLECTION_NAMES_@@', collectionNames)
|
||||||
mkdirSync(dirname(opts.output), { recursive: true });
|
.replace('@@_ENUMS_@@', enums)
|
||||||
writeFileSync(opts.output, content);
|
.replace('@@_RECORDS_@@', records)
|
||||||
|
.replace('@@_SERVICES_@@', services)
|
||||||
|
mkdirSync(dirname(opts.output), { recursive: true })
|
||||||
|
writeFileSync(opts.output, content)
|
||||||
}
|
}
|
||||||
export type GenerateOpts = Omit<ResolvedConfig, "adminEmail" | "adminPassword" | "ignore" | "url">;
|
export type GenerateOpts = Omit<ResolvedConfig, 'adminEmail' | 'adminPassword' | 'ignore' | 'url'>
|
||||||
|
|||||||
48
src/types.ts
48
src/types.ts
@@ -1,31 +1,33 @@
|
|||||||
import type { AnyZodObject, ZodEffects, ZodOptional, ZodRawShape, ZodTypeAny } from "zod";
|
import type { ZodObject, ZodOptional, ZodRawShape, ZodType } from 'zod'
|
||||||
|
|
||||||
export type AnyZodRecord = AnyZodObject | ZodEffects<AnyZodObject>;
|
export type RecordFullListOpts<S extends ZodObject<any, any>> = RecordListOpts<S> & {
|
||||||
|
batch?: number
|
||||||
|
}
|
||||||
|
|
||||||
export type RecordFullListOpts<S extends AnyZodRecord> = RecordListOpts<S> & { batch?: number };
|
export type RecordListOpts<S extends ZodObject<any, any>> = {
|
||||||
|
filter?: string
|
||||||
|
page?: number
|
||||||
|
perPage?: number
|
||||||
|
skipTotal?: boolean
|
||||||
|
sort?: ZodRecordSort<S>
|
||||||
|
}
|
||||||
|
|
||||||
export type RecordListOpts<S extends AnyZodRecord> = {
|
export type RecordIdRef<C extends string> = { collection: C; id: string }
|
||||||
filter?: string;
|
export type RecordSlugRef<C extends string> = { collection: C; slug: string }
|
||||||
page?: number;
|
export type RecordRef<C extends string> = RecordIdRef<C> | RecordSlugRef<C>
|
||||||
perPage?: number;
|
|
||||||
skipTotal?: boolean;
|
|
||||||
sort?: ZodRecordSort<S>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RecordIdRef<C extends string> = { collection: C; id: string };
|
export type ZodRecordKeys<S extends ZodObject<any, any>> = Extract<keyof S['_input'], string>
|
||||||
export type RecordSlugRef<C extends string> = { collection: C; slug: string };
|
|
||||||
export type RecordRef<C extends string> = RecordIdRef<C> | RecordSlugRef<C>;
|
|
||||||
|
|
||||||
export type ZodRecordKeys<S extends AnyZodRecord> = Extract<keyof S["_input"], string>;
|
export type ZodRecordMainKeys<S extends ZodObject<any, any>> = Exclude<ZodRecordKeys<S>, 'expand'>
|
||||||
|
|
||||||
export type ZodRecordMainKeys<S extends AnyZodRecord> = Exclude<ZodRecordKeys<S>, "expand">;
|
export type ZodRecordSort<S extends ZodObject<any, any>> =
|
||||||
|
| `${'+' | '-'}${ZodRecordMainKeys<S>}`
|
||||||
export type ZodRecordSort<S extends AnyZodRecord> = `${"+" | "-"}${ZodRecordMainKeys<S>}` | "@random";
|
| '@random'
|
||||||
|
|
||||||
type RequiredKeysOf<S extends ZodRawShape> = Exclude<
|
type RequiredKeysOf<S extends ZodRawShape> = Exclude<
|
||||||
{
|
{
|
||||||
[Key in keyof S]: S[Key] extends ZodOptional<ZodTypeAny> ? never : Key;
|
[Key in keyof S]: S[Key] extends ZodOptional<ZodType> ? never : Key
|
||||||
}[keyof S],
|
}[keyof S],
|
||||||
undefined
|
undefined
|
||||||
>;
|
>
|
||||||
export type HasRequiredKeys<S extends ZodRawShape> = RequiredKeysOf<S> extends never ? false : true;
|
export type HasRequiredKeys<S extends ZodRawShape> = RequiredKeysOf<S> extends never ? false : true
|
||||||
|
|||||||
14
src/utils.ts
14
src/utils.ts
@@ -1,10 +1,10 @@
|
|||||||
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 type { Credentials } from '@/config.ts'
|
||||||
import type { Credentials } from "./config.ts";
|
import { getPocketbase } from '@/sdk.js'
|
||||||
|
|
||||||
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)
|
||||||
const collections = await pocketbase.collections.getFullList();
|
const collections = await pocketbase.collections.getFullList()
|
||||||
return sortBy(collections, ["name"]);
|
return sortBy(collections, ['name'])
|
||||||
}
|
}
|
||||||
|
|||||||
105
tsconfig.json
105
tsconfig.json
@@ -1,52 +1,57 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/tsconfig",
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// Enable top-level await, and other modern ESM features.
|
// Enable top-level await, and other modern ESM features.
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"module": "NodeNext",
|
"module": "NodeNext",
|
||||||
// Enable module resolution without file extensions on relative paths, for things like npm package imports.
|
// Enable module resolution without file extensions on relative paths, for things like npm package imports.
|
||||||
"moduleResolution": "Node16",
|
"moduleResolution": "nodenext",
|
||||||
// Allow importing TypeScript files using their native extension (.ts(x)).
|
// Allow importing TypeScript files using their native extension (.ts(x)).
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
// Enable JSON imports.
|
// Enable JSON imports.
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
// Enforce the usage of type-only imports when needed, which helps avoiding bundling issues.
|
// Enforce the usage of type-only imports when needed, which helps avoiding bundling issues.
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true,
|
||||||
// Ensure that each file can be transpiled without relying on other imports.
|
// Ensure that each file can be transpiled without relying on other imports.
|
||||||
// This is redundant with the previous option, however it ensures that it's on even if someone disable `verbatimModuleSyntax`
|
// This is redundant with the previous option, however it ensures that it's on even if someone disable `verbatimModuleSyntax`
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
// Astro directly run TypeScript code, no transpilation needed.
|
// Astro directly run TypeScript code, no transpilation needed.
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
// Report an error when importing a file using a casing different from another import of the same file.
|
// Report an error when importing a file using a casing different from another import of the same file.
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
// Properly support importing CJS modules in ESM
|
// Properly support importing CJS modules in ESM
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
// Skip typechecking libraries and .d.ts files
|
// Skip typechecking libraries and .d.ts files
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
// Allow JavaScript files to be imported
|
// Allow JavaScript files to be imported
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
// Allow JSX files (or files that are internally considered JSX, like Astro files) to be imported inside `.js` and `.ts` files.
|
// Allow JSX files (or files that are internally considered JSX, like Astro files) to be imported inside `.js` and `.ts` files.
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
// Enable strict mode. This enables a few options at a time, see https://www.typescriptlang.org/tsconfig#strict for a list.
|
// Enable strict mode. This enables a few options at a time, see https://www.typescriptlang.org/tsconfig#strict for a list.
|
||||||
"strict": true,
|
"strict": true,
|
||||||
// Report errors for fallthrough cases in switch statements
|
// Report errors for fallthrough cases in switch statements
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
// Force functions designed to override their parent class to be specified as `override`.
|
// Force functions designed to override their parent class to be specified as `override`.
|
||||||
"noImplicitOverride": true,
|
"noImplicitOverride": true,
|
||||||
// Force functions to specify that they can return `undefined` if a possible code path does not return a value.
|
// Force functions to specify that they can return `undefined` if a possible code path does not return a value.
|
||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
// Report an error when a variable is declared but never used.
|
// Report an error when a variable is declared but never used.
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
// Report an error when a parameter is declared but never used.
|
// Report an error when a parameter is declared but never used.
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
// Force the usage of the indexed syntax to access fields declared using an index signature.
|
// Force the usage of the indexed syntax to access fields declared using an index signature.
|
||||||
"noUncheckedIndexedAccess": true,
|
"noUncheckedIndexedAccess": true,
|
||||||
// Report an error when the value `undefined` is given to an optional property that doesn't specify `undefined` as a valid value.
|
// Report an error when the value `undefined` is given to an optional property that doesn't specify `undefined` as a valid value.
|
||||||
"exactOptionalPropertyTypes": true,
|
"exactOptionalPropertyTypes": true,
|
||||||
// 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": ".",
|
||||||
"exclude": ["dist"]
|
"paths": {
|
||||||
|
"@/*": ["./src/*"],
|
||||||
|
"%/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": ["dist", "assets/stubs"]
|
||||||
}
|
}
|
||||||
|
|||||||
19
tsdown.config.ts
Normal file
19
tsdown.config.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { defineConfig } from 'tsdown'
|
||||||
|
import packageJson from '%/package.json' with { type: 'json' }
|
||||||
|
|
||||||
|
export default defineConfig((options) => {
|
||||||
|
const dev = !!options.watch
|
||||||
|
return {
|
||||||
|
entry: ['src/**/*.(ts|js)'],
|
||||||
|
format: ['esm'],
|
||||||
|
target: 'node24',
|
||||||
|
unbundle: true,
|
||||||
|
dts: true,
|
||||||
|
sourcemap: true,
|
||||||
|
clean: true,
|
||||||
|
splitting: false,
|
||||||
|
minify: !dev,
|
||||||
|
external: [...Object.keys(packageJson.peerDependencies), 'dotenv'],
|
||||||
|
tsconfig: 'tsconfig.json',
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { defineConfig } from "tsup";
|
|
||||||
import { peerDependencies } from "./package.json";
|
|
||||||
|
|
||||||
export default defineConfig((options) => {
|
|
||||||
const dev = !!options.watch;
|
|
||||||
return {
|
|
||||||
entry: ["src/**/*.(ts|js)"],
|
|
||||||
format: ["esm"],
|
|
||||||
target: "node24",
|
|
||||||
bundle: true,
|
|
||||||
dts: true,
|
|
||||||
sourcemap: true,
|
|
||||||
clean: true,
|
|
||||||
splitting: false,
|
|
||||||
minify: !dev,
|
|
||||||
external: [...Object.keys(peerDependencies), "dotenv"],
|
|
||||||
tsconfig: "tsconfig.json",
|
|
||||||
};
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user