Compare commits
	
		
			14 Commits
		
	
	
		
			ea672a9d91
			...
			v0.6.1
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| dc0ba32801 | |||
| 4d1c06c5be | |||
| 8b1e9d1cf6 | |||
| 2412238236 | |||
| 6c88dd79d8 | |||
| 1638435e26 | |||
| 618d9591b4 | |||
| b2e9e82f5e | |||
| 81005c54cd | |||
| e02737359c | |||
| 827b53e630 | |||
| d4dfed9603 | |||
| 21c9fccf5e | |||
| e5e728705c | 
| @@ -1,10 +1,10 @@ | ||||
| { | ||||
|   "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", | ||||
|   "changelog": "@changesets/cli/changelog", | ||||
|   "commit": false, | ||||
|   "fixed": [], | ||||
|   "linked": [], | ||||
|   "access": "public", | ||||
|   "baseBranch": "main", | ||||
|   "updateInternalDependencies": "patch" | ||||
| 	"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", | ||||
| 	"changelog": "@changesets/cli/changelog", | ||||
| 	"commit": false, | ||||
| 	"fixed": [], | ||||
| 	"linked": [], | ||||
| 	"access": "public", | ||||
| 	"baseBranch": "main", | ||||
| 	"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({ | ||||
|   ...BaseModel.shape, | ||||
|   avatar: z.string(), | ||||
|   email: z.string().email(), | ||||
|   email: z.email(), | ||||
| }); | ||||
| export type AdminModel = z.infer<typeof AdminModel>; | ||||
|  | ||||
| @@ -33,6 +33,41 @@ export const RecordModel = z.object({ | ||||
| }); | ||||
| 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_@@ | ||||
|  | ||||
|   | ||||
							
								
								
									
										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,22 +0,0 @@ | ||||
| import typescript from "@typescript-eslint/eslint-plugin"; | ||||
| import typescriptParser from "@typescript-eslint/parser"; | ||||
|  | ||||
| export default [ | ||||
|   { | ||||
|     files: ["**/*.ts", "**/*.tsx"], | ||||
|     languageOptions: { | ||||
|       parser: typescriptParser, | ||||
|       parserOptions: { | ||||
|         ecmaVersion: "latest", | ||||
|         sourceType: "module", | ||||
|         project: "./tsconfig.json", | ||||
|       }, | ||||
|     }, | ||||
|     plugins: { | ||||
|       "@typescript-eslint": typescript, | ||||
|     }, | ||||
|     rules: { | ||||
|       ...typescript.configs.recommended.rules, | ||||
|     }, | ||||
|   }, | ||||
| ]; | ||||
							
								
								
									
										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 = { | ||||
|     nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; | ||||
| @@ -17,7 +17,6 @@ | ||||
|       let | ||||
|         pkgs = nixpkgs.legacyPackages.${system}; | ||||
|         bun = pkgs.bun; | ||||
|  | ||||
|       in | ||||
|       { | ||||
|         devShells.default = pkgs.mkShell { | ||||
| @@ -26,19 +25,22 @@ | ||||
|             git | ||||
|             nodejs_24 | ||||
|             tea | ||||
|             # nodePackages.node-inspector | ||||
|           ]; | ||||
|  | ||||
|           shellHook = '' | ||||
|             echo "🚀 Zod-PocketBase development environment (Bun-powered)" | ||||
|             echo "🚀 Zod-PocketBase-Continue development environment (Bun-powered)" | ||||
|             echo "Bun version: $(bun --version)" | ||||
|             echo "Tea version: $(tea --version)" | ||||
|             echo "" | ||||
|             echo "Available commands:" | ||||
|             echo "  bun install           - Install dependencies" | ||||
|             echo "  bun run dev           - Start development" | ||||
|             echo "  bun build             - Build with Bun's bundler" | ||||
|             echo "  tea releases create   - Create Gitea release" | ||||
|             echo "  bun run dev           - Start development (build with --watch flag)" | ||||
|             echo "  bun run build         - Build library" | ||||
|             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 "" | ||||
|  | ||||
|             if [ ! -d "node_modules" ]; then | ||||
| @@ -50,25 +52,6 @@ | ||||
|           NODE_ENV = "development"; | ||||
|           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", | ||||
|   "version": "0.5.0", | ||||
|   "description": "", | ||||
|   "author": { | ||||
|     "email": "garandplg@garandplg.com", | ||||
|     "name": "Garand_PLG", | ||||
|     "url": "https://gitea.garandplg.com" | ||||
|   }, | ||||
|   "license": "MIT", | ||||
|   "keywords": [ | ||||
|     "pocketbase", | ||||
|     "schemas", | ||||
|     "typescript", | ||||
|     "typegen", | ||||
|     "type generation", | ||||
|     "zod" | ||||
|   ], | ||||
|   "homepage": "https://gitea.garandplg.com/GarandPLG/zod-pocketbase", | ||||
|   "publishConfig": { | ||||
|     "access": "public" | ||||
|   }, | ||||
|   "type": "module", | ||||
|   "sideEffects": false, | ||||
|   "packageManager": "bun@1.2.23", | ||||
|   "engines": { | ||||
|     "node": ">=24.9.0" | ||||
|   }, | ||||
|   "main": "dist/index.js", | ||||
|   "bin": { | ||||
|     "zod-pocketbase": "dist/server/cli.js" | ||||
|   }, | ||||
|   "exports": { | ||||
|     ".": { | ||||
|       "types": "./dist/index.d.ts", | ||||
|       "default": "./dist/index.js" | ||||
|     }, | ||||
|     "./server": { | ||||
|       "types": "./dist/server/index.d.ts", | ||||
|       "default": "./dist/server/index.js" | ||||
|     } | ||||
|   }, | ||||
|   "files": [ | ||||
|     "dist", | ||||
|     "assets" | ||||
|   ], | ||||
|   "scripts": { | ||||
|     "dev": "tsup --watch", | ||||
|     "build": "tsup", | ||||
|     "changeset": "changeset", | ||||
|     "release": "bun scripts/release.mjs" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@clack/prompts": "^0.11.0", | ||||
|     "c12": "^3.3.0", | ||||
|     "citty": "^0.1.6", | ||||
|     "es-toolkit": "^1.39.10" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@changesets/cli": "^2.29.7", | ||||
|     "@types/node": "^24.6.2", | ||||
|     "@typescript-eslint/parser": "^8.45.0", | ||||
|     "eslint": "^9.37.0", | ||||
|     "eslint-plugin-astro": "^1.3.1", | ||||
|     "eslint-plugin-jsx-a11y": "^6.10.2", | ||||
|     "pocketbase": "^0.26.2", | ||||
|     "prettier": "^3.6.2", | ||||
|     "prettier-plugin-astro": "^0.14.1", | ||||
|     "prettier-plugin-tailwindcss": "^0.6.14", | ||||
|     "tsup": "^8.5.0", | ||||
|     "zod": "^3.25.76" | ||||
|   }, | ||||
|   "peerDependencies": { | ||||
|     "pocketbase": "^0.26.2", | ||||
|     "zod": "^3.25.76" | ||||
|   } | ||||
| 	"name": "zod-pocketbase-continue", | ||||
| 	"version": "0.6.1", | ||||
| 	"description": "Zod tooling for your PocketBase instance.", | ||||
| 	"author": { | ||||
| 		"email": "garandplg@garandplg.com", | ||||
| 		"name": "Garand_PLG", | ||||
| 		"url": "https://gitea.garandplg.com" | ||||
| 	}, | ||||
| 	"license": "MIT", | ||||
| 	"keywords": ["pocketbase", "schemas", "typescript", "typegen", "type generation", "zod"], | ||||
| 	"homepage": "https://gitea.garandplg.com/GarandPLG/zod-pocketbase-continue", | ||||
| 	"publishConfig": { | ||||
| 		"access": "public" | ||||
| 	}, | ||||
| 	"type": "module", | ||||
| 	"sideEffects": false, | ||||
| 	"packageManager": "bun@1.2.23", | ||||
| 	"engines": { | ||||
| 		"node": ">=24.9.0" | ||||
| 	}, | ||||
| 	"main": "dist/index.js", | ||||
| 	"bin": { | ||||
| 		"zod-pocketbase-continue": "dist/server/cli.js" | ||||
| 	}, | ||||
| 	"exports": { | ||||
| 		".": { | ||||
| 			"types": "./dist/index.d.ts", | ||||
| 			"default": "./dist/index.js" | ||||
| 		}, | ||||
| 		"./server": { | ||||
| 			"types": "./dist/server/index.d.ts", | ||||
| 			"default": "./dist/server/index.js" | ||||
| 		} | ||||
| 	}, | ||||
| 	"files": ["dist", "assets"], | ||||
| 	"scripts": { | ||||
| 		"dev": "tsdown --watch", | ||||
| 		"build": "tsdown", | ||||
| 		"changeset": "changeset", | ||||
| 		"release": "bun scripts/release.mjs", | ||||
| 		"lint": "biome check .", | ||||
| 		"lint:w": "biome check --write .", | ||||
| 		"format": "biome format .", | ||||
| 		"format:w": "biome format --write ." | ||||
| 	}, | ||||
| 	"dependencies": { | ||||
| 		"@clack/prompts": "^0.11.0", | ||||
| 		"c12": "^3.3.1", | ||||
| 		"citty": "^0.1.6", | ||||
| 		"es-toolkit": "^1.40.0" | ||||
| 	}, | ||||
| 	"devDependencies": { | ||||
| 		"@biomejs/biome": "2.2.6", | ||||
| 		"@changesets/cli": "^2.29.7", | ||||
| 		"@types/node": "^24.8.1", | ||||
| 		"pocketbase": "^0.26.2", | ||||
| 		"tsdown": "^0.15.8", | ||||
| 		"zod": "^4.1.12" | ||||
| 	}, | ||||
| 	"peerDependencies": { | ||||
| 		"pocketbase": "^0.26.2", | ||||
| 		"zod": "^4.1.12" | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -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 { resolve } from "node:path"; | ||||
| import { spawn } from 'node:child_process' | ||||
| import { resolve } from 'node:path' | ||||
|  | ||||
| /** | ||||
|  * | ||||
| @@ -9,41 +9,41 @@ import { resolve } from "node:path"; | ||||
|  * @returns {Promise<string>} | ||||
|  */ | ||||
| const run = async (command, ...args) => { | ||||
|   const cwd = resolve(); | ||||
|   return new Promise((resolve) => { | ||||
|     const cmd = spawn(command, args, { | ||||
|       stdio: ["inherit", "pipe", "pipe"], // Inherit stdin, pipe stdout, pipe stderr | ||||
|       shell: true, | ||||
|       cwd, | ||||
|     }); | ||||
| 	const cwd = resolve() | ||||
| 	return new Promise((resolve) => { | ||||
| 		const cmd = spawn(command, args, { | ||||
| 			stdio: ['inherit', 'pipe', 'pipe'], // Inherit stdin, pipe stdout, pipe stderr | ||||
| 			shell: true, | ||||
| 			cwd, | ||||
| 		}) | ||||
|  | ||||
|     let output = ""; | ||||
| 		let output = '' | ||||
|  | ||||
|     cmd.stdout.on("data", (data) => { | ||||
|       process.stdout.write(data.toString()); | ||||
|       output += data.toString(); | ||||
|     }); | ||||
| 		cmd.stdout.on('data', (data) => { | ||||
| 			process.stdout.write(data.toString()) | ||||
| 			output += data.toString() | ||||
| 		}) | ||||
|  | ||||
|     cmd.stderr.on("data", (data) => { | ||||
|       process.stderr.write(data.toString()); | ||||
|     }); | ||||
| 		cmd.stderr.on('data', (data) => { | ||||
| 			process.stderr.write(data.toString()) | ||||
| 		}) | ||||
|  | ||||
|     cmd.on("close", () => { | ||||
|       resolve(output); | ||||
|     }); | ||||
|   }); | ||||
| }; | ||||
| 		cmd.on('close', () => { | ||||
| 			resolve(output) | ||||
| 		}) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| const main = async () => { | ||||
|   await run("bun changeset version"); | ||||
|   await run("git add ."); | ||||
|   await run('git commit -m "chore: update version"'); | ||||
|   await run("git push"); | ||||
|   await run("bun run build"); | ||||
|   await run("bun changeset publish"); | ||||
|   await run("git push --follow-tags"); | ||||
|   const tag = (await run("git describe --abbrev=0")).replace("\n", ""); | ||||
|   await run(`tea releases create --tag ${tag} --title "${tag}"`); | ||||
| }; | ||||
| 	await run('bun changeset version') | ||||
| 	await run('git add .') | ||||
| 	await run('git commit -m "chore: update version"') | ||||
| 	await run('git push') | ||||
| 	await run('bun run build') | ||||
| 	await run('bun changeset publish') | ||||
| 	await run('git push --follow-tags') | ||||
| 	const tag = (await run('git describe --abbrev=0')).replace('\n', '') | ||||
| 	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 { z } from "zod"; | ||||
| import { pascalCase, snakeCase } from 'es-toolkit' | ||||
| import { z } from 'zod' | ||||
|  | ||||
| /** | ||||
|  * Default config values. | ||||
|  */ | ||||
| export const defaultConfig = { | ||||
|   ignore: [], | ||||
|   nameEnum: (name: string) => snakeCase(name).toUpperCase(), | ||||
|   nameEnumField: (collectionName: string, fieldName: string) => `${collectionName}${pascalCase(fieldName)}`, | ||||
|   nameEnumSchema: (name: string) => pascalCase(name), | ||||
|   nameEnumType: (name: string) => pascalCase(name), | ||||
|   nameEnumValues: (name: string) => `${name}Values`, | ||||
|   nameRecordSchema: (name: string) => `${pascalCase(name)}Record`, | ||||
|   nameRecordType: (name: string) => `${pascalCase(name)}Record`, | ||||
|   output: "./zod-pocketbase.ts", | ||||
| }; | ||||
| 	ignore: [], | ||||
| 	nameEnum: (name: string) => snakeCase(name).toUpperCase(), | ||||
| 	nameEnumField: (collectionName: string, fieldName: string) => | ||||
| 		`${collectionName}${pascalCase(fieldName)}`, | ||||
| 	nameEnumSchema: (name: string) => pascalCase(name), | ||||
| 	nameEnumType: (name: string) => pascalCase(name), | ||||
| 	nameEnumValues: (name: string) => `${name}Values`, | ||||
| 	nameRecordSchema: (name: string) => `${pascalCase(name)}Record`, | ||||
| 	nameRecordType: (name: string) => `${pascalCase(name)}Record`, | ||||
| 	output: './zod-pocketbase-continue.ts', | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Schema for the PocketBase credentials. | ||||
|  */ | ||||
| export const Credentials = z.object({ | ||||
|   adminEmail: z.string().email(), | ||||
|   adminPassword: z.string(), | ||||
|   url: z.string().url(), | ||||
| }); | ||||
| export type Credentials = z.infer<typeof Credentials>; | ||||
| 	adminEmail: z.email(), | ||||
| 	adminPassword: z.string(), | ||||
| 	url: z.url(), | ||||
| }) | ||||
| export type Credentials = z.infer<typeof Credentials> | ||||
|  | ||||
| /** | ||||
|  * Schema for the config file. | ||||
|  */ | ||||
| export const Config = z.object({ | ||||
|   ...Credentials.partial().shape, | ||||
|   ignore: z.string().array().default(defaultConfig.ignore), | ||||
|   nameEnum: z | ||||
|     .function(z.tuple([z.string()]), z.string()) | ||||
|     .optional() | ||||
|     .transform((f) => f ?? defaultConfig.nameEnum), | ||||
|   nameEnumField: z | ||||
|     .function(z.tuple([z.string(), z.string()]), z.string()) | ||||
|     .optional() | ||||
|     .transform((f) => f ?? defaultConfig.nameEnumField), | ||||
|   nameEnumSchema: z | ||||
|     .function(z.tuple([z.string()]), z.string()) | ||||
|     .optional() | ||||
|     .transform((f) => f ?? defaultConfig.nameEnumSchema), | ||||
|   nameEnumType: z | ||||
|     .function(z.tuple([z.string()]), z.string()) | ||||
|     .optional() | ||||
|     .transform((f) => f ?? defaultConfig.nameEnumType), | ||||
|   nameEnumValues: z | ||||
|     .function(z.tuple([z.string()]), z.string()) | ||||
|     .optional() | ||||
|     .transform((f) => f ?? defaultConfig.nameEnumValues), | ||||
|   nameRecordSchema: z | ||||
|     .function(z.tuple([z.string()]), z.string()) | ||||
|     .optional() | ||||
|     .transform((f) => f ?? defaultConfig.nameRecordSchema), | ||||
|   nameRecordType: z | ||||
|     .function(z.tuple([z.string()]), 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>; | ||||
| 	...Credentials.partial().shape, | ||||
| 	ignore: z.string().array().default(defaultConfig.ignore), | ||||
| 	nameEnum: z | ||||
| 		.function({ | ||||
| 			input: [z.string()], | ||||
| 			output: z.string(), | ||||
| 		}) | ||||
| 		.optional() | ||||
| 		.transform((f) => f ?? defaultConfig.nameEnum), | ||||
| 	nameEnumField: z | ||||
| 		.function({ | ||||
| 			input: [z.string(), z.string()], | ||||
| 			output: z.string(), | ||||
| 		}) | ||||
| 		.optional() | ||||
| 		.transform((f) => f ?? defaultConfig.nameEnumField), | ||||
| 	nameEnumSchema: z | ||||
| 		.function({ | ||||
| 			input: [z.string()], | ||||
| 			output: z.string(), | ||||
| 		}) | ||||
| 		.optional() | ||||
| 		.transform((f) => f ?? defaultConfig.nameEnumSchema), | ||||
| 	nameEnumType: z | ||||
| 		.function({ | ||||
| 			input: [z.string()], | ||||
| 			output: z.string(), | ||||
| 		}) | ||||
| 		.optional() | ||||
| 		.transform((f) => f ?? defaultConfig.nameEnumType), | ||||
| 	nameEnumValues: z | ||||
| 		.function({ | ||||
| 			input: [z.string()], | ||||
| 			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> | ||||
|   | ||||
							
								
								
									
										335
									
								
								src/content.ts
									
									
									
									
									
								
							
							
						
						
									
										335
									
								
								src/content.ts
									
									
									
									
									
								
							| @@ -1,182 +1,203 @@ | ||||
| import { sortBy } from "es-toolkit"; | ||||
| import type { CollectionModel, CollectionField } from "pocketbase"; | ||||
| import type { GenerateOpts } from "./server/utils.ts"; | ||||
| import { sortBy } from 'es-toolkit' | ||||
| import type { CollectionField, CollectionModel } from 'pocketbase' | ||||
| import type { GenerateOpts } from '@/server/utils.ts' | ||||
|  | ||||
| export function stringifyContent(collections: CollectionModel[], opts: GenerateOpts) { | ||||
|   function getCollectionSelectFields() { | ||||
|     return collections.flatMap((collection) => | ||||
|       collection.fields | ||||
|         .filter((field: CollectionField) => field.type === "select") | ||||
|         .map((field: CollectionField) => ({ | ||||
|           name: opts.nameEnumField(collection.name, field.name), | ||||
|           values: ((field as any).options?.values ?? []) as string[], | ||||
|         })), | ||||
|     ); | ||||
|   } | ||||
| 	function getCollectionSelectFields() { | ||||
| 		return collections.flatMap((collection) => | ||||
| 			collection.fields | ||||
| 				.filter((field: CollectionField) => field.type === 'select') | ||||
| 				.map((field: CollectionField) => ({ | ||||
| 					name: opts.nameEnumField(collection.name, field.name), | ||||
| 					values: ((field as any).values ?? []) as string[], | ||||
| 				})), | ||||
| 		) | ||||
| 	} | ||||
|  | ||||
|   function stringifyEnum({ name, values }: SelectField) { | ||||
|     const valuesName = opts.nameEnumValues(name); | ||||
|     const schemaName = opts.nameEnumSchema(name); | ||||
|     const enumName = opts.nameEnum(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;`; | ||||
|   } | ||||
| 	function stringifyEnum({ name, values }: SelectField) { | ||||
| 		const valuesName = opts.nameEnumValues(name) | ||||
| 		const schemaName = opts.nameEnumSchema(name) | ||||
| 		const enumName = opts.nameEnum(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;` | ||||
| 	} | ||||
|  | ||||
|   function stringifyRecord({ name, fields }: CollectionModel) { | ||||
|     const schemaName = opts.nameRecordSchema(name); | ||||
|     const typeName = opts.nameRecordType(name); | ||||
| 	function stringifyRecord({ name, fields }: CollectionModel) { | ||||
| 		const schemaName = opts.nameRecordSchema(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", // inherited from BaseModel/RecordModel | ||||
|       "password", | ||||
|       "tokenKey", // should not be in response schema | ||||
|     ]); | ||||
| 		const systemFields = new Set([ | ||||
| 			'id', | ||||
| 			'created', | ||||
| 			'updated', | ||||
| 			'collectionId', | ||||
| 			'collectionName', | ||||
| 			'expand', | ||||
| 			'password', | ||||
| 			'tokenKey', | ||||
| 		]) | ||||
|  | ||||
|     const customFields = fields.filter((field) => !systemFields.has(field.name)); | ||||
|     const fieldStrings = sortBy(customFields, ["name"]).map((field) => stringifyField(field, name)); | ||||
| 		const customFields = fields.filter((field) => !systemFields.has(field.name)) | ||||
| 		const fieldStrings = sortBy(customFields, ['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${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 stringifySchemasEntry({ name }: CollectionModel) { | ||||
| 		return `["${name}", ${opts.nameRecordSchema(name)}]` | ||||
| 	} | ||||
|  | ||||
|   function stringifyBoolField(_: CollectionField) { | ||||
|     return "z.boolean()"; | ||||
|   } | ||||
| 	function stringifyService({ name }: CollectionModel) { | ||||
| 		return `\t\tcollection(idOrName: "${name}"): RecordService<z.input<typeof ${opts.nameRecordSchema(name)}>>;` | ||||
| 	} | ||||
|  | ||||
|   function stringifyDateField(_field: CollectionField) { | ||||
|     // TODO: implement min and max | ||||
|     return "z.string().pipe(z.coerce.date())"; | ||||
|   } | ||||
| 	function stringifyField(field: CollectionField, collectionName: string) { | ||||
| 		let schema: string | ||||
| 		switch (field.type) { | ||||
| 			case 'bool': | ||||
| 				schema = 'z.boolean()' | ||||
| 				break | ||||
|  | ||||
|   function stringifyEditorField(_field: CollectionField) { | ||||
|     // TODO: implement convertUrls | ||||
|     return "z.string()"; | ||||
|   } | ||||
| 			case 'date': { | ||||
| 				const minConstraintDate = field.min ? `.min(new Date("${field.min}"))` : '' | ||||
| 				const maxConstraintDate = field.max ? `.max(new Date("${field.max}"))` : '' | ||||
|  | ||||
|   function stringifyEmailField(_field: CollectionField) { | ||||
|     // TODO: implement exceptDomains and onlyDomains | ||||
|     return "z.string().email()"; | ||||
|   } | ||||
| 				schema = `z.coerce.date()${minConstraintDate}${maxConstraintDate}` | ||||
| 				break | ||||
| 			} | ||||
|  | ||||
|   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})` : ""}`}`; | ||||
|   } | ||||
| 			case 'editor': | ||||
| 				// TODO: implement convertUrls | ||||
| 				schema = 'z.string()' | ||||
| 				break | ||||
|  | ||||
|   function stringifyJsonField(_field: CollectionField) { | ||||
|     // TODO: implement maxSize and json schema | ||||
|     return "z.any()"; | ||||
|   } | ||||
| 			case 'email': { | ||||
| 				const onlyDomainsConstraint = createDomainConstraint(field.onlyDomains, true, 'email') | ||||
| 				const exceptDomainsConstraint = createDomainConstraint(field.exceptDomains, false, 'email') | ||||
|  | ||||
|   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})` : ""}`; | ||||
|   } | ||||
| 				schema = `z.email()${onlyDomainsConstraint}${exceptDomainsConstraint}` | ||||
| 				break | ||||
| 			} | ||||
|  | ||||
|   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}`; | ||||
|   } | ||||
| 			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 || []; | ||||
|  | ||||
|   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})`}`; | ||||
|   } | ||||
| 				const fileFieldMaxSelect = maxSelectFile ? `.max(${maxSelectFile})` : '' | ||||
| 				const fileFieldTypeArray = maxSelectFile === 1 ? '' : `.array()${fileFieldMaxSelect}` | ||||
|  | ||||
|   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})` : ""}`; | ||||
|   } | ||||
| 				const fileMaxSizeFile = maxSizeFile ? `.max(${maxSizeFile})` : '' | ||||
| 				const fileMimeTypesArray = | ||||
| 					mimeTypesArray.length > 0 ? `.mime(${JSON.stringify(mimeTypesArray)})` : '' | ||||
|  | ||||
|   function stringifyUrlField(_field: CollectionField) { | ||||
|     // TODO: implement exceptDomains and onlyDomains | ||||
|     return "z.string().url()"; | ||||
|   } | ||||
| 				const baseFileSchema = `z.union([z.string(), z.file()${fileMaxSizeFile}${fileMimeTypesArray}])` | ||||
| 				schema = `${baseFileSchema}${fileFieldTypeArray}` | ||||
| 				break | ||||
| 			} | ||||
|  | ||||
|   function stringifyGeoPointField(_field: CollectionField) { | ||||
|     return "z.object({ lat: z.number().min(-90).max(90), lng: z.number().min(-180).max(180) })"; | ||||
|   } | ||||
| 			case 'json': | ||||
| 				schema = field.maxSize > 0 ? `pbJsonField(${field.maxSize})` : 'pbJsonField()' | ||||
| 				break | ||||
|  | ||||
|   function stringifySchemasEntry({ name }: CollectionModel) { | ||||
|     return `["${name}", ${opts.nameRecordSchema(name)}]`; | ||||
|   } | ||||
| 			case 'number': { | ||||
| 				const maxNumber = field.maxNumber ? `.max(${field.maxNumber})` : '' | ||||
| 				const minNumber = field.minNumber ? `.min(${field.minNumber})` : '' | ||||
| 				const noDecimal = field.noDecimal ? '.int()' : '' | ||||
|  | ||||
|   function stringifyService({ name }: CollectionModel) { | ||||
|     return `\t\tcollection(idOrName: "${name}"): RecordService<z.input<typeof ${opts.nameRecordSchema(name)}>>;`; | ||||
|   } | ||||
| 				schema = `z.number()${noDecimal}${minNumber}${maxNumber}` | ||||
| 				break | ||||
| 			} | ||||
|  | ||||
|   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.AnyZodObject>([\n\t${collections.map(stringifySchemasEntry).join(",\n\t")},\n]);`, | ||||
|     services: collections.map(stringifyService).join("\n"), | ||||
|   }; | ||||
| 			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 { fullListOptionsFrom, optionsFrom } from "./options.js"; | ||||
| import type { AnyZodRecord, RecordFullListOpts, RecordIdRef, RecordRef, RecordSlugRef } from "./types.ts"; | ||||
| import { AnyRecordsList, type RecordsList } from "./schemas.ts"; | ||||
| import type { default as Pocketbase, SendOptions } from 'pocketbase' | ||||
| import type { ZodObject } from 'zod' | ||||
| import { fullListOptionsFrom, optionsFrom } from '@/options.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) { | ||||
|   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: RecordIdRef<C>, opts: GetRecordOpts<S>): Promise<S["_output"]>; | ||||
|   async function getRecord<C extends string, S extends AnyZodRecord>(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 getRecord<C extends string, S extends ZodObject>( | ||||
| 		ref: RecordSlugRef<C>, | ||||
| 		opts: GetRecordOpts<S>, | ||||
| 	): Promise<S['_output']> | ||||
| 	async function getRecord<C extends string, S extends ZodObject>( | ||||
| 		ref: RecordIdRef<C>, | ||||
| 		opts: GetRecordOpts<S>, | ||||
| 	): Promise<S['_output']> | ||||
| 	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>> { | ||||
|     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); | ||||
|   } | ||||
| 	async function getRecords<C extends string, S extends ZodObject>( | ||||
| 		collection: C, | ||||
| 		opts: GetRecordsOpts<S>, | ||||
| 	): Promise<RecordsList<S>> { | ||||
| 		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 GetRecordsOpts<S extends AnyZodRecord> = RecordFullListOpts<S> & { schema: S }; | ||||
| export type HelpersFromOpts = { fetch?: SendOptions["fetch"]; pocketbase: Pocketbase }; | ||||
| export type GetRecordOpts<S extends ZodObject> = { schema: S } | ||||
| export type GetRecordsOpts<S extends ZodObject> = RecordFullListOpts<S> & { schema: S } | ||||
| 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 "./helpers.js"; | ||||
| export * from "./options.js"; | ||||
| export * from "./schemas.js"; | ||||
| export * from "./types.ts"; | ||||
| export * from "./utils.js"; | ||||
| export * from '@/config.js' | ||||
| export * from '@/helpers.js' | ||||
| export * from '@/options.js' | ||||
| export * from '@/schemas.js' | ||||
| export * from '@/types.ts' | ||||
| 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 { AnyZodRecord, RecordFullListOpts, RecordListOpts } from "./types.ts"; | ||||
| import type { ZodObject } from 'zod' | ||||
| import { z } from 'zod' | ||||
| import type { RecordFullListOpts, RecordListOpts } from '@/types.ts' | ||||
|  | ||||
| /** | ||||
|  * 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 | ||||
|  * @returns A new schema extended with the given expansion | ||||
|  */ | ||||
| export function expandFrom<S extends AnyZodRecord>(schema: S) { | ||||
|   return expandFromRec(schema) | ||||
|     .sort((k1, k2) => (k1 < k2 ? -1 : 1)) | ||||
|     .join(","); | ||||
| export function expandFrom<S extends ZodObject<any, any>>(schema: S) { | ||||
| 	return expandFromRec(schema) | ||||
| 		.sort((k1, k2) => (k1 < k2 ? -1 : 1)) | ||||
| 		.join(',') | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -19,10 +20,10 @@ export function expandFrom<S extends AnyZodRecord>(schema: S) { | ||||
|  * @param shape - The shape of the expansion | ||||
|  * @returns A new schema extended with the given expansion | ||||
|  */ | ||||
| export function fieldsFrom<S extends AnyZodRecord>(schema: S) { | ||||
|   return fieldsFromRec(schema) | ||||
|     .sort((k1, k2) => (k1 < k2 ? -1 : 1)) | ||||
|     .join(","); | ||||
| export function fieldsFrom<S extends ZodObject<any, any>>(schema: S) { | ||||
| 	return fieldsFromRec(schema) | ||||
| 		.sort((k1, k2) => (k1 < k2 ? -1 : 1)) | ||||
| 		.join(',') | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -31,8 +32,8 @@ export function fieldsFrom<S extends AnyZodRecord>(schema: S) { | ||||
|  * @param shape - The shape of the expansion | ||||
|  * @returns A new schema extended with the given expansion | ||||
|  */ | ||||
| export function optionsFrom<S extends AnyZodRecord>(schema: S) { | ||||
|   return { expand: expandFrom(schema), fields: fieldsFrom(schema) }; | ||||
| export function optionsFrom<S extends ZodObject<any, any>>(schema: S) { | ||||
| 	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 | ||||
|  * @returns A new schema extended with the given expansion | ||||
|  */ | ||||
| export function listOptionsFrom<S extends AnyZodRecord>(schema: S, opts: RecordListOpts<S>) { | ||||
|   const { page = 1, perPage = 30, ...rest } = opts; | ||||
|   return { ...optionsFrom(schema), page, perPage, ...rest }; | ||||
| export function listOptionsFrom<S extends ZodObject<any, any>>(schema: S, opts: RecordListOpts<S>) { | ||||
| 	const { page = 1, perPage = 30, ...rest } = opts | ||||
| 	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 | ||||
|  * @returns A new schema extended with the given expansion | ||||
|  */ | ||||
| export function fullListOptionsFrom<S extends AnyZodRecord>(schema: S, opts: RecordFullListOpts<S>) { | ||||
|   const { page = 1, perPage = 200, skipTotal = true, ...rest } = opts; | ||||
|   return listOptionsFrom(schema, { page, perPage, skipTotal, ...rest }); | ||||
| export function fullListOptionsFrom<S extends ZodObject<any, any>>( | ||||
| 	schema: S, | ||||
| 	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 = "") { | ||||
|   let expands: string[] = []; | ||||
|   const shape = getObjectSchemaDescendant(schema)?.shape; | ||||
|   if (!shape || !("expand" in shape)) return []; | ||||
|   for (const [key, value] of Object.entries(getObjectSchemaDescendant(shape.expand)!.shape)) { | ||||
|     expands = [...expands, `${prefix}${key}`]; | ||||
|     if (hasObjectSchemaDescendant(value)) expands = [...expands, ...expandFromRec(getObjectSchemaDescendant(value)!, `${prefix}${key}.`)]; | ||||
|   } | ||||
|   return expands; | ||||
| function expandFromRec<S>(schema: S, prefix = '') { | ||||
| 	let expands: string[] = [] | ||||
| 	const shape = getObjectSchemaDescendant(schema)?.shape | ||||
| 	if (!shape || !('expand' in shape)) return [] | ||||
| 	for (const [key, value] of Object.entries(getObjectSchemaDescendant(shape.expand)!.shape)) { | ||||
| 		expands = [...expands, `${prefix}${key}`] | ||||
| 		if (hasObjectSchemaDescendant(value)) | ||||
| 			expands = [ | ||||
| 				...expands, | ||||
| 				...expandFromRec(getObjectSchemaDescendant(value)!, `${prefix}${key}.`), | ||||
| 			] | ||||
| 	} | ||||
| 	return expands | ||||
| } | ||||
|  | ||||
| function fieldsFromRec<S extends z.ZodTypeAny>(schema: S, prefix = "") { | ||||
|   let fields: string[] = []; | ||||
|   const shape = getObjectSchemaDescendant(schema)?.shape; | ||||
|   if (!shape) return []; | ||||
|   for (const [key, value] of Object.entries(shape)) { | ||||
|     fields = [ | ||||
|       ...fields, | ||||
|       ...(hasObjectSchemaDescendant(value) ? fieldsFromRec(getObjectSchemaDescendant(value)!, `${prefix}${key}.`) : [`${prefix}${key}`]), | ||||
|     ]; | ||||
|   } | ||||
|   return fields.sort((k1, k2) => (k1 < k2 ? -1 : 1)); | ||||
| function fieldsFromRec<S>(schema: S, prefix = '') { | ||||
| 	let fields: string[] = [] | ||||
| 	const shape = getObjectSchemaDescendant(schema)?.shape | ||||
| 	if (!shape) return [] | ||||
| 	for (const [key, value] of Object.entries(shape)) { | ||||
| 		fields = [ | ||||
| 			...fields, | ||||
| 			...(hasObjectSchemaDescendant(value) | ||||
| 				? fieldsFromRec(getObjectSchemaDescendant(value)!, `${prefix}${key}.`) | ||||
| 				: [`${prefix}${key}`]), | ||||
| 		] | ||||
| 	} | ||||
| 	return fields.sort((k1, k2) => (k1 < k2 ? -1 : 1)) | ||||
| } | ||||
|  | ||||
| function hasObjectSchemaDescendant(value: unknown): value is z.ZodTypeAny { | ||||
|   if (value instanceof z.ZodEffects) return hasObjectSchemaDescendant(value.innerType()); | ||||
|   if (value instanceof z.ZodArray) return hasObjectSchemaDescendant(value.element); | ||||
|   if (value instanceof z.ZodOptional) return hasObjectSchemaDescendant(value.unwrap()); | ||||
|   return value instanceof z.ZodObject; | ||||
| function hasObjectSchemaDescendant(value: unknown): value is any { | ||||
| 	// Handle ZodPipe | ||||
| 	if (value && typeof value === 'object' && 'constructor' in value) { | ||||
| 		const valueConstructor = value.constructor | ||||
| 		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 { | ||||
|   if (schema instanceof z.ZodEffects) return getObjectSchemaDescendant(schema.innerType()); | ||||
|   if (schema instanceof z.ZodArray) return getObjectSchemaDescendant(schema.element); | ||||
|   if (schema instanceof z.ZodOptional) return getObjectSchemaDescendant(schema.unwrap()); | ||||
|   if (schema instanceof z.ZodObject) return schema; | ||||
|   return; | ||||
| function getObjectSchemaDescendant<S>(schema: S): ZodObject | undefined { | ||||
| 	// Handle ZodPipe | ||||
| 	if (schema && typeof schema === 'object' && 'constructor' in schema) { | ||||
| 		const schemaConstructor = schema.constructor | ||||
| 		if (schemaConstructor?.name === 'ZodPipe') { | ||||
| 			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 { AnyZodRecord, HasRequiredKeys, ZodRecordKeys } from "./types.ts"; | ||||
| import type { ZodObject, ZodOptional, ZodRawShape } from 'zod' | ||||
| import { z } from 'zod' | ||||
| import type { HasRequiredKeys, ZodRecordKeys } from '@/types.ts' | ||||
|  | ||||
| /** | ||||
|  * Records list schema. | ||||
|  */ | ||||
| export const AnyRecordsList = z.object({ | ||||
|   items: z.any().array(), | ||||
|   page: z.number().int().min(1), | ||||
|   perPage: z.number().int().min(1), | ||||
|   totalItems: z.number().int().min(-1), | ||||
|   totalPages: z.number().int().min(-1), | ||||
| }); | ||||
| 	items: z.any().array(), | ||||
| 	page: z.int().min(1), | ||||
| 	perPage: z.int().min(1), | ||||
| 	totalItems: z.int().min(-1), | ||||
| 	totalPages: z.int().min(-1), | ||||
| }) | ||||
|  | ||||
| /** | ||||
|  * Extends the given schema with the given expansion. | ||||
| @@ -18,11 +19,19 @@ export const AnyRecordsList = z.object({ | ||||
|  * @param shape - The shape of the expansion | ||||
|  * @returns A new schema extended with the given expansion | ||||
|  */ | ||||
| export function expand<S extends AnyZodObject, E extends ZodRawShape>(schema: S, shape: E) { | ||||
|   const isExpandOptional = Object.entries(shape).every(([, value]) => value instanceof z.ZodOptional); | ||||
|   return z | ||||
|     .object({ ...schema.shape, expand: isExpandOptional ? z.object(shape).optional() : z.object(shape) }) | ||||
|     .transform(({ expand, ...rest }) => ({ ...rest, ...(expand ?? {}) })) as ZodObjectExpand<S, 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, | ||||
| 	) | ||||
| 	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 | ||||
|  * @returns A new schema with only the given keys | ||||
|  */ | ||||
| export function pick<S extends AnyZodObject, K extends ZodRecordKeys<S>[]>(schema: S, keys: K) { | ||||
|   return schema.pick(Object.fromEntries(keys.map((key) => [key, true]))) as ZodObjectPick<S, K>; | ||||
| export function pick<S extends ZodObject<any, any>, K extends ZodRecordKeys<S>[]>( | ||||
| 	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 | ||||
|  * @returns A new schema with only the given keys | ||||
|  */ | ||||
| export function select<S extends AnyZodObject, K extends ZodRecordKeys<S>[], E extends ZodRawShape>( | ||||
|   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 AnyZodObject, 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 function select< | ||||
| 	S extends ZodObject<any, any>, | ||||
| 	K extends ZodRecordKeys<S>[], | ||||
| 	E extends ZodRawShape, | ||||
| >(schema: S, keys: K, shape: E): ZodObjectExpand<ZodObjectPick<S, K>, E> | ||||
| export function select<S extends ZodObject<any, any>, K extends ZodRecordKeys<S>[]>( | ||||
| 	schema: S, | ||||
| 	keys: K, | ||||
| ): ZodObjectPick<S, K> | ||||
| export function select< | ||||
| 	S extends ZodObject<any, any>, | ||||
| 	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> = | ||||
|   S extends ZodObject<infer T, infer U, infer C> | ||||
|     ? ZodEffects< | ||||
|         ZodObject<objectUtil.extendShape<T, { expand: HasRequiredKeys<E> extends true ? ZodObject<E> : ZodOptional<ZodObject<E>> }>, U, C>, | ||||
|         ZodObject<objectUtil.extendShape<T, E>, U, C>["_output"] | ||||
|       > | ||||
|     : never; | ||||
| export type ZodObjectExpand< | ||||
| 	S extends ZodObject<any, any>, | ||||
| 	E extends ZodRawShape, | ||||
| > = S extends ZodObject<infer T, infer U> | ||||
| 	? z.ZodPipe< | ||||
| 			ZodObject< | ||||
| 				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>[]> = | ||||
|   S extends ZodObject<infer T, infer U, infer C> ? ZodObject<Pick<T, K[number]>, U, C> : never; | ||||
| export type ZodObjectPick< | ||||
| 	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 RecordsList<S extends AnyZodRecord> = Omit<AnyRecordsList, "items"> & { items: S["_output"][] }; | ||||
| export type AnyRecordsList = z.infer<typeof AnyRecordsList> | ||||
| 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 type { Credentials } from "./config.ts"; | ||||
| import Pocketbase from 'pocketbase' | ||||
| import type { Credentials } from '@/config.ts' | ||||
|  | ||||
| let adminPocketbase: Pocketbase; | ||||
| let adminPocketbase: Pocketbase | ||||
|  | ||||
| export async function getPocketbase({ adminEmail, adminPassword, url }: Credentials) { | ||||
|   if (!adminPocketbase) { | ||||
|     const pocketbase = new Pocketbase(url); | ||||
|     await pocketbase.collection("_superusers").authWithPassword(adminEmail, adminPassword); | ||||
|     adminPocketbase = pocketbase; | ||||
|   } | ||||
|   return adminPocketbase; | ||||
| 	if (!adminPocketbase) { | ||||
| 		const pocketbase = new Pocketbase(url) | ||||
| 		await pocketbase.collection('_superusers').authWithPassword(adminEmail, adminPassword) | ||||
| 		adminPocketbase = pocketbase | ||||
| 	} | ||||
| 	return adminPocketbase | ||||
| } | ||||
|   | ||||
| @@ -1,91 +1,125 @@ | ||||
| #!/usr/bin/env bun | ||||
|  | ||||
| import { Config, type ResolvedConfig } from "../config.ts"; | ||||
| import pkg from "../../package.json" with { type: "json" }; | ||||
| import { loadConfig } from "c12"; | ||||
| import { defineCommand, runMain } from "citty"; | ||||
| import { cancel, group, intro, log, outro, confirm, text, spinner, multiselect, isCancel } from "@clack/prompts"; | ||||
| import { fetchCollections } from "../utils.ts"; | ||||
| import { generate } from "./utils.ts"; | ||||
| import { existsSync } from "node:fs"; | ||||
| import { existsSync } from 'node:fs' | ||||
| import { | ||||
| 	cancel, | ||||
| 	confirm, | ||||
| 	group, | ||||
| 	intro, | ||||
| 	isCancel, | ||||
| 	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() { | ||||
|   const { config } = await loadConfig({ name: "zod-pocketbase", rcFile: false, dotenv: true }); | ||||
|   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 }); | ||||
|   if (!result.success) { | ||||
|     log.error("Invalid fields in your config file."); | ||||
|     onCancel(); | ||||
|   } | ||||
|   return result.data!; | ||||
| 	const { config } = await loadConfig({ | ||||
| 		name: 'zod-pocketbase', | ||||
| 		rcFile: false, | ||||
| 		dotenv: true, | ||||
| 	}) | ||||
| 	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 }) | ||||
| 	if (!result.success) { | ||||
| 		log.error('Invalid fields in your config file.') | ||||
| 		onCancel() | ||||
| 	} | ||||
| 	return result.data! | ||||
| } | ||||
|  | ||||
| function onCancel() { | ||||
|   cancel("Operation cancelled."); | ||||
|   process.exit(0); | ||||
| 	cancel('Operation cancelled.') | ||||
| 	process.exit(0) | ||||
| } | ||||
|  | ||||
| async function selectCollections(config: ResolvedConfig) { | ||||
|   const credentialPrompts = { | ||||
|     url: () => text({ message: "What is the url of your pocketbase instance?", initialValue: config.url ?? "" }), | ||||
|     adminEmail: () => text({ message: "What is your admin's email?", initialValue: config.adminEmail ?? "" }), | ||||
|     adminPassword: () => text({ message: "What is your admin's password?", initialValue: config.adminPassword ?? "" }), | ||||
|   }; | ||||
|   const credentials = await group(credentialPrompts, { onCancel }); | ||||
|   const s = spinner(); | ||||
|   s.start("Fetching collections..."); | ||||
|   try { | ||||
|     const allCollections = await fetchCollections(credentials); | ||||
|     s.stop("Successfully fetched collections."); | ||||
|     const collectionNames = await multiselect({ | ||||
|       message: "What collections do you want to generate schemas for?", | ||||
|       options: allCollections.map(({ name: value }) => ({ value })), | ||||
|       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); | ||||
|   } | ||||
| 	const credentialPrompts = { | ||||
| 		url: () => | ||||
| 			text({ | ||||
| 				message: 'What is the url of your pocketbase instance?', | ||||
| 				initialValue: config.url ?? '', | ||||
| 			}), | ||||
| 		adminEmail: () => | ||||
| 			text({ message: "What is your admin's email?", initialValue: config.adminEmail ?? '' }), | ||||
| 		adminPassword: () => | ||||
| 			text({ message: "What is your admin's password?", initialValue: config.adminPassword ?? '' }), | ||||
| 	} | ||||
| 	const credentials = await group(credentialPrompts, { onCancel }) | ||||
| 	const s = spinner() | ||||
| 	s.start('Fetching collections...') | ||||
| 	try { | ||||
| 		const allCollections = await fetchCollections(credentials) | ||||
| 		s.stop('Successfully fetched collections.') | ||||
| 		const collectionNames = await multiselect({ | ||||
| 			message: 'What collections do you want to generate schemas for?', | ||||
| 			options: allCollections.map(({ name: value }) => ({ value })), | ||||
| 			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) { | ||||
|   const output = await text({ | ||||
|     message: "What is the generated file path?", | ||||
|     initialValue: config.output, | ||||
|     validate: (value) => { | ||||
|       if (!value) return "Please enter a path."; | ||||
|       if (value[0] !== ".") return "Please enter a relative path."; | ||||
|       return; | ||||
|     }, | ||||
|   }); | ||||
|   if (isCancel(output)) onCancel(); | ||||
| 	const output = await text({ | ||||
| 		message: 'What is the generated file path?', | ||||
| 		initialValue: config.output, | ||||
| 		validate: (value) => { | ||||
| 			if (!value) return 'Please enter a path.' | ||||
| 			if (value[0] !== '.') return 'Please enter a relative path.' | ||||
| 			return | ||||
| 		}, | ||||
| 	}) | ||||
| 	if (isCancel(output)) onCancel() | ||||
|  | ||||
|   if (existsSync(output as string)) { | ||||
|     const confirmed = await confirm({ message: "The file already exists, would you like to overwrite it?" }); | ||||
|     if (isCancel(confirmed)) onCancel(); | ||||
|     if (!confirmed) return setGeneratedFilePath(config); | ||||
|   } | ||||
| 	if (existsSync(output as string)) { | ||||
| 		const confirmed = await confirm({ | ||||
| 			message: 'The file already exists, would you like to overwrite it?', | ||||
| 		}) | ||||
| 		if (isCancel(confirmed)) onCancel() | ||||
| 		if (!confirmed) return setGeneratedFilePath(config) | ||||
| 	} | ||||
|  | ||||
|   return output as string; | ||||
| 	return output as string | ||||
| } | ||||
|  | ||||
| const main = defineCommand({ | ||||
|   meta: { name: "zod-pocketbase", version: pkg.version, description: "Generate Zod schemas for your pocketbase instance." }, | ||||
|   run: async () => { | ||||
|     intro(`ZOD POCKETBASE`); | ||||
|     const config = await getConfig(); | ||||
|     const collections = await selectCollections(config); | ||||
|     const output = await setGeneratedFilePath(config); | ||||
| 	meta: { | ||||
| 		name: 'zod-pocketbase-continue', | ||||
| 		version: pkg.version, | ||||
| 		description: 'Generate Zod schemas for your pocketbase instance.', | ||||
| 	}, | ||||
| 	run: async () => { | ||||
| 		intro(`ZOD POCKETBASE`) | ||||
| 		const config = await getConfig() | ||||
| 		const collections = await selectCollections(config) | ||||
| 		const output = await setGeneratedFilePath(config) | ||||
|  | ||||
|     const s = spinner(); | ||||
|     s.start("Generating your schemas..."); | ||||
|     await generate(collections, { ...config, output }); | ||||
|     s.stop("Schemas successfully generated."); | ||||
| 		const s = spinner() | ||||
| 		s.start('Generating your schemas...') | ||||
| 		await generate(collections, { ...config, output }) | ||||
| 		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 { dirname, resolve } from "node:path"; | ||||
| import { fileURLToPath } from "node:url"; | ||||
| import type { CollectionModel } from "pocketbase"; | ||||
| import { stringifyContent } from "../content.js"; | ||||
| import type { ResolvedConfig } from "../config.ts"; | ||||
| import { mkdirSync, readFileSync, writeFileSync } from 'node:fs' | ||||
| import { dirname, resolve } from 'node:path' | ||||
| import { fileURLToPath } from 'node:url' | ||||
| import type { CollectionModel } from 'pocketbase' | ||||
| import type { ResolvedConfig } from '@/config.ts' | ||||
| import { stringifyContent } from '@/content.js' | ||||
|  | ||||
| export async function generate(collections: CollectionModel[], opts: GenerateOpts) { | ||||
|   const stub = readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), "../../assets/stubs/index.ts"), "utf-8"); | ||||
|   const { collectionNames, enums, records, services } = stringifyContent(collections, opts); | ||||
|   const content = stub | ||||
|     .replace("@@_COLLECTION_NAMES_@@", collectionNames) | ||||
|     .replace("@@_ENUMS_@@", enums) | ||||
|     .replace("@@_RECORDS_@@", records) | ||||
|     .replace("@@_SERVICES_@@", services); | ||||
|   mkdirSync(dirname(opts.output), { recursive: true }); | ||||
|   writeFileSync(opts.output, content); | ||||
| 	const stub = readFileSync( | ||||
| 		resolve(dirname(fileURLToPath(import.meta.url)), '../../assets/stubs/index.ts'), | ||||
| 		'utf-8', | ||||
| 	) | ||||
| 	const { collectionNames, enums, records, services } = stringifyContent(collections, opts) | ||||
| 	const content = stub | ||||
| 		.replace('@@_COLLECTION_NAMES_@@', collectionNames) | ||||
| 		.replace('@@_ENUMS_@@', enums) | ||||
| 		.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> = { | ||||
|   filter?: string; | ||||
|   page?: number; | ||||
|   perPage?: number; | ||||
|   skipTotal?: boolean; | ||||
|   sort?: ZodRecordSort<S>; | ||||
| }; | ||||
| export type RecordIdRef<C extends string> = { collection: C; id: string } | ||||
| export type RecordSlugRef<C extends string> = { collection: C; slug: string } | ||||
| export type RecordRef<C extends string> = RecordIdRef<C> | RecordSlugRef<C> | ||||
|  | ||||
| export type RecordIdRef<C extends string> = { collection: C; id: 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 ZodObject<any, any>> = Extract<keyof S['_input'], string> | ||||
|  | ||||
| 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 AnyZodRecord> = `${"+" | "-"}${ZodRecordMainKeys<S>}` | "@random"; | ||||
| export type ZodRecordSort<S extends ZodObject<any, any>> = | ||||
| 	| `${'+' | '-'}${ZodRecordMainKeys<S>}` | ||||
| 	| '@random' | ||||
|  | ||||
| type RequiredKeysOf<S extends ZodRawShape> = Exclude< | ||||
|   { | ||||
|     [Key in keyof S]: S[Key] extends ZodOptional<ZodTypeAny> ? never : Key; | ||||
|   }[keyof S], | ||||
|   undefined | ||||
| >; | ||||
| export type HasRequiredKeys<S extends ZodRawShape> = RequiredKeysOf<S> extends never ? false : true; | ||||
| 	{ | ||||
| 		[Key in keyof S]: S[Key] extends ZodOptional<ZodType> ? never : Key | ||||
| 	}[keyof S], | ||||
| 	undefined | ||||
| > | ||||
| 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 type { CollectionModel } from "pocketbase"; | ||||
| import { getPocketbase } from "./sdk.js"; | ||||
| import type { Credentials } from "./config.ts"; | ||||
| import { sortBy } from 'es-toolkit' | ||||
| import type { CollectionModel } from 'pocketbase' | ||||
| import type { Credentials } from '@/config.ts' | ||||
| import { getPocketbase } from '@/sdk.js' | ||||
|  | ||||
| export async function fetchCollections(credentials: Credentials): Promise<CollectionModel[]> { | ||||
|   const pocketbase = await getPocketbase(credentials); | ||||
|   const collections = await pocketbase.collections.getFullList(); | ||||
|   return sortBy(collections, ["name"]); | ||||
| 	const pocketbase = await getPocketbase(credentials) | ||||
| 	const collections = await pocketbase.collections.getFullList() | ||||
| 	return sortBy(collections, ['name']) | ||||
| } | ||||
|   | ||||
							
								
								
									
										105
									
								
								tsconfig.json
									
									
									
									
									
								
							
							
						
						
									
										105
									
								
								tsconfig.json
									
									
									
									
									
								
							| @@ -1,52 +1,57 @@ | ||||
| { | ||||
|   "$schema": "https://json.schemastore.org/tsconfig", | ||||
|   "compilerOptions": { | ||||
|     // Enable top-level await, and other modern ESM features. | ||||
|     "target": "ESNext", | ||||
|     "module": "NodeNext", | ||||
|     // Enable module resolution without file extensions on relative paths, for things like npm package imports. | ||||
|     "moduleResolution": "nodenext", | ||||
|     // Allow importing TypeScript files using their native extension (.ts(x)). | ||||
|     "allowImportingTsExtensions": true, | ||||
|     // Enable JSON imports. | ||||
|     "resolveJsonModule": true, | ||||
|     // Enforce the usage of type-only imports when needed, which helps avoiding bundling issues. | ||||
|     "verbatimModuleSyntax": true, | ||||
|     // 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` | ||||
|     "isolatedModules": true, | ||||
|     // Astro directly run TypeScript code, no transpilation needed. | ||||
|     "noEmit": true, | ||||
|     // Report an error when importing a file using a casing different from another import of the same file. | ||||
|     "forceConsistentCasingInFileNames": true, | ||||
|     // Properly support importing CJS modules in ESM | ||||
|     "esModuleInterop": true, | ||||
|     // Skip typechecking libraries and .d.ts files | ||||
|     "skipLibCheck": true, | ||||
|     // Allow JavaScript files to be imported | ||||
|     "allowJs": true, | ||||
|     // Allow JSX files (or files that are internally considered JSX, like Astro files) to be imported inside `.js` and `.ts` files. | ||||
|     "jsx": "preserve", | ||||
|     // Enable strict mode. This enables a few options at a time, see https://www.typescriptlang.org/tsconfig#strict for a list. | ||||
|     "strict": true, | ||||
|     // Report errors for fallthrough cases in switch statements | ||||
|     "noFallthroughCasesInSwitch": true, | ||||
|     // Force functions designed to override their parent class to be specified as `override`. | ||||
|     "noImplicitOverride": true, | ||||
|     // Force functions to specify that they can return `undefined` if a possible code path does not return a value. | ||||
|     "noImplicitReturns": true, | ||||
|     // Report an error when a variable is declared but never used. | ||||
|     "noUnusedLocals": true, | ||||
|     // Report an error when a parameter is declared but never used. | ||||
|     "noUnusedParameters": true, | ||||
|     // Force the usage of the indexed syntax to access fields declared using an index signature. | ||||
|     "noUncheckedIndexedAccess": true, | ||||
|     // Report an error when the value `undefined` is given to an optional property that doesn't specify `undefined` as a valid value. | ||||
|     "exactOptionalPropertyTypes": true, | ||||
|     // Report an error for unreachable code instead of just a warning. | ||||
|     "allowUnreachableCode": false, | ||||
|     // Report an error for unused labels instead of just a warning. | ||||
|     "allowUnusedLabels": false | ||||
|   }, | ||||
|   "exclude": ["dist", "assets/stubs"] | ||||
| 	"$schema": "https://json.schemastore.org/tsconfig", | ||||
| 	"compilerOptions": { | ||||
| 		// Enable top-level await, and other modern ESM features. | ||||
| 		"target": "ESNext", | ||||
| 		"module": "NodeNext", | ||||
| 		// Enable module resolution without file extensions on relative paths, for things like npm package imports. | ||||
| 		"moduleResolution": "nodenext", | ||||
| 		// Allow importing TypeScript files using their native extension (.ts(x)). | ||||
| 		"allowImportingTsExtensions": true, | ||||
| 		// Enable JSON imports. | ||||
| 		"resolveJsonModule": true, | ||||
| 		// Enforce the usage of type-only imports when needed, which helps avoiding bundling issues. | ||||
| 		"verbatimModuleSyntax": true, | ||||
| 		// 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` | ||||
| 		"isolatedModules": true, | ||||
| 		// Astro directly run TypeScript code, no transpilation needed. | ||||
| 		"noEmit": true, | ||||
| 		// Report an error when importing a file using a casing different from another import of the same file. | ||||
| 		"forceConsistentCasingInFileNames": true, | ||||
| 		// Properly support importing CJS modules in ESM | ||||
| 		"esModuleInterop": true, | ||||
| 		// Skip typechecking libraries and .d.ts files | ||||
| 		"skipLibCheck": true, | ||||
| 		// Allow JavaScript files to be imported | ||||
| 		"allowJs": true, | ||||
| 		// Allow JSX files (or files that are internally considered JSX, like Astro files) to be imported inside `.js` and `.ts` files. | ||||
| 		"jsx": "preserve", | ||||
| 		// Enable strict mode. This enables a few options at a time, see https://www.typescriptlang.org/tsconfig#strict for a list. | ||||
| 		"strict": true, | ||||
| 		// Report errors for fallthrough cases in switch statements | ||||
| 		"noFallthroughCasesInSwitch": true, | ||||
| 		// Force functions designed to override their parent class to be specified as `override`. | ||||
| 		"noImplicitOverride": true, | ||||
| 		// Force functions to specify that they can return `undefined` if a possible code path does not return a value. | ||||
| 		"noImplicitReturns": true, | ||||
| 		// Report an error when a variable is declared but never used. | ||||
| 		"noUnusedLocals": true, | ||||
| 		// Report an error when a parameter is declared but never used. | ||||
| 		"noUnusedParameters": true, | ||||
| 		// Force the usage of the indexed syntax to access fields declared using an index signature. | ||||
| 		"noUncheckedIndexedAccess": true, | ||||
| 		// Report an error when the value `undefined` is given to an optional property that doesn't specify `undefined` as a valid value. | ||||
| 		"exactOptionalPropertyTypes": true, | ||||
| 		// Report an error for unreachable code instead of just a warning. | ||||
| 		"allowUnreachableCode": false, | ||||
| 		// Report an error for unused labels instead of just a warning. | ||||
| 		"allowUnusedLabels": false, | ||||
| 		"baseUrl": ".", | ||||
| 		"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 packageJson from "./package.json" with { type: "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(packageJson.peerDependencies), "dotenv"], | ||||
|     tsconfig: "tsconfig.json", | ||||
|   }; | ||||
| }); | ||||
		Reference in New Issue
	
	Block a user