├── packages ├── analyzer │ ├── src │ │ └── shims.d.ts │ ├── test │ │ ├── fixtures │ │ │ ├── .gitignore │ │ │ ├── pg-enum.mjs │ │ │ ├── schema.mjs │ │ │ ├── schema.ts │ │ │ ├── schema-indexes.mjs │ │ │ └── schema-indexes.cjs │ │ ├── analyzer.spec.ts │ │ ├── pg-enum.spec.ts │ │ ├── sqlite-types.spec.ts │ │ ├── gel-types.spec.ts │ │ ├── mysql-types.spec.ts │ │ ├── pg-types.spec.ts │ │ └── singlestore-types.spec.ts │ ├── tsconfig.json │ ├── CHANGELOG.md │ ├── package.json │ └── README.md ├── template-standard │ ├── src │ │ ├── shims.d.ts │ │ └── index.ts │ ├── tsconfig.json │ ├── test │ │ └── template.spec.ts │ ├── CHANGELOG.md │ ├── package.json │ └── README.md ├── generator-orpc │ ├── src │ │ ├── shims.d.ts │ │ └── analyzer-types.d.ts │ ├── tsconfig.json │ ├── package.json │ ├── CHANGELOG.md │ ├── README.md │ └── test │ │ └── generator.spec.ts ├── cli │ ├── tsconfig.json │ ├── src │ │ ├── shims.d.ts │ │ ├── sponsor.ts │ │ └── config.ts │ ├── test │ │ └── config.spec.ts │ ├── README.md │ ├── package.json │ └── CHANGELOG.md ├── generator-zod │ ├── tsconfig.json │ ├── package.json │ ├── CHANGELOG.md │ ├── README.md │ ├── test │ │ └── generator.spec.ts │ └── src │ │ └── index.ts ├── validation-core │ ├── tsconfig.json │ ├── CHANGELOG.md │ ├── package.json │ ├── README.md │ ├── test │ │ └── core.spec.ts │ └── src │ │ └── index.ts ├── generator-arktype │ ├── tsconfig.json │ ├── package.json │ ├── README.md │ ├── CHANGELOG.md │ ├── test │ │ └── generator.spec.ts │ └── src │ │ └── index.ts ├── generator-service │ ├── tsconfig.json │ ├── src │ │ └── analyzer-types.d.ts │ ├── package.json │ ├── CHANGELOG.md │ ├── README.md │ └── test │ │ └── generator.spec.ts ├── generator-valibot │ ├── tsconfig.json │ ├── package.json │ ├── CHANGELOG.md │ ├── README.md │ ├── test │ │ └── generator.spec.ts │ └── src │ │ └── index.ts └── template-orpc-service │ ├── tsconfig.json │ ├── CHANGELOG.md │ ├── test │ └── template.spec.ts │ ├── package.json │ ├── README.md │ └── src │ └── index.ts ├── docs ├── public │ ├── banner.png │ ├── icon-192.png │ ├── icon-512.png │ ├── brand │ │ ├── logo.png │ │ └── logo-dark.png │ ├── favicon-16.png │ ├── favicon-32.png │ ├── favicon-48.png │ ├── social-card.png │ └── apple-touch-icon.png ├── packages │ ├── analyzer.md │ └── validation-core.md ├── package.json ├── contributing.md ├── templates │ ├── standard.md │ ├── custom.md │ └── orpc-service.md ├── sponsor.md ├── roadmap │ └── premium-templates.md ├── cli │ ├── generate.md │ ├── init.md │ ├── watch.md │ ├── generate-orpc.md │ └── analyze.md ├── examples │ ├── relations.md │ └── validation-mix.md ├── adapters │ ├── overview.md │ └── router.md ├── index.md ├── generators │ ├── arktype.md │ ├── zod.md │ ├── valibot.md │ ├── service.md │ └── orpc.md ├── guide │ ├── configuration.md │ └── getting-started.md ├── .vitepress │ └── config.ts └── cli.md ├── types └── orpc-server.d.ts ├── assets └── brand │ └── source.jpeg ├── .eslintignore ├── pnpm-workspace.yaml ├── .npmrc ├── .prettierrc.json ├── .prettierignore ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ └── bug_report.yml ├── workflows │ ├── ci.yml │ ├── publish-dry-run.yml │ ├── docs.yml │ └── release.yml └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── tsconfig.base.json ├── .changeset └── config.json ├── .eslintrc.cjs ├── eslint.config.js ├── scripts ├── changeset-commit.cjs └── brand-gen.mjs ├── package.json ├── AGENTS.md ├── CONTRIBUTING.md └── README.md /packages/analyzer/src/shims.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'jiti'; 2 | -------------------------------------------------------------------------------- /packages/template-standard/src/shims.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@drzl/analyzer'; 2 | -------------------------------------------------------------------------------- /packages/analyzer/test/fixtures/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !pg-enum.mjs 4 | -------------------------------------------------------------------------------- /docs/public/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/use-drzl/drzl/HEAD/docs/public/banner.png -------------------------------------------------------------------------------- /types/orpc-server.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@orpc/server' { 2 | export const os: any; 3 | } 4 | -------------------------------------------------------------------------------- /assets/brand/source.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/use-drzl/drzl/HEAD/assets/brand/source.jpeg -------------------------------------------------------------------------------- /docs/public/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/use-drzl/drzl/HEAD/docs/public/icon-192.png -------------------------------------------------------------------------------- /docs/public/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/use-drzl/drzl/HEAD/docs/public/icon-512.png -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | real-world-example 2 | docs/.vitepress 3 | **/.wrangler 4 | **/.tmp-e2e 5 | **/*.mjs 6 | -------------------------------------------------------------------------------- /docs/public/brand/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/use-drzl/drzl/HEAD/docs/public/brand/logo.png -------------------------------------------------------------------------------- /docs/public/favicon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/use-drzl/drzl/HEAD/docs/public/favicon-16.png -------------------------------------------------------------------------------- /docs/public/favicon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/use-drzl/drzl/HEAD/docs/public/favicon-32.png -------------------------------------------------------------------------------- /docs/public/favicon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/use-drzl/drzl/HEAD/docs/public/favicon-48.png -------------------------------------------------------------------------------- /docs/public/social-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/use-drzl/drzl/HEAD/docs/public/social-card.png -------------------------------------------------------------------------------- /docs/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/use-drzl/drzl/HEAD/docs/public/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/public/brand/logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/use-drzl/drzl/HEAD/docs/public/brand/logo-dark.png -------------------------------------------------------------------------------- /packages/generator-orpc/src/shims.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'jiti'; 2 | declare module '@drzl/template-standard'; 3 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/* 3 | - docs 4 | 5 | onlyBuiltDependencies: 6 | - esbuild 7 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | always-auth=true 3 | @drzl:registry=https://registry.npmjs.org/ 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "printWidth": 100 6 | } 7 | -------------------------------------------------------------------------------- /packages/analyzer/test/fixtures/pg-enum.mjs: -------------------------------------------------------------------------------- 1 | export const statusEnum = { enumName: 'status', enumValues: ['draft','published','archived'] }; -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | packages/**/dist/ 4 | packages/**/test/tmp/ 5 | docs/.vitepress/cache/ 6 | docs/.vitepress/dist/ 7 | -------------------------------------------------------------------------------- /packages/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "dist" 6 | }, 7 | "include": ["src", "test"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/analyzer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "dist" 6 | }, 7 | "include": ["src", "test"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/generator-orpc/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "dist" 6 | }, 7 | "include": ["src", "test"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/generator-zod/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "dist" 6 | }, 7 | "include": ["src", "test"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/validation-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "dist" 6 | }, 7 | "include": ["src", "test"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/generator-arktype/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "dist" 6 | }, 7 | "include": ["src", "test"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/generator-service/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "dist" 6 | }, 7 | "include": ["src", "test"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/generator-valibot/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "dist" 6 | }, 7 | "include": ["src", "test"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/template-standard/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "dist" 6 | }, 7 | "include": ["src", "test"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/template-orpc-service/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "dist" 6 | }, 7 | "include": ["src", "test"] 8 | } 9 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: omar-dulaimi 2 | # You can add additional funding platforms below if desired 3 | # patreon: your-patreon-id 4 | # open_collective: your-collective 5 | # custom: ["https://buymeacoffee.com/yourid"] 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Questions / Help 4 | url: https://github.com/use-drzl/drzl/discussions 5 | about: Ask questions, share ideas, or get help in Discussions. 6 | -------------------------------------------------------------------------------- /docs/packages/analyzer.md: -------------------------------------------------------------------------------- 1 | # Analyzer 2 | 3 | Schema analyzer for Drizzle ORM projects. Produces a normalized `Analysis` used by generators. 4 | 5 | See the [package README](https://github.com/use-drzl/drzl/blob/master/packages/analyzer/README.md) for API and output shape. 6 | -------------------------------------------------------------------------------- /docs/packages/validation-core.md: -------------------------------------------------------------------------------- 1 | # Validation Core 2 | 3 | Shared interfaces and helpers for validation schema codegen across libraries. 4 | 5 | See the [package README](https://github.com/use-drzl/drzl/blob/master/packages/validation-core/README.md) for APIs and utilities. 6 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@drzl/docs", 3 | "private": true, 4 | "scripts": { 5 | "dev": "vitepress dev", 6 | "build": "vitepress build", 7 | "preview": "vitepress preview" 8 | }, 9 | "devDependencies": { 10 | "vitepress": "^1.6.4" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/analyzer/test/fixtures/schema.mjs: -------------------------------------------------------------------------------- 1 | const cols = Symbol.for('drizzle:Columns'); 2 | const name = Symbol.for('drizzle:Name'); 3 | const schemaSym = Symbol.for('drizzle:Schema'); 4 | const userId = { name: 'id', primary: true, notNull: true }; 5 | const email = { name: 'email', isUnique: true }; 6 | export const users = { [cols]: { id: userId, email }, [name]: 'users', [schemaSym]: 'main' }; 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | .turbo 5 | .idea 6 | .vscode 7 | .pnpm-store 8 | *.log 9 | # Test artifacts 10 | .vitest 11 | coverage 12 | packages/*/.vitest 13 | packages/*/coverage 14 | packages/*/test/tmp 15 | real-world-example 16 | 17 | # Local registry artifacts 18 | .verdaccio.pid 19 | .verdaccio.log 20 | 21 | # VitePress build/cache 22 | docs/.vitepress/cache 23 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for your interest in DRZL! Please read the full guide in [CONTRIBUTING.md](https://github.com/omar-dulaimi/drzl/blob/master/CONTRIBUTING.md). 4 | 5 | Quick checklist: 6 | 7 | - Fork and create a feature branch 8 | - Keep changes focused; follow existing patterns 9 | - Run tests: `pnpm -r test` and lint: `pnpm lint` 10 | - Open a PR with a clear description and rationale 11 | -------------------------------------------------------------------------------- /docs/templates/standard.md: -------------------------------------------------------------------------------- 1 | # Standard Template 2 | 3 | Minimal oRPC router template for quick scaffolding without service wiring. 4 | 5 | See the [package README](https://github.com/use-drzl/drzl/blob/master/packages/template-standard/README.md) for hooks and notes. 6 | 7 | ::: tip Need something else? 8 | If this template doesn't cover what you need, DM me on X (https://x.com/omardulaimidev) and we can scope it together. 9 | ::: 10 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2021", 4 | "module": "ESNext", 5 | "moduleResolution": "Bundler", 6 | "strict": true, 7 | "skipLibCheck": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "outDir": "dist", 11 | "esModuleInterop": true, 12 | "resolveJsonModule": true, 13 | "forceConsistentCasingInFileNames": true 14 | }, 15 | "include": ["packages", "types"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/cli/src/shims.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'cli-progress' { 2 | const cliProgress: any; 3 | export default cliProgress; 4 | } 5 | declare module '@drzl/generator-service' { 6 | export const ServiceGenerator: any; 7 | } 8 | declare module '@drzl/generator-zod' { 9 | export const ZodGenerator: any; 10 | } 11 | declare module '@drzl/generator-valibot' { 12 | export const ValibotGenerator: any; 13 | } 14 | declare module '@drzl/generator-arktype' { 15 | export const ArkTypeGenerator: any; 16 | } 17 | -------------------------------------------------------------------------------- /packages/analyzer/test/fixtures/schema.ts: -------------------------------------------------------------------------------- 1 | const cols = Symbol.for('drizzle:Columns'); 2 | const name = Symbol.for('drizzle:Name'); 3 | const schemaSym = Symbol.for('drizzle:Schema'); 4 | const PrimaryKey = Symbol.for('drizzle:PrimaryKey'); 5 | 6 | const userId = { name: 'id', primary: true, notNull: true, constructor: { name: 'SQLiteInteger' } }; 7 | const email = { name: 'email', isUnique: true, constructor: { name: 'SQLiteText' } }; 8 | const users: any = { [cols]: { id: userId, email }, [name]: 'users', [schemaSym]: 'main' }; 9 | 10 | export { users }; 11 | -------------------------------------------------------------------------------- /docs/sponsor.md: -------------------------------------------------------------------------------- 1 | # Sponsor 2 | 3 | If DRZL saves you time, consider sponsoring to help me keep improving it: 4 | 5 | - GitHub Sponsors: https://github.com/sponsors/omar-dulaimi 6 | - Perks: priority issues, roadmap votes, shout‑outs. 7 | - Premium/custom template requests? DM me directly on X: https://x.com/omardulaimidev 8 | - Paid work lands in this repo under Apache‑2.0—funding buys dev time, not exclusive ownership. 9 | - Want to fund a scoped task? Look for issues labeled `sponsor-wanted` (or DM me to create one) and we’ll reserve it for you. 10 | 11 | Thank you for your support! 12 | -------------------------------------------------------------------------------- /packages/analyzer/test/analyzer.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { describe, expect, it } from 'vitest'; 3 | import { SchemaAnalyzer } from '../src/index'; 4 | 5 | describe('SchemaAnalyzer', () => { 6 | const tmp = path.resolve(__dirname, 'fixtures'); 7 | 8 | it('reports missing schema file with an error', async () => { 9 | const analyzer = new SchemaAnalyzer(path.join(tmp, 'does-not-exist.ts')); 10 | const res = await analyzer.analyze(); 11 | expect(res.issues.some((i) => i.code === 'DRZL_ANL_NOFILE' && i.level === 'error')).toBe(true); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": ["../scripts/changeset-commit.cjs", { "skipCI": "version" }], 5 | "fixed": [], 6 | "linked": [["@drzl/generator-orpc", "@drzl/template-orpc-service", "@drzl/template-standard"]], 7 | "access": "public", 8 | "baseBranch": "master", 9 | "updateInternalDependencies": "patch", 10 | "bumpVersionsWithWorkspaceProtocolOnly": true, 11 | "ignore": [], 12 | "privatePackages": { 13 | "version": false, 14 | "tag": false 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /docs/roadmap/premium-templates.md: -------------------------------------------------------------------------------- 1 | # Premium Templates (Roadmap) 2 | 3 | We plan to offer premium adapter templates and deeper integrations as paid add‑ons. 4 | 5 | Have a specific stack or pattern in mind (e.g., tRPC, Express, Nest, Next.js, Prisma, auth, multi‑tenant)? 6 | 7 | **How to request a premium/custom template** 8 | 9 | - DM me on X: https://x.com/omardulaimidev (mention DRZL + the stack you need) 10 | 11 | Licensing & Access 12 | 13 | - Deliverables land in this repo under the same Apache‑2.0 license 14 | - Payments cover build/maintenance time; no exclusive ownership or private licensing 15 | -------------------------------------------------------------------------------- /packages/template-orpc-service/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @drzl/template-orpc-service 2 | 3 | ## 1.1.0 4 | 5 | ### Minor Changes 6 | 7 | - c48d79a: sponsor initiatives 8 | 9 | ## 1.0.0 10 | 11 | ### Major Changes 12 | 13 | - 5da6f6b: support MySQL, SingleStore, and Gel; expand Postgres/SQLite; add tests (fixes #13) 14 | 15 | ## 0.4.0 16 | 17 | ### Minor Changes 18 | 19 | - 811dd61: feat: strict database injection for services and oRPC middleware (typed db context; valibot v1 compatibility) 20 | 21 | ## 0.3.0 22 | 23 | ## 0.2.0 24 | 25 | ## 0.1.0 26 | 27 | ## 0.0.3 28 | 29 | ## 0.0.2 30 | 31 | ## 0.0.1 32 | -------------------------------------------------------------------------------- /packages/analyzer/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @drzl/analyzer 2 | 3 | ## 1.2.0 4 | 5 | ### Minor Changes 6 | 7 | - c48d79a: sponsor initiatives 8 | 9 | ## 1.1.0 10 | 11 | ### Minor Changes 12 | 13 | - 2ca4b77: Fix ArkType generator emitting double-wrapped enum strings; pgEnum unions now render with JSON-escaped literals so `drzl generate` succeeds even when 14 | 15 | ## 1.0.0 16 | 17 | ### Major Changes 18 | 19 | - 5da6f6b: support MySQL, SingleStore, and Gel; expand Postgres/SQLite; add tests (fixes #13) 20 | 21 | ## 0.3.0 22 | 23 | ## 0.2.0 24 | 25 | ## 0.1.0 26 | 27 | ## 0.0.3 28 | 29 | ## 0.0.2 30 | 31 | ## 0.0.1 32 | -------------------------------------------------------------------------------- /docs/cli/generate.md: -------------------------------------------------------------------------------- 1 | # Generate 2 | 3 | Run configured generators from `drzl.config.*`. 4 | 5 | Usage: 6 | 7 | ::: code-group 8 | 9 | ```bash [pnpm] 10 | pnpm dlx @drzl/cli generate -c drzl.config.ts 11 | ``` 12 | 13 | ```bash [npm] 14 | npx @drzl/cli generate -c drzl.config.ts 15 | ``` 16 | 17 | ```bash [yarn] 18 | yarn dlx @drzl/cli generate -c drzl.config.ts 19 | ``` 20 | 21 | ```bash [bun] 22 | bunx @drzl/cli generate -c drzl.config.ts 23 | ``` 24 | 25 | ::: 26 | 27 | Behavior: 28 | 29 | - Analyzes your schema then runs each generator in `generators[]` 30 | - Prints a file summary per generator kind 31 | 32 | See also: [Guide → Configuration](/guide/configuration) 33 | -------------------------------------------------------------------------------- /docs/cli/init.md: -------------------------------------------------------------------------------- 1 | # Init 2 | 3 | Scaffold a minimal `drzl.config.ts` in the current directory. 4 | 5 | Usage: 6 | 7 | ::: code-group 8 | 9 | ```bash [pnpm] 10 | pnpm dlx @drzl/cli init 11 | ``` 12 | 13 | ```bash [npm] 14 | npx @drzl/cli init 15 | ``` 16 | 17 | ```bash [yarn] 18 | yarn dlx @drzl/cli init 19 | ``` 20 | 21 | ```bash [bun] 22 | bunx @drzl/cli init 23 | ``` 24 | 25 | ::: 26 | 27 | Output example: 28 | 29 | ```ts 30 | export default { 31 | schema: 'src/db/schema.ts', 32 | outDir: 'src/api', 33 | analyzer: { includeRelations: true, validateConstraints: true }, 34 | generators: [{ kind: 'orpc', template: 'standard', includeRelations: true }], 35 | } as const; 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/examples/relations.md: -------------------------------------------------------------------------------- 1 | # Relations Example 2 | 3 | Enable `includeRelations` to generate relation endpoints in oRPC routers. 4 | 5 | ```ts 6 | export default defineConfig({ 7 | schema: 'src/db/schemas/index.ts', 8 | outDir: 'src/api', 9 | generators: [{ kind: 'orpc', includeRelations: true }], 10 | }); 11 | ``` 12 | 13 | This will add endpoints like `listByParentId`, `listChildren`, or similar, depending on your template’s `procedures` definitions. 14 | 15 | Tip: ensure foreign keys and joining table metadata are present in your Drizzle schema so analyzer can infer relations. 16 | 17 | ::: tip Need something else? 18 | If this example doesn't cover what you need, DM me on X (https://x.com/omardulaimidev) and we can scope it together. 19 | ::: 20 | -------------------------------------------------------------------------------- /packages/generator-orpc/src/analyzer-types.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@drzl/analyzer' { 2 | export interface Column { 3 | name: string; 4 | tsType: string; 5 | dbType: string; 6 | nullable: boolean; 7 | hasDefault: boolean; 8 | isGenerated: boolean; 9 | defaultExpression?: string; 10 | references?: { table: string; column: string; onDelete?: string; onUpdate?: string }; 11 | enumValues?: string[]; 12 | } 13 | export interface Table { 14 | name: string; 15 | tsName: string; 16 | schema?: string; 17 | columns: Column[]; 18 | } 19 | export interface Analysis { 20 | tables: Table[]; 21 | dialect?: string; 22 | enums?: any[]; 23 | relations?: any[]; 24 | issues?: any[]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/analyzer/test/fixtures/schema-indexes.mjs: -------------------------------------------------------------------------------- 1 | const COLUMNS = Symbol.for('drizzle:Columns'); 2 | const NAME = Symbol.for('drizzle:Name'); 3 | const EXTRA = Symbol.for('drizzle:ExtraConfigBuilder'); 4 | const users = { 5 | [COLUMNS]: { 6 | email: { name: 'email' }, 7 | username: { name: 'username' }, 8 | createdAt: { name: 'createdAt' }, 9 | }, 10 | [NAME]: 'users', 11 | }; 12 | users[EXTRA] = (t) => ({ 13 | uq_email_username: { 14 | config: { 15 | name: 'uq_email_username', 16 | unique: true, 17 | columns: [t[COLUMNS].email, t[COLUMNS].username], 18 | }, 19 | }, 20 | idx_created_at: { 21 | config: { name: 'idx_created_at', unique: false, columns: [t[COLUMNS].createdAt] }, 22 | }, 23 | }); 24 | export { users }; 25 | -------------------------------------------------------------------------------- /packages/generator-service/src/analyzer-types.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@drzl/analyzer' { 2 | export interface Column { 3 | name: string; 4 | tsType: string; 5 | dbType: string; 6 | nullable: boolean; 7 | hasDefault: boolean; 8 | isGenerated: boolean; 9 | defaultExpression?: string; 10 | references?: { table: string; column: string; onDelete?: string; onUpdate?: string }; 11 | enumValues?: string[]; 12 | } 13 | export interface Table { 14 | name: string; 15 | tsName: string; 16 | columns: Column[]; 17 | primaryKey?: { columns: string[] }; 18 | } 19 | export interface Analysis { 20 | tables: Table[]; 21 | dialect?: string; 22 | enums?: any[]; 23 | relations?: any[]; 24 | issues?: any[]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /docs/adapters/overview.md: -------------------------------------------------------------------------------- 1 | # Adapters (Overview) 2 | 3 | DRZL is adapter‑agnostic. Router generation is driven by adapter templates so you can target different stacks. 4 | 5 | Current support: 6 | 7 | - oRPC adapter (via generator-orpc and related templates) 8 | 9 | Planned/possible adapters (community interest welcome): 10 | 11 | - tRPC, Express, NestJS, Next.js, Prisma, and more 12 | 13 | How it works: 14 | 15 | - Adapters define a small template interface (hooks) that tell the generator how to name files, export router identifiers, inject imports/prelude, and render procedure code. 16 | - You can write custom templates to adapt to your runtime or conventions. 17 | 18 | See also: 19 | 20 | - [Router Adapters](/adapters/router) 21 | - [Custom Templates](/templates/custom) 22 | -------------------------------------------------------------------------------- /packages/analyzer/test/fixtures/schema-indexes.cjs: -------------------------------------------------------------------------------- 1 | const COLUMNS = Symbol.for('drizzle:Columns'); 2 | const NAME = Symbol.for('drizzle:Name'); 3 | const EXTRA = Symbol.for('drizzle:ExtraConfigBuilder'); 4 | const users = { 5 | [COLUMNS]: { 6 | email: { name: 'email' }, 7 | username: { name: 'username' }, 8 | createdAt: { name: 'createdAt' }, 9 | }, 10 | [NAME]: 'users', 11 | }; 12 | users[EXTRA] = (t) => ({ 13 | uq_email_username: { 14 | config: { 15 | name: 'uq_email_username', 16 | unique: true, 17 | columns: [t[COLUMNS].email, t[COLUMNS].username], 18 | }, 19 | }, 20 | idx_created_at: { 21 | config: { name: 'idx_created_at', unique: false, columns: [t[COLUMNS].createdAt] }, 22 | }, 23 | }); 24 | module.exports = { users }; 25 | -------------------------------------------------------------------------------- /packages/template-orpc-service/test/template.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import hooks from '../src/index'; 3 | import path from 'node:path'; 4 | 5 | describe('@drzl/template-orpc-service', () => { 6 | const table = { name: 'users', tsName: 'users' } as any; 7 | 8 | it('applies routerSuffix to routerName', () => { 9 | const r = hooks.routerName(table, { naming: { routerSuffix: 'Router' } }); 10 | expect(r).toBe('usersRouter'); 11 | }); 12 | 13 | it('imports path points to services dir relative to outDir', () => { 14 | const out = path.resolve('/tmp/api'); 15 | const imp = hooks.imports?.([table], { outDir: out, servicesDir: '/tmp/api/services' } as any); 16 | expect(imp).toContain("from 'services/userService'"); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | root: true, 4 | parser: '@typescript-eslint/parser', 5 | plugins: ['@typescript-eslint'], 6 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], 7 | ignorePatterns: ['dist', 'node_modules', 'packages/**/test/tmp', 'packages/**/test/fixtures'], 8 | parserOptions: { ecmaVersion: 2022, sourceType: 'module' }, 9 | overrides: [ 10 | { 11 | files: ['**/*.ts'], 12 | rules: { 13 | '@typescript-eslint/no-explicit-any': 'off', 14 | 'no-empty': 'off', 15 | '@typescript-eslint/no-unused-vars': [ 16 | 'warn', 17 | { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' }, 18 | ], 19 | }, 20 | }, 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /docs/cli/watch.md: -------------------------------------------------------------------------------- 1 | # Watch 2 | 3 | Watch schema (and template paths) and regenerate on changes. 4 | 5 | Usage: 6 | 7 | ::: code-group 8 | 9 | ```bash [pnpm] 10 | pnpm dlx @drzl/cli watch -c drzl.config.ts --pipeline all --debounce 200 [--json] 11 | ``` 12 | 13 | ```bash [npm] 14 | npx @drzl/cli watch -c drzl.config.ts --pipeline all --debounce 200 [--json] 15 | ``` 16 | 17 | ```bash [yarn] 18 | yarn dlx @drzl/cli watch -c drzl.config.ts --pipeline all --debounce 200 [--json] 19 | ``` 20 | 21 | ```bash [bun] 22 | bunx @drzl/cli watch -c drzl.config.ts --pipeline all --debounce 200 [--json] 23 | ``` 24 | 25 | ::: 26 | 27 | Options: 28 | 29 | - `-c, --config ` 30 | - `--pipeline `: `all | analyze | generate-orpc` (default `all`) 31 | - `--debounce `: debounce milliseconds (default `200`) 32 | - `--json`: emit structured JSON logs 33 | -------------------------------------------------------------------------------- /packages/analyzer/test/pg-enum.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { SchemaAnalyzer } from '../src/index'; 3 | import { promises as fs } from 'node:fs'; 4 | import path from 'node:path'; 5 | 6 | describe('Analyzer pgEnum detection', () => { 7 | it('detects enumName + enumValues export', async () => { 8 | const dir = path.resolve(__dirname, 'fixtures'); 9 | await fs.mkdir(dir, { recursive: true }); 10 | const schema = path.join(dir, 'pg-enum.mjs'); 11 | const code = `export const statusEnum = { enumName: 'status', enumValues: ['draft','published','archived'] };`; 12 | await fs.writeFile(schema, code, 'utf8'); 13 | const a = new SchemaAnalyzer(schema); 14 | const res = await a.analyze(); 15 | expect(res.enums.some((e) => e.name === 'status' && e.values.includes('draft'))).toBe(true); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/template-standard/test/template.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import hooks from '../src/index'; 3 | import os from 'node:os'; 4 | import path from 'node:path'; 5 | 6 | describe('@drzl/template-standard', () => { 7 | const table = { name: 'users', tsName: 'users' } as any; 8 | 9 | it('applies routerSuffix and kebab case to filePath', () => { 10 | const tmpDir = path.join(os.tmpdir(), 'drzl-template-'); 11 | const p = hooks.filePath(table, { 12 | outDir: tmpDir, 13 | naming: { routerSuffix: 'Router', procedureCase: 'kebab' }, 14 | }); 15 | expect(p.endsWith('/users-router.ts')).toBe(true); 16 | }); 17 | 18 | it('applies routerSuffix to routerName', () => { 19 | const r = hooks.routerName(table, { naming: { routerSuffix: 'Router' } }); 20 | expect(r).toBe('usersRouter'); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /docs/cli/generate-orpc.md: -------------------------------------------------------------------------------- 1 | # Generate (oRPC) 2 | 3 | Quickly generate oRPC routers without a config. 4 | 5 | Usage: 6 | 7 | ::: code-group 8 | 9 | ```bash [pnpm] 10 | pnpm dlx @drzl/cli generate:orpc -o src/api --template standard --includeRelations 11 | ``` 12 | 13 | ```bash [npm] 14 | npx @drzl/cli generate:orpc -o src/api --template standard --includeRelations 15 | ``` 16 | 17 | ```bash [yarn] 18 | yarn dlx @drzl/cli generate:orpc -o src/api --template standard --includeRelations 19 | ``` 20 | 21 | ```bash [bun] 22 | bunx @drzl/cli generate:orpc -o src/api --template standard --includeRelations 23 | ``` 24 | 25 | ::: 26 | 27 | Options: 28 | 29 | - `-o, --outDir ` (default `src/api`) 30 | - `--template ` (default `standard`) — can be `standard` or a custom path 31 | - `--includeRelations` — include relation endpoints 32 | -------------------------------------------------------------------------------- /docs/cli/analyze.md: -------------------------------------------------------------------------------- 1 | # Analyze 2 | 3 | Analyze a Drizzle schema (TypeScript) and output a normalized Analysis. 4 | 5 | Usage: 6 | 7 | ::: code-group 8 | 9 | ```bash [pnpm] 10 | pnpm dlx @drzl/cli analyze [--relations] [--validate] [--out FILE] [--json] 11 | ``` 12 | 13 | ```bash [npm] 14 | npx @drzl/cli analyze [--relations] [--validate] [--out FILE] [--json] 15 | ``` 16 | 17 | ```bash [yarn] 18 | yarn dlx @drzl/cli analyze [--relations] [--validate] [--out FILE] [--json] 19 | ``` 20 | 21 | ```bash [bun] 22 | bunx @drzl/cli analyze [--relations] [--validate] [--out FILE] [--json] 23 | ``` 24 | 25 | ::: 26 | 27 | Options: 28 | 29 | - `--relations` (default true): include relation inference 30 | - `--validate` (default true): validate constraints 31 | - `--out `: write JSON to file 32 | - `--json`: print JSON to stdout (overrides `--out`) 33 | 34 | Exits non‑zero when `issues` include any errors. 35 | -------------------------------------------------------------------------------- /packages/template-orpc-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@drzl/template-orpc-service", 3 | "version": "1.1.0", 4 | "private": false, 5 | "license": "Apache-2.0", 6 | "type": "module", 7 | "main": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "files": [ 10 | "dist" 11 | ], 12 | "sideEffects": false, 13 | "scripts": { 14 | "build": "tsup src/index.ts --dts --format esm,cjs", 15 | "lint": "eslint . --ext .ts", 16 | "test": "vitest run" 17 | }, 18 | "engines": { 19 | "node": ">=18.17.0" 20 | }, 21 | "publishConfig": { 22 | "access": "public", 23 | "registry": "https://registry.npmjs.org/", 24 | "provenance": true 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/use-drzl/drzl", 29 | "directory": "packages/template-orpc-service" 30 | }, 31 | "funding": { 32 | "type": "github", 33 | "url": "https://github.com/sponsors/omar-dulaimi" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/validation-core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @drzl/validation-core 2 | 3 | ## 1.1.0 4 | 5 | ### Minor Changes 6 | 7 | - c48d79a: sponsor initiatives 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies [c48d79a] 12 | - @drzl/analyzer@1.2.0 13 | 14 | ## 1.0.0 15 | 16 | ### Major Changes 17 | 18 | - 5da6f6b: support MySQL, SingleStore, and Gel; expand Postgres/SQLite; add tests (fixes #13) 19 | 20 | ### Patch Changes 21 | 22 | - Updated dependencies [5da6f6b] 23 | - @drzl/analyzer@1.0.0 24 | 25 | ## 0.3.0 26 | 27 | ### Patch Changes 28 | 29 | - @drzl/analyzer@0.3.0 30 | 31 | ## 0.2.0 32 | 33 | ### Patch Changes 34 | 35 | - @drzl/analyzer@0.2.0 36 | 37 | ## 0.1.0 38 | 39 | ### Patch Changes 40 | 41 | - @drzl/analyzer@0.1.0 42 | 43 | ## 0.0.3 44 | 45 | ### Patch Changes 46 | 47 | - @drzl/analyzer@0.0.3 48 | 49 | ## 0.0.2 50 | 51 | ### Patch Changes 52 | 53 | - @drzl/analyzer@0.0.2 54 | 55 | ## 0.0.1 56 | 57 | ### Patch Changes 58 | 59 | - @drzl/analyzer@0.0.1 60 | -------------------------------------------------------------------------------- /packages/template-standard/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @drzl/template-standard 2 | 3 | ## 1.1.0 4 | 5 | ### Minor Changes 6 | 7 | - c48d79a: sponsor initiatives 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies [c48d79a] 12 | - @drzl/analyzer@1.2.0 13 | 14 | ## 1.0.0 15 | 16 | ### Major Changes 17 | 18 | - 5da6f6b: support MySQL, SingleStore, and Gel; expand Postgres/SQLite; add tests (fixes #13) 19 | 20 | ### Patch Changes 21 | 22 | - Updated dependencies [5da6f6b] 23 | - @drzl/analyzer@1.0.0 24 | 25 | ## 0.3.0 26 | 27 | ### Patch Changes 28 | 29 | - @drzl/analyzer@0.3.0 30 | 31 | ## 0.2.0 32 | 33 | ### Patch Changes 34 | 35 | - @drzl/analyzer@0.2.0 36 | 37 | ## 0.1.0 38 | 39 | ### Patch Changes 40 | 41 | - @drzl/analyzer@0.1.0 42 | 43 | ## 0.0.3 44 | 45 | ### Patch Changes 46 | 47 | - @drzl/analyzer@0.0.3 48 | 49 | ## 0.0.2 50 | 51 | ### Patch Changes 52 | 53 | - @drzl/analyzer@0.0.2 54 | 55 | ## 0.0.1 56 | 57 | ### Patch Changes 58 | 59 | - @drzl/analyzer@0.0.1 60 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // ESLint v9 flat config 2 | import tseslint from '@typescript-eslint/eslint-plugin'; 3 | import tsparser from '@typescript-eslint/parser'; 4 | 5 | export default [ 6 | { 7 | ignores: [ 8 | 'node_modules/**', 9 | 'dist/**', 10 | 'packages/**/dist/**', 11 | 'packages/**/test/tmp/**', 12 | 'packages/**/test/fixtures/**', 13 | 'docs/.vitepress/**', 14 | 'real-world-example/**', 15 | '**/.tmp-e2e/**', 16 | '**/.wrangler/**', 17 | ], 18 | }, 19 | { 20 | files: ['**/*.ts'], 21 | languageOptions: { 22 | parser: tsparser, 23 | ecmaVersion: 2022, 24 | sourceType: 'module', 25 | }, 26 | plugins: { 27 | '@typescript-eslint': tseslint, 28 | }, 29 | rules: { 30 | '@typescript-eslint/no-explicit-any': 'off', 31 | 'no-empty': 'off', 32 | '@typescript-eslint/no-unused-vars': [ 33 | 'warn', 34 | { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' }, 35 | ], 36 | }, 37 | }, 38 | ]; 39 | -------------------------------------------------------------------------------- /packages/validation-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@drzl/validation-core", 3 | "version": "1.1.0", 4 | "private": false, 5 | "license": "Apache-2.0", 6 | "type": "module", 7 | "main": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "files": [ 10 | "dist" 11 | ], 12 | "sideEffects": false, 13 | "scripts": { 14 | "build": "tsup src/index.ts --dts --format esm,cjs", 15 | "test": "vitest run" 16 | }, 17 | "dependencies": { 18 | "@drzl/analyzer": "workspace:^" 19 | }, 20 | "devDependencies": { 21 | "tsup": "^8.5.0", 22 | "typescript": "^5.9.2" 23 | }, 24 | "engines": { 25 | "node": ">=18.17.0" 26 | }, 27 | "publishConfig": { 28 | "access": "public", 29 | "registry": "https://registry.npmjs.org/", 30 | "provenance": true 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/use-drzl/drzl", 35 | "directory": "packages/validation-core" 36 | }, 37 | "funding": { 38 | "type": "github", 39 | "url": "https://github.com/sponsors/omar-dulaimi" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /docs/examples/validation-mix.md: -------------------------------------------------------------------------------- 1 | # Validation Mix Example 2 | 3 | You can mix validation libraries across generators or reuse shared schemas in oRPC. 4 | 5 | ## Separate validators, shared in oRPC 6 | 7 | ```ts 8 | export default defineConfig({ 9 | schema: 'src/db/schemas/index.ts', 10 | outDir: 'src/api', 11 | generators: [ 12 | { kind: 'zod', path: 'src/validators/zod', schemaSuffix: 'Schema' }, 13 | { kind: 'valibot', path: 'src/validators/valibot', schemaSuffix: 'Schema' }, 14 | { 15 | kind: 'orpc', 16 | template: '@drzl/template-orpc-service', 17 | validation: { 18 | useShared: true, 19 | library: 'zod', 20 | importPath: 'src/validators/zod', 21 | schemaSuffix: 'Schema', 22 | }, 23 | }, 24 | ], 25 | }); 26 | ``` 27 | 28 | ## Switch libraries 29 | 30 | Change `validation.library` to `valibot` or `arktype` and the generator will adapt input/output wiring accordingly. 31 | 32 | ::: tip Need something else? 33 | If this example doesn't cover what you need, DM me on X (https://x.com/omardulaimidev) and we can scope it together. 34 | ::: 35 | -------------------------------------------------------------------------------- /packages/generator-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@drzl/generator-service", 3 | "version": "1.1.0", 4 | "private": false, 5 | "license": "Apache-2.0", 6 | "type": "module", 7 | "main": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "files": [ 10 | "dist" 11 | ], 12 | "sideEffects": false, 13 | "scripts": { 14 | "build": "tsup src/index.ts --dts --format esm,cjs", 15 | "lint": "eslint . --ext .ts", 16 | "test": "vitest run" 17 | }, 18 | "dependencies": { 19 | "@drzl/analyzer": "workspace:^" 20 | }, 21 | "devDependencies": { 22 | "tsup": "^8.5.0", 23 | "typescript": "^5.9.2" 24 | }, 25 | "engines": { 26 | "node": ">=18.17.0" 27 | }, 28 | "publishConfig": { 29 | "access": "public", 30 | "registry": "https://registry.npmjs.org/", 31 | "provenance": true 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "https://github.com/use-drzl/drzl", 36 | "directory": "packages/generator-service" 37 | }, 38 | "funding": { 39 | "type": "github", 40 | "url": "https://github.com/sponsors/omar-dulaimi" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/template-standard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@drzl/template-standard", 3 | "version": "1.1.0", 4 | "private": false, 5 | "license": "Apache-2.0", 6 | "type": "module", 7 | "main": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "files": [ 10 | "dist" 11 | ], 12 | "sideEffects": false, 13 | "scripts": { 14 | "build": "tsup src/index.ts --dts --format esm,cjs", 15 | "lint": "eslint . --ext .ts", 16 | "test": "vitest run" 17 | }, 18 | "dependencies": { 19 | "@drzl/analyzer": "workspace:^" 20 | }, 21 | "devDependencies": { 22 | "tsup": "^8.5.0", 23 | "typescript": "^5.9.2" 24 | }, 25 | "engines": { 26 | "node": ">=18.17.0" 27 | }, 28 | "publishConfig": { 29 | "access": "public", 30 | "registry": "https://registry.npmjs.org/", 31 | "provenance": true 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "https://github.com/use-drzl/drzl", 36 | "directory": "packages/template-standard" 37 | }, 38 | "funding": { 39 | "type": "github", 40 | "url": "https://github.com/sponsors/omar-dulaimi" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/generator-zod/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@drzl/generator-zod", 3 | "version": "1.1.0", 4 | "private": false, 5 | "license": "Apache-2.0", 6 | "type": "module", 7 | "main": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "files": [ 10 | "dist" 11 | ], 12 | "sideEffects": false, 13 | "scripts": { 14 | "build": "tsup src/index.ts --dts --format esm,cjs", 15 | "lint": "eslint . --ext .ts", 16 | "test": "vitest run" 17 | }, 18 | "dependencies": { 19 | "@drzl/analyzer": "workspace:^", 20 | "@drzl/validation-core": "workspace:^" 21 | }, 22 | "devDependencies": { 23 | "tsup": "^8.5.0", 24 | "typescript": "^5.9.2" 25 | }, 26 | "engines": { 27 | "node": ">=18.17.0" 28 | }, 29 | "publishConfig": { 30 | "access": "public", 31 | "registry": "https://registry.npmjs.org/", 32 | "provenance": true 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "https://github.com/use-drzl/drzl", 37 | "directory": "packages/generator-zod" 38 | }, 39 | "funding": { 40 | "type": "github", 41 | "url": "https://github.com/sponsors/omar-dulaimi" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | paths-ignore: 5 | - 'docs/**' 6 | pull_request: 7 | jobs: 8 | build-test-lint: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | - uses: pnpm/action-setup@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: 20 18 | cache: 'pnpm' 19 | - run: pnpm install --frozen-lockfile 20 | - run: pnpm build 21 | - run: pnpm -r test 22 | - run: pnpm lint 23 | 24 | changeset-check: 25 | runs-on: ubuntu-latest 26 | if: github.event_name == 'pull_request' 27 | steps: 28 | - uses: actions/checkout@v4 29 | with: 30 | fetch-depth: 0 31 | - name: Fetch master branch 32 | run: git fetch origin master:master 33 | - uses: pnpm/action-setup@v4 34 | - uses: actions/setup-node@v4 35 | with: 36 | node-version: 20 37 | cache: 'pnpm' 38 | - run: pnpm install --frozen-lockfile 39 | - name: Check for changeset 40 | run: pnpm changeset status --verbose 41 | -------------------------------------------------------------------------------- /packages/generator-service/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @drzl/generator-service 2 | 3 | ## 1.1.0 4 | 5 | ### Minor Changes 6 | 7 | - c48d79a: sponsor initiatives 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies [c48d79a] 12 | - @drzl/analyzer@1.2.0 13 | 14 | ## 1.0.0 15 | 16 | ### Major Changes 17 | 18 | - 5da6f6b: support MySQL, SingleStore, and Gel; expand Postgres/SQLite; add tests (fixes #13) 19 | 20 | ### Patch Changes 21 | 22 | - Updated dependencies [5da6f6b] 23 | - @drzl/analyzer@1.0.0 24 | 25 | ## 0.4.0 26 | 27 | ### Minor Changes 28 | 29 | - 811dd61: feat: strict database injection for services and oRPC middleware (typed db context; valibot v1 compatibility) 30 | 31 | ## 0.3.0 32 | 33 | ### Patch Changes 34 | 35 | - @drzl/analyzer@0.3.0 36 | 37 | ## 0.2.0 38 | 39 | ### Patch Changes 40 | 41 | - @drzl/analyzer@0.2.0 42 | 43 | ## 0.1.0 44 | 45 | ### Patch Changes 46 | 47 | - @drzl/analyzer@0.1.0 48 | 49 | ## 0.0.3 50 | 51 | ### Patch Changes 52 | 53 | - @drzl/analyzer@0.0.3 54 | 55 | ## 0.0.2 56 | 57 | ### Patch Changes 58 | 59 | - @drzl/analyzer@0.0.2 60 | 61 | ## 0.0.1 62 | 63 | ### Patch Changes 64 | 65 | - @drzl/analyzer@0.0.1 66 | -------------------------------------------------------------------------------- /packages/generator-arktype/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@drzl/generator-arktype", 3 | "version": "1.2.0", 4 | "private": false, 5 | "license": "Apache-2.0", 6 | "type": "module", 7 | "main": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "files": [ 10 | "dist" 11 | ], 12 | "sideEffects": false, 13 | "scripts": { 14 | "build": "tsup src/index.ts --dts --format esm,cjs", 15 | "lint": "eslint . --ext .ts", 16 | "test": "vitest run" 17 | }, 18 | "dependencies": { 19 | "@drzl/analyzer": "workspace:^", 20 | "@drzl/validation-core": "workspace:^" 21 | }, 22 | "devDependencies": { 23 | "tsup": "^8.5.0", 24 | "typescript": "^5.9.2" 25 | }, 26 | "engines": { 27 | "node": ">=18.17.0" 28 | }, 29 | "publishConfig": { 30 | "access": "public", 31 | "registry": "https://registry.npmjs.org/", 32 | "provenance": true 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "https://github.com/use-drzl/drzl", 37 | "directory": "packages/generator-arktype" 38 | }, 39 | "funding": { 40 | "type": "github", 41 | "url": "https://github.com/sponsors/omar-dulaimi" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/generator-valibot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@drzl/generator-valibot", 3 | "version": "1.1.0", 4 | "private": false, 5 | "license": "Apache-2.0", 6 | "type": "module", 7 | "main": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "files": [ 10 | "dist" 11 | ], 12 | "sideEffects": false, 13 | "scripts": { 14 | "build": "tsup src/index.ts --dts --format esm,cjs", 15 | "lint": "eslint . --ext .ts", 16 | "test": "vitest run" 17 | }, 18 | "dependencies": { 19 | "@drzl/analyzer": "workspace:^", 20 | "@drzl/validation-core": "workspace:^" 21 | }, 22 | "devDependencies": { 23 | "tsup": "^8.5.0", 24 | "typescript": "^5.9.2" 25 | }, 26 | "engines": { 27 | "node": ">=18.17.0" 28 | }, 29 | "publishConfig": { 30 | "access": "public", 31 | "registry": "https://registry.npmjs.org/", 32 | "provenance": true 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "https://github.com/use-drzl/drzl", 37 | "directory": "packages/generator-valibot" 38 | }, 39 | "funding": { 40 | "type": "github", 41 | "url": "https://github.com/sponsors/omar-dulaimi" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | title: DRZL 4 | hero: 5 | # name: DRZL 6 | text: Developer tooling for Drizzle ORM 7 | tagline: Analyze schemas. Generate services, routers (adapter-based), and validation. 8 | image: 9 | light: /brand/logo.png 10 | dark: /brand/logo-dark.png 11 | alt: DRZL logo 12 | actions: 13 | - theme: brand 14 | text: Get Started 15 | link: /guide/getting-started 16 | - theme: alt 17 | text: CLI 18 | link: /cli 19 | features: 20 | - title: Schema Analyzer 21 | details: Normalize Drizzle schemas into a portable Analysis for generators. 22 | - title: Generators 23 | details: Routers (adapter-based; currently oRPC), typed services (with serverless-friendly database injection), and validation schemas (Zod, Valibot, ArkType). 24 | - title: Templates 25 | details: Adapter templates for quick scaffolding or service wiring. Request custom templates as a paid service. 26 | --- 27 | 28 | ## Funded Features 29 | 30 | - _None yet — be the first!_ Need a template, generator, or adapter that doesn’t exist yet? DM me on X (https://x.com/omardulaimidev) to fund it. All funded work ships back into DRZL under Apache‑2.0. 31 | -------------------------------------------------------------------------------- /packages/cli/test/config.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { loadConfig, defineConfig } from '../src/config'; 3 | import { promises as fs } from 'node:fs'; 4 | import path from 'node:path'; 5 | import os from 'node:os'; 6 | 7 | describe('@drzl/cli config', () => { 8 | it('defineConfig returns shape', () => { 9 | const cfg = defineConfig({ 10 | schema: 'x', 11 | generators: [{ kind: 'orpc' }], 12 | outDir: 'out', 13 | analyzer: { includeRelations: true, validateConstraints: true }, 14 | } as any); 15 | expect(cfg.schema).toBe('x'); 16 | }); 17 | 18 | it('loadConfig reads JSON and applies defaults', async () => { 19 | const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'drzl-cli-')); 20 | const tmp = path.join(tmpDir, 'tmp.config.json'); 21 | try { 22 | await fs.writeFile( 23 | tmp, 24 | JSON.stringify({ schema: 'x', generators: [{ kind: 'orpc' }] }), 25 | 'utf8' 26 | ); 27 | const cfg = await loadConfig(tmp); 28 | expect(cfg?.analyzer?.includeRelations).toBe(true); 29 | } finally { 30 | await fs.rm(tmpDir, { recursive: true, force: true }); 31 | } 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | 4 | 5 | Fixes # 6 | 7 | ## Type of change 8 | 9 | - [ ] feat: new feature 10 | - [ ] fix: bug fix 11 | - [ ] docs: documentation only 12 | - [ ] refactor: code change with no behavior change 13 | - [ ] chore: tooling/build/deps 14 | - [ ] perf: performance improvement 15 | 16 | ## Screenshots / Demos (optional) 17 | 18 | 19 | 20 | ## Checklist 21 | 22 | - [ ] I read the docs and [CONTRIBUTING.md](../CONTRIBUTING.md) 23 | - [ ] I searched existing [Issues](../issues) and [Discussions](../discussions) 24 | - [ ] I’m on the latest version of DRZL packages 25 | - [ ] I added/updated tests 26 | - [ ] I updated docs/readmes where appropriate 27 | - [ ] I ran `pnpm -r test` locally 28 | - [ ] I ran `pnpm lint` locally and fixed any issues 29 | - [ ] I linked related issues (Fixes/Closes #) 30 | - [ ] I have starred the repository ⭐ 31 | - [ ] I’m willing to sponsor this work 💖 (optional) 32 | 33 | ## Breaking changes 34 | 35 | - [ ] No 36 | - [ ] Yes (describe migration notes below) 37 | 38 | Migration notes: 39 | 40 | ``` 41 | 42 | ``` 43 | -------------------------------------------------------------------------------- /packages/generator-orpc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@drzl/generator-orpc", 3 | "version": "1.1.0", 4 | "private": false, 5 | "license": "Apache-2.0", 6 | "type": "module", 7 | "main": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "files": [ 10 | "dist" 11 | ], 12 | "sideEffects": false, 13 | "scripts": { 14 | "build": "tsup src/index.ts --dts --format esm,cjs", 15 | "lint": "eslint . --ext .ts", 16 | "test": "vitest run" 17 | }, 18 | "dependencies": { 19 | "@drzl/analyzer": "workspace:^", 20 | "@drzl/template-orpc-service": "workspace:^", 21 | "@drzl/template-standard": "workspace:^", 22 | "zod": "^4.1.5" 23 | }, 24 | "devDependencies": { 25 | "tsup": "^8.5.0", 26 | "typescript": "^5.9.2" 27 | }, 28 | "engines": { 29 | "node": ">=18.17.0" 30 | }, 31 | "publishConfig": { 32 | "access": "public", 33 | "registry": "https://registry.npmjs.org/", 34 | "provenance": true 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "https://github.com/use-drzl/drzl", 39 | "directory": "packages/generator-orpc" 40 | }, 41 | "funding": { 42 | "type": "github", 43 | "url": "https://github.com/sponsors/omar-dulaimi" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/analyzer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@drzl/analyzer", 3 | "version": "1.2.0", 4 | "private": false, 5 | "license": "Apache-2.0", 6 | "type": "module", 7 | "main": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "exports": { 10 | ".": { 11 | "types": "./dist/index.d.ts", 12 | "import": "./dist/index.js", 13 | "require": "./dist/index.cjs" 14 | } 15 | }, 16 | "files": [ 17 | "dist" 18 | ], 19 | "sideEffects": false, 20 | "scripts": { 21 | "build": "tsup src/index.ts --dts --format esm,cjs", 22 | "lint": "eslint . --ext .ts", 23 | "test": "vitest run" 24 | }, 25 | "dependencies": { 26 | "jiti": "^2.5.1" 27 | }, 28 | "devDependencies": { 29 | "tsup": "^8.5.0", 30 | "typescript": "^5.9.2" 31 | }, 32 | "engines": { 33 | "node": ">=18.17.0" 34 | }, 35 | "publishConfig": { 36 | "access": "public", 37 | "registry": "https://registry.npmjs.org/", 38 | "provenance": true 39 | }, 40 | "repository": { 41 | "type": "git", 42 | "url": "https://github.com/use-drzl/drzl", 43 | "directory": "packages/analyzer" 44 | }, 45 | "funding": { 46 | "type": "github", 47 | "url": "https://github.com/sponsors/omar-dulaimi" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/generator-zod/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @drzl/generator-zod 2 | 3 | ## 1.1.0 4 | 5 | ### Minor Changes 6 | 7 | - c48d79a: sponsor initiatives 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies [c48d79a] 12 | - @drzl/validation-core@1.1.0 13 | - @drzl/analyzer@1.2.0 14 | 15 | ## 1.0.0 16 | 17 | ### Major Changes 18 | 19 | - 5da6f6b: support MySQL, SingleStore, and Gel; expand Postgres/SQLite; add tests (fixes #13) 20 | 21 | ### Patch Changes 22 | 23 | - Updated dependencies [5da6f6b] 24 | - @drzl/analyzer@1.0.0 25 | - @drzl/validation-core@1.0.0 26 | 27 | ## 0.3.0 28 | 29 | ### Patch Changes 30 | 31 | - @drzl/analyzer@0.3.0 32 | - @drzl/validation-core@0.3.0 33 | 34 | ## 0.2.0 35 | 36 | ### Patch Changes 37 | 38 | - @drzl/analyzer@0.2.0 39 | - @drzl/validation-core@0.2.0 40 | 41 | ## 0.1.0 42 | 43 | ### Patch Changes 44 | 45 | - @drzl/analyzer@0.1.0 46 | - @drzl/validation-core@0.1.0 47 | 48 | ## 0.0.3 49 | 50 | ### Patch Changes 51 | 52 | - @drzl/analyzer@0.0.3 53 | - @drzl/validation-core@0.0.3 54 | 55 | ## 0.0.2 56 | 57 | ### Patch Changes 58 | 59 | - @drzl/analyzer@0.0.2 60 | - @drzl/validation-core@0.0.2 61 | 62 | ## 0.0.1 63 | 64 | ### Patch Changes 65 | 66 | - @drzl/analyzer@0.0.1 67 | - @drzl/validation-core@0.0.1 68 | -------------------------------------------------------------------------------- /packages/generator-valibot/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @drzl/generator-valibot 2 | 3 | ## 1.1.0 4 | 5 | ### Minor Changes 6 | 7 | - c48d79a: sponsor initiatives 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies [c48d79a] 12 | - @drzl/validation-core@1.1.0 13 | - @drzl/analyzer@1.2.0 14 | 15 | ## 1.0.0 16 | 17 | ### Major Changes 18 | 19 | - 5da6f6b: support MySQL, SingleStore, and Gel; expand Postgres/SQLite; add tests (fixes #13) 20 | 21 | ### Patch Changes 22 | 23 | - Updated dependencies [5da6f6b] 24 | - @drzl/analyzer@1.0.0 25 | - @drzl/validation-core@1.0.0 26 | 27 | ## 0.3.0 28 | 29 | ### Patch Changes 30 | 31 | - @drzl/analyzer@0.3.0 32 | - @drzl/validation-core@0.3.0 33 | 34 | ## 0.2.0 35 | 36 | ### Patch Changes 37 | 38 | - @drzl/analyzer@0.2.0 39 | - @drzl/validation-core@0.2.0 40 | 41 | ## 0.1.0 42 | 43 | ### Patch Changes 44 | 45 | - @drzl/analyzer@0.1.0 46 | - @drzl/validation-core@0.1.0 47 | 48 | ## 0.0.3 49 | 50 | ### Patch Changes 51 | 52 | - @drzl/analyzer@0.0.3 53 | - @drzl/validation-core@0.0.3 54 | 55 | ## 0.0.2 56 | 57 | ### Patch Changes 58 | 59 | - @drzl/analyzer@0.0.2 60 | - @drzl/validation-core@0.0.2 61 | 62 | ## 0.0.1 63 | 64 | ### Patch Changes 65 | 66 | - @drzl/analyzer@0.0.1 67 | - @drzl/validation-core@0.0.1 68 | -------------------------------------------------------------------------------- /.github/workflows/publish-dry-run.yml: -------------------------------------------------------------------------------- 1 | name: Publish Dry Run 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | tag: 7 | description: Optional npm dist-tag 8 | required: false 9 | default: latest 10 | 11 | jobs: 12 | dry-run: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Setup Node 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: 22 24 | registry-url: https://registry.npmjs.org 25 | 26 | - name: Setup pnpm 27 | uses: pnpm/action-setup@v4 28 | 29 | - name: Install deps 30 | run: pnpm -w i --frozen-lockfile 31 | 32 | - name: Build 33 | run: pnpm -w build 34 | 35 | - name: Changesets publish (dry run) 36 | env: 37 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 38 | NPM_CONFIG_DRY_RUN: 'true' 39 | # Ensure npm registry points to npmjs, regardless of user-level npmrc 40 | NPM_CONFIG_REGISTRY: https://registry.npmjs.org 41 | run: | 42 | pnpm exec changeset publish --no-git-tag ${ { github.event.inputs.tag && format('--tag {0}', github.event.inputs.tag) || '' } } 43 | -------------------------------------------------------------------------------- /docs/templates/custom.md: -------------------------------------------------------------------------------- 1 | # Custom Templates 2 | 3 | You can provide a custom oRPC template by passing a module path to `template`. 4 | 5 | ```ts 6 | export default defineConfig({ 7 | generators: [ 8 | { 9 | kind: 'orpc', 10 | template: './src/templates/my-orpc-template.ts', 11 | naming: { routerSuffix: 'Router', procedureCase: 'camel' }, 12 | }, 13 | ], 14 | }); 15 | ``` 16 | 17 | Your template should export `ORPCTemplateHooks`: 18 | 19 | ```ts 20 | import type { ORPCTemplateHooks } from '@drzl/generator-orpc'; 21 | 22 | const template: ORPCTemplateHooks = { 23 | filePath: (table, ctx) => `${ctx.outDir}/${table.tsName}.ts`, 24 | routerName: (table) => `${table.tsName}Router`, 25 | imports: () => `import { os } from '@orpc/server'`, 26 | prelude: () => `// helpers`, 27 | header: (table) => `// Router for table: ${table.name}`, 28 | procedures: (table) => [ 29 | { 30 | name: 'list', 31 | varName: `list${table.tsName}`, 32 | code: `const list${table.tsName} = os.handler(async () => [])`, 33 | }, 34 | ], 35 | }; 36 | 37 | export default template; 38 | ``` 39 | 40 | See also: [Template Hooks API](/generators/orpc#template-hooks-api) 41 | 42 | ::: tip Need something else? 43 | If this template doesn't cover what you need, DM me on X (https://x.com/omardulaimidev) and we can scope it together. 44 | ::: 45 | -------------------------------------------------------------------------------- /packages/generator-zod/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # @drzl/generator-zod 4 | 5 |
6 | 7 | [![CI](https://github.com/use-drzl/drzl/actions/workflows/ci.yml/badge.svg)](https://github.com/use-drzl/drzl/actions/workflows/ci.yml) 8 | [![npm](https://img.shields.io/npm/v/%40drzl%2Fgenerator-zod)](https://www.npmjs.com/package/@drzl/generator-zod) 9 | 10 |
11 | 12 | Zod schemas from your Drizzle analysis (insert / update / select). 13 | 14 |
15 | 16 | ## 💚 Sponsor DRZL 17 | 18 |
19 | 20 | DRZL is crafted nights & weekends. Sponsorships keep the generators fast, tested, and free. 21 | 22 | [![Sponsor DRZL](https://img.shields.io/badge/GitHub%20Sponsors-Support%20the%20project-ff69b4?logo=github)](https://github.com/sponsors/omar-dulaimi) 23 | 24 |
25 | 26 | - Every dollar speeds up CI hardware and offsets long test runs on my aging laptop. 27 | - Sponsors get roadmap input and priority responses in GitHub Issues. 28 | - Prefer a quick overview? Check `docs/sponsor.md` for the current goals and thank-yous. 29 | 30 | ## Use 31 | 32 | Add to `drzl.config.ts`: 33 | 34 | ```ts 35 | generators: [{ kind: 'zod', path: 'src/validators/zod' }]; 36 | ``` 37 | 38 | ## Output 39 | 40 | - `InsertSchema`, `Update
Schema`, `Select
Schema` 41 | - Optional `index` barrel 42 | - Shared vs inlined schemas supported 43 | -------------------------------------------------------------------------------- /packages/generator-valibot/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # @drzl/generator-valibot 4 | 5 |
6 | 7 | [![CI](https://github.com/use-drzl/drzl/actions/workflows/ci.yml/badge.svg)](https://github.com/use-drzl/drzl/actions/workflows/ci.yml) 8 | [![npm](https://img.shields.io/npm/v/%40drzl%2Fgenerator-valibot)](https://www.npmjs.com/package/@drzl/generator-valibot) 9 | 10 |
11 | 12 | Valibot schemas from your Drizzle analysis (insert / update / select). 13 | 14 |
15 | 16 | ## 💚 Sponsor DRZL 17 | 18 |
19 | 20 | DRZL is crafted nights & weekends. Sponsorships keep the generators fast, tested, and free. 21 | 22 | [![Sponsor DRZL](https://img.shields.io/badge/GitHub%20Sponsors-Support%20the%20project-ff69b4?logo=github)](https://github.com/sponsors/omar-dulaimi) 23 | 24 |
25 | 26 | - Every dollar speeds up CI hardware and offsets long test runs on my aging laptop. 27 | - Sponsors get roadmap input and priority responses in GitHub Issues. 28 | - Prefer a quick overview? Check `docs/sponsor.md` for the current goals and thank-yous. 29 | 30 | ## Use 31 | 32 | Add to `drzl.config.ts`: 33 | 34 | ```ts 35 | generators: [{ kind: 'valibot', path: 'src/validators/valibot' }]; 36 | ``` 37 | 38 | ## Output 39 | 40 | - `Insert
Schema`, `Update
Schema`, `Select
Schema` 41 | - Optional `index` barrel 42 | - Shared vs inlined schemas supported 43 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | paths: 7 | - 'docs/**' 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: read 12 | pages: write 13 | id-token: write 14 | 15 | concurrency: 16 | group: pages 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | - name: Setup pnpm 26 | uses: pnpm/action-setup@v4 27 | - name: Setup Node 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: 20 31 | cache: 'pnpm' 32 | - name: Install deps 33 | run: pnpm install --frozen-lockfile=false 34 | - name: Install docs deps 35 | run: pnpm -C docs install --frozen-lockfile=false 36 | - name: Build docs 37 | run: pnpm -C docs build 38 | - name: Setup Pages 39 | uses: actions/configure-pages@v5 40 | - name: Upload artifact 41 | uses: actions/upload-pages-artifact@v3 42 | with: 43 | path: docs/.vitepress/dist 44 | 45 | deploy: 46 | needs: build 47 | runs-on: ubuntu-latest 48 | environment: 49 | name: github-pages 50 | url: ${{ steps.deployment.outputs.page_url }} 51 | steps: 52 | - name: Deploy to GitHub Pages 53 | id: deployment 54 | uses: actions/deploy-pages@v4 55 | -------------------------------------------------------------------------------- /docs/generators/arktype.md: -------------------------------------------------------------------------------- 1 | # ArkType Generator 2 | 3 | Generates ArkType schemas per table (insert/update/select) and an index barrel. 4 | 5 | See the [package README](https://github.com/use-drzl/drzl/blob/master/packages/generator-arktype/README.md) for details. 6 | 7 | ## Example output 8 | 9 | ```ts 10 | import { type } from 'arktype'; 11 | 12 | export const InsertusersSchema = type({ 13 | email: 'string', 14 | }); 15 | 16 | export const UpdateusersSchema = type({ 17 | id: 'number?', 18 | email: 'string?', 19 | }); 20 | 21 | export const SelectusersSchema = type({ 22 | id: 'number', 23 | email: 'string', 24 | }); 25 | 26 | export type InsertusersInput = (typeof InsertusersSchema)['infer']; 27 | export type UpdateusersInput = (typeof UpdateusersSchema)['infer']; 28 | export type SelectusersOutput = (typeof SelectusersSchema)['infer']; 29 | ``` 30 | 31 | ## Generated Output License 32 | 33 | - You own the generated output. DRZL grants you a worldwide, royalty‑free, irrevocable license to use, copy, modify, and distribute the generated files under your project’s license. 34 | - A short header is added by default. Configure via `outputHeader` in `drzl.config.ts`: 35 | - `outputHeader.enabled = false` to disable 36 | - `outputHeader.text = '...'` to customize 37 | 38 | ::: tip Need something else? 39 | If this generator doesn't cover what you need, DM me on X (https://x.com/omardulaimidev) and we can scope it together. 40 | ::: 41 | -------------------------------------------------------------------------------- /packages/template-standard/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # @drzl/template-standard 4 | 5 |
6 | 7 | [![CI](https://github.com/use-drzl/drzl/actions/workflows/ci.yml/badge.svg)](https://github.com/use-drzl/drzl/actions/workflows/ci.yml) 8 | [![npm](https://img.shields.io/npm/v/%40drzl%2Ftemplate-standard)](https://www.npmjs.com/package/@drzl/template-standard) 9 | 10 |
11 | 12 | Minimal oRPC router template (no service wiring) — great for quick starts. 13 | 14 |
15 | 16 | ## 💚 Sponsor DRZL 17 | 18 |
19 | 20 | DRZL is crafted nights & weekends. Sponsorships keep the generators fast, tested, and free. 21 | 22 | [![Sponsor DRZL](https://img.shields.io/badge/GitHub%20Sponsors-Support%20the%20project-ff69b4?logo=github)](https://github.com/sponsors/omar-dulaimi) 23 | 24 |
25 | 26 | - Every dollar speeds up CI hardware and offsets long test runs on my aging laptop. 27 | - Sponsors get roadmap input and priority responses in GitHub Issues. 28 | - Prefer a quick overview? Check `docs/sponsor.md` for the current goals and thank-yous. 29 | 30 | ## Use 31 | 32 | Reference from the oRPC generator: 33 | 34 | ```ts 35 | generators: [{ kind: 'orpc', template: '@drzl/template-standard' }]; 36 | ``` 37 | 38 | ## Hooks (template API) 39 | 40 | - filePath(table, ctx) 41 | - routerName(table, ctx) 42 | - procedures(table) 43 | - imports?(), prelude?(), header?(table) 44 | -------------------------------------------------------------------------------- /docs/generators/zod.md: -------------------------------------------------------------------------------- 1 | # Zod Generator 2 | 3 | Generates Zod schemas per table (insert/update/select) and an index barrel. 4 | 5 | See the [package README](https://github.com/use-drzl/drzl/blob/master/packages/generator-zod/README.md) for details. 6 | 7 | ## Example output 8 | 9 | ```ts 10 | import { z } from 'zod'; 11 | 12 | export const InsertusersSchema = z.object({ 13 | email: z.string(), 14 | }); 15 | 16 | export const UpdateusersSchema = z.object({ 17 | id: z.number().optional(), 18 | email: z.string().optional(), 19 | }); 20 | 21 | export const SelectusersSchema = z.object({ 22 | id: z.number(), 23 | email: z.string(), 24 | }); 25 | 26 | export type InsertusersInput = z.input; 27 | export type UpdateusersInput = z.input; 28 | export type SelectusersOutput = z.output; 29 | ``` 30 | 31 | ## Generated Output License 32 | 33 | - You own the generated output. DRZL grants you a worldwide, royalty‑free, irrevocable license to use, copy, modify, and distribute the generated files under your project’s license. 34 | - A short header is added by default. Configure via `outputHeader` in `drzl.config.ts`: 35 | - `outputHeader.enabled = false` to disable 36 | - `outputHeader.text = '...'` to customize 37 | 38 | ::: tip Need something else? 39 | If this generator doesn't cover what you need, DM me on X (https://x.com/omardulaimidev) and we can scope it together. 40 | ::: 41 | -------------------------------------------------------------------------------- /packages/validation-core/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # @drzl/validation-core 4 | 5 |
6 | 7 | [![CI](https://github.com/use-drzl/drzl/actions/workflows/ci.yml/badge.svg)](https://github.com/use-drzl/drzl/actions/workflows/ci.yml) 8 | [![npm](https://img.shields.io/npm/v/%40drzl%2Fvalidation-core)](https://www.npmjs.com/package/@drzl/validation-core) 9 | 10 |
11 | 12 | Shared interfaces and helpers used by validation generators. 13 | 14 |
15 | 16 | ## 💚 Sponsor DRZL 17 | 18 |
19 | 20 | DRZL is crafted nights & weekends. Sponsorships keep the generators fast, tested, and free. 21 | 22 | [![Sponsor DRZL](https://img.shields.io/badge/GitHub%20Sponsors-Support%20the%20project-ff69b4?logo=github)](https://github.com/sponsors/omar-dulaimi) 23 | 24 |
25 | 26 | - Every dollar speeds up CI hardware and offsets long test runs on my aging laptop. 27 | - Sponsors get roadmap input and priority responses in GitHub Issues. 28 | - Prefer a quick overview? Check `docs/sponsor.md` for the current goals and thank-yous. 29 | 30 | ## Exports (essentials) 31 | 32 | - ValidationLibrary: `'zod' | 'valibot' | 'arktype'` 33 | - ValidationRenderer 34 | - `library` 35 | - `renderTable(table, opts)` 36 | - `renderIndex?(analysis, opts)` 37 | - Helpers 38 | - `insertColumns(table)`, `updateColumns(table)`, `selectColumns(table)` 39 | - `formatCode(code, filePath, formatOpts)` 40 | -------------------------------------------------------------------------------- /docs/generators/valibot.md: -------------------------------------------------------------------------------- 1 | # Valibot Generator 2 | 3 | Generates Valibot schemas per table (insert/update/select) and an index barrel. 4 | 5 | See the [package README](https://github.com/use-drzl/drzl/blob/master/packages/generator-valibot/README.md) for details. 6 | 7 | ## Example output 8 | 9 | ```ts 10 | import * as v from 'valibot'; 11 | import type { InferInput, InferOutput } from 'valibot'; 12 | 13 | export const InsertusersSchema = v.object({ 14 | email: v.string(), 15 | }); 16 | 17 | export const UpdateusersSchema = v.object({ 18 | id: v.number(), 19 | email: v.optional(v.string()), 20 | }); 21 | 22 | export const SelectusersSchema = v.object({ 23 | id: v.number(), 24 | email: v.string(), 25 | }); 26 | 27 | export type InsertusersInput = InferInput; 28 | export type UpdateusersInput = InferInput; 29 | export type SelectusersOutput = InferOutput; 30 | ``` 31 | 32 | ## Generated Output License 33 | 34 | - You own the generated output. DRZL grants you a worldwide, royalty‑free, irrevocable license to use, copy, modify, and distribute the generated files under your project’s license. 35 | - A short header is added by default. Configure via `outputHeader` in `drzl.config.ts`: 36 | - `outputHeader.enabled = false` to disable 37 | - `outputHeader.text = '...'` to customize 38 | 39 | ::: tip Need something else? 40 | If this generator doesn't cover what you need, DM me on X (https://x.com/omardulaimidev) and we can scope it together. 41 | ::: 42 | -------------------------------------------------------------------------------- /packages/generator-arktype/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # @drzl/generator-arktype 4 | 5 |
6 | 7 | [![CI](https://github.com/use-drzl/drzl/actions/workflows/ci.yml/badge.svg)](https://github.com/use-drzl/drzl/actions/workflows/ci.yml) 8 | [![npm](https://img.shields.io/npm/v/%40drzl%2Fgenerator-arktype)](https://www.npmjs.com/package/@drzl/generator-arktype) 9 | 10 |
11 | 12 | ArkType schemas from your Drizzle analysis (insert / update / select). 13 | 14 |
15 | 16 | ## 💚 Sponsor DRZL 17 | 18 |
19 | 20 | DRZL is crafted nights & weekends. Sponsorships keep the generators fast, tested, and free. 21 | 22 | [![Sponsor DRZL](https://img.shields.io/badge/GitHub%20Sponsors-Support%20the%20project-ff69b4?logo=github)](https://github.com/sponsors/omar-dulaimi) 23 | 24 |
25 | 26 | - Every dollar speeds up CI hardware and offsets long test runs on my aging laptop. 27 | - Sponsors get roadmap input and priority responses in GitHub Issues. 28 | - Prefer a quick overview? Check `docs/sponsor.md` for the current goals and thank-yous. 29 | 30 | ## Use 31 | 32 | Add to `drzl.config.ts`: 33 | 34 | ```ts 35 | generators: [{ kind: 'arktype', path: 'src/validators/arktype' }]; 36 | ``` 37 | 38 | ## Output 39 | 40 | - `Insert
Schema`, `Update
Schema`, `Select
Schema` 41 | - Optional `index` barrel 42 | - Shared vs inlined schemas supported 43 | 44 | ## Notes 45 | 46 | - Formatting integrates with Prettier/Biome (via `format.engine: 'auto'`). 47 | -------------------------------------------------------------------------------- /packages/generator-zod/test/generator.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { ZodGenerator } from '../src'; 3 | import type { Analysis } from '@drzl/analyzer'; 4 | 5 | describe('@drzl/generator-zod', () => { 6 | it('renders zod schemas for a simple table', async () => { 7 | const analysis: Analysis = { 8 | dialect: 'sqlite', 9 | tables: [ 10 | { 11 | name: 'users', 12 | tsName: 'users', 13 | columns: [ 14 | { 15 | name: 'id', 16 | tsType: 'number', 17 | dbType: 'INTEGER', 18 | nullable: false, 19 | hasDefault: true, 20 | isGenerated: true, 21 | }, 22 | { 23 | name: 'email', 24 | tsType: 'string', 25 | dbType: 'TEXT', 26 | nullable: false, 27 | hasDefault: false, 28 | isGenerated: false, 29 | }, 30 | ], 31 | unique: [], 32 | indexes: [], 33 | } as any, 34 | ], 35 | enums: [], 36 | relations: [], 37 | issues: [], 38 | }; 39 | const gen = new ZodGenerator(analysis); 40 | const code = gen.renderTable(analysis.tables[0]); 41 | expect(code).toContain('import { z } from'); 42 | expect(code).toContain('export const InsertusersSchema'); 43 | expect(code).toContain('export const UpdateusersSchema'); 44 | expect(code).toContain('export const SelectusersSchema'); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /docs/templates/orpc-service.md: -------------------------------------------------------------------------------- 1 | # oRPC + Service Template 2 | 3 | oRPC router template that connects procedures to the generated Service layer, with optional database injection middleware. 4 | 5 | See the [package README](https://github.com/use-drzl/drzl/blob/master/packages/template-orpc-service/README.md) for hooks and options. 6 | 7 | ## Database middleware 8 | 9 | When `databaseInjection.enabled` is true (configured via the oRPC generator), routers include a `dbMiddleware`: 10 | 11 | ```ts 12 | import type { Database } from 'src/db/db'; 13 | 14 | export const dbMiddleware = os 15 | .$context<{ db?: Database }>() 16 | .middleware(async ({ context, next }) => { 17 | if (!context.db) throw new ORPCError('INTERNAL_SERVER_ERROR'); 18 | return next({ context: { db: context.db } }); 19 | }); 20 | ``` 21 | 22 | Procedures call services with `context.db`. 23 | 24 | ## Example (Cloudflare D1) 25 | 26 | ```ts 27 | import { RPCHandler } from '@orpc/server/fetch'; 28 | import { createDatabase } from 'src/db/db'; 29 | import { router } from 'src/api'; 30 | 31 | const handler = new RPCHandler(router); 32 | export default { 33 | async fetch(request, env) { 34 | const db = createDatabase(env.DATABASE); 35 | return ( 36 | (await handler.handle(request, { prefix: '/api', context: { db } })).response ?? 37 | new Response('Not Found', { status: 404 }) 38 | ); 39 | }, 40 | }; 41 | ``` 42 | 43 | ::: tip Need something else? 44 | If this template doesn't cover what you need, DM me on X (https://x.com/omardulaimidev) and we can scope it together. 45 | ::: 46 | -------------------------------------------------------------------------------- /packages/generator-arktype/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @drzl/generator-arktype 2 | 3 | ## 1.2.0 4 | 5 | ### Minor Changes 6 | 7 | - c48d79a: sponsor initiatives 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies [c48d79a] 12 | - @drzl/validation-core@1.1.0 13 | - @drzl/analyzer@1.2.0 14 | 15 | ## 1.1.0 16 | 17 | ### Minor Changes 18 | 19 | - 2ca4b77: Fix ArkType generator emitting double-wrapped enum strings; pgEnum unions now render with JSON-escaped literals so `drzl generate` succeeds even when 20 | 21 | ### Patch Changes 22 | 23 | - Updated dependencies [2ca4b77] 24 | - @drzl/analyzer@1.1.0 25 | 26 | ## 1.0.0 27 | 28 | ### Major Changes 29 | 30 | - 5da6f6b: support MySQL, SingleStore, and Gel; expand Postgres/SQLite; add tests (fixes #13) 31 | 32 | ### Patch Changes 33 | 34 | - Updated dependencies [5da6f6b] 35 | - @drzl/analyzer@1.0.0 36 | - @drzl/validation-core@1.0.0 37 | 38 | ## 0.3.0 39 | 40 | ### Patch Changes 41 | 42 | - @drzl/analyzer@0.3.0 43 | - @drzl/validation-core@0.3.0 44 | 45 | ## 0.2.0 46 | 47 | ### Patch Changes 48 | 49 | - @drzl/analyzer@0.2.0 50 | - @drzl/validation-core@0.2.0 51 | 52 | ## 0.1.0 53 | 54 | ### Patch Changes 55 | 56 | - @drzl/analyzer@0.1.0 57 | - @drzl/validation-core@0.1.0 58 | 59 | ## 0.0.3 60 | 61 | ### Patch Changes 62 | 63 | - @drzl/analyzer@0.0.3 64 | - @drzl/validation-core@0.0.3 65 | 66 | ## 0.0.2 67 | 68 | ### Patch Changes 69 | 70 | - @drzl/analyzer@0.0.2 71 | - @drzl/validation-core@0.0.2 72 | 73 | ## 0.0.1 74 | 75 | ### Patch Changes 76 | 77 | - @drzl/analyzer@0.0.1 78 | - @drzl/validation-core@0.0.1 79 | -------------------------------------------------------------------------------- /packages/generator-valibot/test/generator.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { ValibotGenerator } from '../src'; 3 | import type { Analysis } from '@drzl/analyzer'; 4 | 5 | describe('@drzl/generator-valibot', () => { 6 | it('renders valibot schemas for a simple table', async () => { 7 | const analysis: Analysis = { 8 | dialect: 'sqlite', 9 | tables: [ 10 | { 11 | name: 'posts', 12 | tsName: 'posts', 13 | columns: [ 14 | { 15 | name: 'id', 16 | tsType: 'number', 17 | dbType: 'INTEGER', 18 | nullable: false, 19 | hasDefault: true, 20 | isGenerated: true, 21 | }, 22 | { 23 | name: 'title', 24 | tsType: 'string', 25 | dbType: 'TEXT', 26 | nullable: false, 27 | hasDefault: false, 28 | isGenerated: false, 29 | }, 30 | ], 31 | unique: [], 32 | indexes: [], 33 | } as any, 34 | ], 35 | enums: [], 36 | relations: [], 37 | issues: [], 38 | }; 39 | const gen = new ValibotGenerator(analysis); 40 | const code = gen.renderTable(analysis.tables[0]); 41 | expect(code).toContain("import * as v from 'valibot'"); 42 | expect(code).toContain('export const InsertpostsSchema'); 43 | expect(code).toContain('export const UpdatepostsSchema'); 44 | expect(code).toContain('export const SelectpostsSchema'); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /packages/validation-core/test/core.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { insertColumns, updateColumns, selectColumns, type Table } from '../src'; 3 | 4 | describe('@drzl/validation-core helpers', () => { 5 | const table: Table = { 6 | name: 'users', 7 | tsName: 'users', 8 | columns: [ 9 | { 10 | name: 'id', 11 | tsType: 'number', 12 | dbType: 'INTEGER', 13 | nullable: false, 14 | hasDefault: true, 15 | isGenerated: true, 16 | }, 17 | { 18 | name: 'email', 19 | tsType: 'string', 20 | dbType: 'TEXT', 21 | nullable: false, 22 | hasDefault: false, 23 | isGenerated: false, 24 | }, 25 | { 26 | name: 'bio', 27 | tsType: 'string', 28 | dbType: 'TEXT', 29 | nullable: true, 30 | hasDefault: false, 31 | isGenerated: false, 32 | }, 33 | ], 34 | primaryKey: { columns: ['id'] }, 35 | }; 36 | 37 | it('insertColumns omits PK/generated', () => { 38 | const cols = insertColumns(table); 39 | expect(cols.some((c) => c.name === 'id')).toBe(false); 40 | expect(cols.some((c) => c.name === 'email')).toBe(true); 41 | }); 42 | 43 | it('updateColumns includes non-PK fields', () => { 44 | const cols = updateColumns(table); 45 | expect(cols.some((c) => c.name === 'id')).toBe(false); 46 | expect(cols.some((c) => c.name === 'bio')).toBe(true); 47 | }); 48 | 49 | it('selectColumns returns all', () => { 50 | const cols = selectColumns(table); 51 | expect(cols.length).toBe(3); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /packages/generator-orpc/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @drzl/generator-orpc 2 | 3 | ## 1.1.0 4 | 5 | ### Minor Changes 6 | 7 | - c48d79a: sponsor initiatives 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies [c48d79a] 12 | - @drzl/template-orpc-service@1.1.0 13 | - @drzl/template-standard@1.1.0 14 | - @drzl/analyzer@1.2.0 15 | 16 | ## 1.0.0 17 | 18 | ### Major Changes 19 | 20 | - 5da6f6b: support MySQL, SingleStore, and Gel; expand Postgres/SQLite; add tests (fixes #13) 21 | 22 | ### Patch Changes 23 | 24 | - Updated dependencies [5da6f6b] 25 | - @drzl/analyzer@1.0.0 26 | - @drzl/template-orpc-service@1.0.0 27 | - @drzl/template-standard@1.0.0 28 | 29 | ## 0.4.0 30 | 31 | ### Minor Changes 32 | 33 | - 811dd61: feat: strict database injection for services and oRPC middleware (typed db context; valibot v1 compatibility) 34 | 35 | ### Patch Changes 36 | 37 | - Updated dependencies [811dd61] 38 | - @drzl/template-orpc-service@0.4.0 39 | 40 | ## 0.3.0 41 | 42 | ### Patch Changes 43 | 44 | - @drzl/analyzer@0.3.0 45 | - @drzl/template-standard@0.3.0 46 | 47 | ## 0.2.0 48 | 49 | ### Patch Changes 50 | 51 | - @drzl/analyzer@0.2.0 52 | - @drzl/template-standard@0.2.0 53 | 54 | ## 0.1.0 55 | 56 | ### Patch Changes 57 | 58 | - @drzl/analyzer@0.1.0 59 | - @drzl/template-standard@0.1.0 60 | 61 | ## 0.0.3 62 | 63 | ### Patch Changes 64 | 65 | - @drzl/analyzer@0.0.3 66 | - @drzl/template-standard@0.0.3 67 | 68 | ## 0.0.2 69 | 70 | ### Patch Changes 71 | 72 | - @drzl/analyzer@0.0.2 73 | - @drzl/template-standard@0.0.2 74 | 75 | ## 0.0.1 76 | 77 | ### Patch Changes 78 | 79 | - @drzl/analyzer@0.0.1 80 | - @drzl/template-standard@0.0.1 81 | -------------------------------------------------------------------------------- /docs/adapters/router.md: -------------------------------------------------------------------------------- 1 | # Router Adapters 2 | 3 | Router adapters are implemented as templates with a small hook interface. The generator calls these hooks to produce files, names, imports, and procedure code. 4 | 5 | Generic template interface (example): 6 | 7 | ```ts 8 | interface RouterTemplateHooks
{ 9 | filePath( 10 | table: Table, 11 | ctx: { 12 | outDir: string; 13 | naming?: { routerSuffix?: string; procedureCase?: 'camel' | 'kebab' | 'snake' }; 14 | } 15 | ): string; 16 | routerName( 17 | table: Table, 18 | ctx: { naming?: { routerSuffix?: string; procedureCase?: 'camel' | 'kebab' | 'snake' } } 19 | ): string; 20 | procedures(table: Table): Array<{ name: string; varName: string; code: string }>; 21 | imports?(tables: Table[], ctx?: any): string; 22 | prelude?(tables: Table[], ctx?: any): string; 23 | header?(table: Table): string; 24 | } 25 | ``` 26 | 27 | - `filePath`: absolute path for a table’s router file 28 | - `routerName`: the exported constant name 29 | - `procedures`: an array of procedures; each item provides an exported key (`name`), a variable identifier (`varName`), and the implementation `code` 30 | - `imports`: additional imports at file top 31 | - `prelude`: helper code after imports 32 | - `header`: banner/comment string at top 33 | 34 | Example custom template: see [/templates/custom](/templates/custom) 35 | 36 | Notes: 37 | 38 | - Validation libraries can be reused/injected by the generator (Zod/Valibot/ArkType) 39 | - Output typing is attached by the generator when possible 40 | - Adapters can map to oRPC, tRPC, Express handlers, etc., by changing the `procedures` implementation 41 | -------------------------------------------------------------------------------- /packages/analyzer/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # @drzl/analyzer 4 | 5 |
6 | 7 | [![CI](https://github.com/use-drzl/drzl/actions/workflows/ci.yml/badge.svg)](https://github.com/use-drzl/drzl/actions/workflows/ci.yml) 8 | [![npm](https://img.shields.io/npm/v/%40drzl%2Fanalyzer)](https://www.npmjs.com/package/@drzl/analyzer) 9 | 10 |
11 | 12 | Drizzle schema → normalized analysis for fast, reliable codegen. 13 | 14 |
15 | 16 | ## 💚 Sponsor DRZL 17 | 18 |
19 | 20 | DRZL is crafted nights & weekends. Sponsorships keep the generators fast, tested, and free. 21 | 22 | [![Sponsor DRZL](https://img.shields.io/badge/GitHub%20Sponsors-Support%20the%20project-ff69b4?logo=github)](https://github.com/sponsors/omar-dulaimi) 23 | 24 |
25 | 26 | - Every dollar speeds up CI hardware and offsets long test runs on my aging laptop. 27 | - Sponsors get roadmap input and priority responses in GitHub Issues. 28 | - Prefer a quick overview? Check `docs/sponsor.md` for the current goals and thank-yous. 29 | 30 | ## Use 31 | 32 | ```ts 33 | import { SchemaAnalyzer } from '@drzl/analyzer'; 34 | 35 | const analyzer = new SchemaAnalyzer('src/db/schemas/index.ts'); 36 | const analysis = await analyzer.analyze({ 37 | includeRelations: true, 38 | validateConstraints: true, 39 | }); 40 | ``` 41 | 42 | The CLI consumes this analysis to generate validation, services, and routers. 43 | 44 | ## Output (high level) 45 | 46 | - dialect, tables, columns, keys, indexes 47 | - relations (incl. inferred), enums 48 | - issues (warnings/errors) for constraints and shape 49 | 50 | ## Notes 51 | 52 | - Best‑effort introspection aligned with Drizzle symbols across versions. 53 | -------------------------------------------------------------------------------- /docs/guide/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | DRZL reads a `drzl.config.ts` that describes your schema path and generators. 4 | 5 | ```ts 6 | import { defineConfig } from '@drzl/cli/config'; 7 | 8 | export default defineConfig({ 9 | schema: 'src/db/schemas/index.ts', 10 | outDir: 'src/api', 11 | analyzer: { includeRelations: true, validateConstraints: true }, 12 | generators: [ 13 | { kind: 'zod', path: 'src/validators/zod', schemaSuffix: 'Schema' }, 14 | { 15 | kind: 'service', 16 | path: 'src/services', 17 | dataAccess: 'drizzle', 18 | schemaImportPath: 'src/db/schema', 19 | databaseInjection: { 20 | enabled: true, 21 | databaseType: 'Database', 22 | databaseTypeImport: { name: 'Database', from: 'src/db/db' }, 23 | }, 24 | }, 25 | { 26 | kind: 'orpc', 27 | template: '@drzl/template-orpc-service', 28 | includeRelations: true, 29 | naming: { routerSuffix: 'Router', procedureCase: 'kebab' }, 30 | validation: { library: 'valibot' }, 31 | databaseInjection: { 32 | enabled: true, 33 | databaseType: 'Database', 34 | databaseTypeImport: { name: 'Database', from: 'src/db/db' }, 35 | }, 36 | servicesDir: 'src/services', 37 | }, 38 | ], 39 | }); 40 | ``` 41 | 42 | ## Config File Formats 43 | 44 | DRZL accepts multiple config formats: 45 | 46 | - TypeScript: `drzl.config.ts` 47 | - ES Module: `drzl.config.mjs` 48 | - CommonJS: `drzl.config.js` 49 | - JSON: `drzl.config.json` 50 | 51 | When using JSON, ensure it’s strict JSON (no comments/trailing commas). TS/JS configs can export either a default object or use `defineConfig(...)`. 52 | 53 | See package READMEs for generator‑specific options. 54 | -------------------------------------------------------------------------------- /scripts/changeset-commit.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom Changesets commit message generator using Conventional Commits style. 3 | * Exports getAddMessage and getVersionMessage. 4 | */ 5 | 6 | /** 7 | * @param {import('@changesets/types').NewChangeset} changeset 8 | * @param {{ skipCI?: 'add'|'version'|true }} [options] 9 | */ 10 | async function getAddMessage(changeset, options) { 11 | const skipCI = options?.skipCI === 'add' || options?.skipCI === true; 12 | const skipMsg = skipCI ? '\n\n[skip ci]\n' : ''; 13 | // Use chore for adding a changeset entry 14 | return `chore(changeset): ${changeset.summary}${skipMsg}`; 15 | } 16 | 17 | /** 18 | * @param {import('@changesets/types').ReleasePlan} releasePlan 19 | * @param {{ skipCI?: 'add'|'version'|true }} [options] 20 | */ 21 | async function getVersionMessage(releasePlan, options) { 22 | const skipCI = options?.skipCI === 'version' || options?.skipCI === true; 23 | const publishable = releasePlan.releases.filter((r) => r.type !== 'none'); 24 | const count = publishable.length; 25 | // If all packages share same version (lockstep), show that; otherwise list first few 26 | const versions = new Set(publishable.map((r) => r.newVersion)); 27 | const versionPart = versions.size === 1 ? `v${[...versions][0]}` : `${count} packages`; 28 | const lines = publishable 29 | .slice(0, 20) 30 | .map((r) => ` - ${r.name}@${r.newVersion}`) 31 | .join('\n'); 32 | const more = publishable.length > 20 ? `\n - ...and ${publishable.length - 20} more` : ''; 33 | const body = publishable.length ? `\n\nReleases:\n${lines}${more}` : ''; 34 | return `chore(release): publish ${versionPart}${skipCI ? '\n\n[skip ci]\n' : ''}${body}`; 35 | } 36 | 37 | module.exports = { 38 | getAddMessage, 39 | getVersionMessage, 40 | }; 41 | -------------------------------------------------------------------------------- /packages/analyzer/test/sqlite-types.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { SchemaAnalyzer } from '../src/index'; 3 | import { promises as fs } from 'node:fs'; 4 | import path from 'node:path'; 5 | 6 | describe('Analyzer SQLite coarse type inference', () => { 7 | it('maps common SQLite constructor names to ts types', async () => { 8 | const dir = path.resolve(__dirname, 'fixtures'); 9 | await fs.mkdir(dir, { recursive: true }); 10 | const schema = path.join(dir, 'sqlite-types.mjs'); 11 | const code = ` 12 | class SQLiteInteger { constructor(){ this.config = {}; } } 13 | class SQLiteText {} 14 | class SQLiteReal {} 15 | class SQLiteBlob {} 16 | class SQLiteNumeric {} 17 | class SQLiteBoolean {} 18 | 19 | const tsInt = new SQLiteInteger(); 20 | tsInt.config.mode = 'timestamp'; 21 | 22 | const table = {}; 23 | table[Symbol.for('drizzle:Name')] = 'sq_table'; 24 | table[Symbol.for('drizzle:Columns')] = { 25 | i: new SQLiteInteger(), 26 | t: new SQLiteText(), 27 | r: new SQLiteReal(), 28 | b: new SQLiteBlob(), 29 | n: new SQLiteNumeric(), 30 | bool: new SQLiteBoolean(), 31 | createdAt: tsInt, 32 | }; 33 | 34 | export { table as sq_table }; 35 | `; 36 | await fs.writeFile(schema, code, 'utf8'); 37 | const a = new SchemaAnalyzer(schema); 38 | const res = await a.analyze(); 39 | const t = res.tables.find((t) => t.name === 'sq_table'); 40 | expect(t).toBeTruthy(); 41 | const get = (n: string) => new Map(t!.columns.map((c) => [c.name, c.tsType])).get(n); 42 | expect(get('i')).toBe('number'); 43 | expect(get('t')).toBe('string'); 44 | expect(get('r')).toBe('number'); 45 | expect(get('b')).toBe('Uint8Array'); 46 | expect(get('n')).toBe('number'); 47 | expect(get('bool')).toBe('boolean'); 48 | expect(get('createdAt')).toBe('Date'); 49 | }); 50 | }); 51 | 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@drzl/root", 3 | "private": true, 4 | "license": "Apache-2.0", 5 | "version": "0.0.0", 6 | "description": "DRZL monorepo", 7 | "packageManager": "pnpm@10.15.1", 8 | "workspaces": [ 9 | "packages/*" 10 | ], 11 | "scripts": { 12 | "build": "pnpm -r run build", 13 | "build:packages": "pnpm -r --sort --workspace-concurrency=1 --filter='{packages/*}' run build", 14 | "build:publish": "pnpm build:packages && pnpm exec changeset publish", 15 | "dev": "pnpm --filter @drzl/cli run dev", 16 | "test": "pnpm -r test", 17 | "lint": "eslint . --ext .ts", 18 | "format": "prettier -w .", 19 | "typecheck": "pnpm -r --filter @drzl/* exec tsc -p tsconfig.json --noEmit", 20 | "changeset": "changeset", 21 | "changeset:status": "changeset status", 22 | "changeset:version": "changeset version", 23 | "changeset:publish": "pnpm -w build && changeset publish", 24 | "release": "pnpm -w test && pnpm -w build && pnpm -w changeset:version && pnpm -w changeset:publish", 25 | "docs:dev": "pnpm -C docs dev", 26 | "docs:build": "pnpm -C docs build", 27 | "docs:preview": "pnpm -C docs preview", 28 | "brand:gen": "node scripts/brand-gen.mjs assets/brand/source.jpeg", 29 | "brand:gen:lockup": "node scripts/brand-gen.mjs assets/brand/source.jpeg --mode=lockup" 30 | }, 31 | "devDependencies": { 32 | "@changesets/cli": "^2.27.9", 33 | "@types/node": "^24.3.1", 34 | "@typescript-eslint/eslint-plugin": "^8.42.0", 35 | "@typescript-eslint/parser": "^8.42.0", 36 | "eslint": "^9.35.0", 37 | "prettier": "^3.6.2", 38 | "sharp": "^0.33.5", 39 | "tsup": "^8.5.0", 40 | "typescript": "^5.9.2", 41 | "vitest": "^3.2.4" 42 | }, 43 | "funding": { 44 | "type": "github", 45 | "url": "https://github.com/sponsors/omar-dulaimi" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/cli/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # @drzl/cli 4 | 5 |
6 | 7 | [![CI](https://github.com/use-drzl/drzl/actions/workflows/ci.yml/badge.svg)](https://github.com/use-drzl/drzl/actions/workflows/ci.yml) 8 | [![npm](https://img.shields.io/npm/v/%40drzl%2Fcli)](https://www.npmjs.com/package/@drzl/cli) 9 | 10 |
11 | 12 | Analyze your Drizzle schema and generate validation, services, and routers. 13 | 14 |
15 | 16 | ## 💚 Sponsor DRZL 17 | 18 |
19 | 20 | DRZL is crafted nights & weekends. Sponsorships keep the generators fast, tested, and free. 21 | 22 | [![Sponsor DRZL](https://img.shields.io/badge/GitHub%20Sponsors-Support%20the%20project-ff69b4?logo=github)](https://github.com/sponsors/omar-dulaimi) 23 | 24 |
25 | 26 | - Every dollar speeds up CI hardware and offsets long test runs on my aging laptop. 27 | - Sponsors get roadmap input and priority responses in GitHub Issues. 28 | - Prefer a quick overview? Check `docs/sponsor.md` for the current goals and thank-yous. 29 | 30 | ## Commands 31 | 32 | - Init: `pnpm dlx @drzl/cli init` 33 | - Analyze: `pnpm dlx @drzl/cli analyze [--relations] [--validate]` 34 | - Generate: `pnpm dlx @drzl/cli generate -c drzl.config.ts` 35 | - Watch: `pnpm dlx @drzl/cli watch -c drzl.config.ts` 36 | 37 | ## Minimal config 38 | 39 | ```ts 40 | import { defineConfig } from '@drzl/cli/config'; 41 | 42 | export default defineConfig({ 43 | schema: 'src/db/schemas/index.ts', 44 | outDir: 'src/api', 45 | generators: [ 46 | { kind: 'zod', path: 'src/validators/zod' }, 47 | { kind: 'service', path: 'src/services', dataAccess: 'drizzle' }, 48 | { kind: 'orpc', template: '@drzl/template-orpc-service' }, 49 | ], 50 | }); 51 | ``` 52 | 53 | Notes 54 | 55 | - `format.engine: 'auto'` tries Prettier, then Biome. 56 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@drzl/cli", 3 | "version": "1.1.0", 4 | "private": false, 5 | "license": "Apache-2.0", 6 | "type": "module", 7 | "bin": { 8 | "drzl": "dist/cli.js" 9 | }, 10 | "main": "dist/cli.js", 11 | "types": "dist/config.d.ts", 12 | "exports": { 13 | ".": { 14 | "import": "./dist/cli.js" 15 | }, 16 | "./config": { 17 | "import": "./dist/config.js", 18 | "types": "./dist/config.d.ts" 19 | } 20 | }, 21 | "files": [ 22 | "dist" 23 | ], 24 | "scripts": { 25 | "build": "tsup src/cli.ts src/config.ts --format esm,cjs --sourcemap --dts", 26 | "dev": "node --loader ts-node/esm src/cli.ts --help", 27 | "lint": "eslint . --ext .ts", 28 | "test": "vitest run" 29 | }, 30 | "dependencies": { 31 | "@drzl/analyzer": "workspace:^", 32 | "@drzl/generator-orpc": "workspace:^", 33 | "@drzl/generator-zod": "workspace:^", 34 | "@drzl/generator-service": "workspace:^", 35 | "@drzl/generator-valibot": "workspace:^", 36 | "@drzl/generator-arktype": "workspace:^", 37 | "commander": "^14.0.0", 38 | "chalk": "^5.6.0", 39 | "chokidar": "^4.0.3", 40 | "ora": "^8.2.0", 41 | "cli-progress": "^3.12.0", 42 | "zod": "^4.1.5", 43 | "jiti": "^2.5.1" 44 | }, 45 | "devDependencies": { 46 | "tsup": "^8.5.0", 47 | "typescript": "^5.9.2", 48 | "ts-node": "^10.9.2" 49 | }, 50 | "engines": { 51 | "node": ">=18.17.0" 52 | }, 53 | "publishConfig": { 54 | "access": "public", 55 | "registry": "https://registry.npmjs.org/", 56 | "provenance": true 57 | }, 58 | "repository": { 59 | "type": "git", 60 | "url": "https://github.com/use-drzl/drzl", 61 | "directory": "packages/cli" 62 | }, 63 | "funding": { 64 | "type": "github", 65 | "url": "https://github.com/sponsors/omar-dulaimi" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/generator-service/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # @drzl/generator-service 4 | 5 |
6 | 7 | [![CI](https://github.com/use-drzl/drzl/actions/workflows/ci.yml/badge.svg)](https://github.com/use-drzl/drzl/actions/workflows/ci.yml) 8 | [![npm](https://img.shields.io/npm/v/%40drzl%2Fgenerator-service)](https://www.npmjs.com/package/@drzl/generator-service) 9 | 10 |
11 | 12 | Typed CRUD service classes per table — Drizzle‑aware or stubbed. 13 | 14 |
15 | 16 | ## 💚 Sponsor DRZL 17 | 18 |
19 | 20 | DRZL is crafted nights & weekends. Sponsorships keep the generators fast, tested, and free. 21 | 22 | [![Sponsor DRZL](https://img.shields.io/badge/GitHub%20Sponsors-Support%20the%20project-ff69b4?logo=github)](https://github.com/sponsors/omar-dulaimi) 23 | 24 |
25 | 26 | - Every dollar speeds up CI hardware and offsets long test runs on my aging laptop. 27 | - Sponsors get roadmap input and priority responses in GitHub Issues. 28 | - Prefer a quick overview? Check `docs/sponsor.md` for the current goals and thank-yous. 29 | 30 | ## Use 31 | 32 | Add to `drzl.config.ts`: 33 | 34 | ```ts 35 | generators: [ 36 | { 37 | kind: 'service', 38 | path: 'src/services', 39 | dataAccess: 'drizzle', 40 | schemaImportPath: 'src/db/schema', 41 | databaseInjection: { 42 | enabled: true, 43 | databaseType: 'Database', 44 | databaseTypeImport: { name: 'Database', from: 'src/db/db' }, 45 | }, 46 | }, 47 | ]; 48 | ``` 49 | 50 | ## Notes 51 | 52 | - In drizzle mode, uses `$inferSelect` / `$inferInsert` for end‑to‑end types. 53 | - `Update` is derived from `Insert` with PKs omitted and fields partial. 54 | - `databaseInjection` makes services accept `db: Database` (or your chosen type) instead of importing a global singleton — ideal for serverless runtimes. 55 | -------------------------------------------------------------------------------- /packages/template-orpc-service/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # @drzl/template-orpc-service 4 | 5 |
6 | 7 | [![CI](https://github.com/use-drzl/drzl/actions/workflows/ci.yml/badge.svg)](https://github.com/use-drzl/drzl/actions/workflows/ci.yml) 8 | [![npm](https://img.shields.io/npm/v/%40drzl%2Ftemplate-orpc-service)](https://www.npmjs.com/package/@drzl/template-orpc-service) 9 | 10 |
11 | 12 | oRPC router template wired to the generated Service layer. 13 | 14 |
15 | 16 | ## 💚 Sponsor DRZL 17 | 18 |
19 | 20 | DRZL is crafted nights & weekends. Sponsorships keep the generators fast, tested, and free. 21 | 22 | [![Sponsor DRZL](https://img.shields.io/badge/GitHub%20Sponsors-Support%20the%20project-ff69b4?logo=github)](https://github.com/sponsors/omar-dulaimi) 23 | 24 |
25 | 26 | - Every dollar speeds up CI hardware and offsets long test runs on my aging laptop. 27 | - Sponsors get roadmap input and priority responses in GitHub Issues. 28 | - Prefer a quick overview? Check `docs/sponsor.md` for the current goals and thank-yous. 29 | 30 | ## Use 31 | 32 | Reference from the oRPC generator: 33 | 34 | ```ts 35 | generators: [ 36 | { 37 | kind: 'orpc', 38 | template: '@drzl/template-orpc-service', 39 | validation: { library: 'valibot' }, 40 | databaseInjection: { 41 | enabled: true, 42 | databaseType: 'Database', 43 | databaseTypeImport: { name: 'Database', from: 'src/db/db' }, 44 | }, 45 | servicesDir: 'src/services', 46 | }, 47 | ]; 48 | ``` 49 | 50 | ## Hooks (template API) 51 | 52 | - filePath(table, ctx) 53 | - routerName(table, ctx) 54 | - procedures(table) 55 | - imports?(tables, ctx) 56 | - header?(table) 57 | 58 | When `databaseInjection.enabled` is true, the template emits a `dbMiddleware` and expects `context.db` to be provided by your runtime (e.g., Cloudflare Workers via D1). 59 | -------------------------------------------------------------------------------- /packages/generator-orpc/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # @drzl/generator-orpc 4 | 5 |
6 | 7 | [![CI](https://github.com/use-drzl/drzl/actions/workflows/ci.yml/badge.svg)](https://github.com/use-drzl/drzl/actions/workflows/ci.yml) 8 | [![npm](https://img.shields.io/npm/v/%40drzl%2Fgenerator-orpc)](https://www.npmjs.com/package/@drzl/generator-orpc) 9 | 10 |
11 | 12 | oRPC routers per table — with optional reuse of shared validation schemas. 13 | 14 |
15 | 16 | ## 💚 Sponsor DRZL 17 | 18 |
19 | 20 | DRZL is crafted nights & weekends. Sponsorships keep the generators fast, tested, and free. 21 | 22 | [![Sponsor DRZL](https://img.shields.io/badge/GitHub%20Sponsors-Support%20the%20project-ff69b4?logo=github)](https://github.com/sponsors/omar-dulaimi) 23 | 24 |
25 | 26 | - Every dollar speeds up CI hardware and offsets long test runs on my aging laptop. 27 | - Sponsors get roadmap input and priority responses in GitHub Issues. 28 | - Prefer a quick overview? Check `docs/sponsor.md` for the current goals and thank-yous. 29 | 30 | ## Use 31 | 32 | Add to `drzl.config.ts`: 33 | 34 | ```ts 35 | generators: [ 36 | { 37 | kind: 'orpc', 38 | template: '@drzl/template-orpc-service', 39 | includeRelations: true, 40 | validation: { library: 'valibot' }, 41 | databaseInjection: { 42 | enabled: true, 43 | databaseType: 'Database', 44 | databaseTypeImport: { name: 'Database', from: 'src/db/db' }, 45 | }, 46 | servicesDir: 'src/services', 47 | }, 48 | ]; 49 | ``` 50 | 51 | ## Behavior 52 | 53 | - Reuses pre-generated Insert/Update/Select schemas when `validation.useShared` is true. 54 | - Otherwise, inlines schemas using the chosen library (zod/valibot/arktype). 55 | - Works with templates for different wiring (service-backed, minimal, custom). With `@drzl/template-orpc-service`, a `dbMiddleware` is emitted and `context.db` is passed to services. 56 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | paths-ignore: 7 | - 'docs/**' 8 | 9 | jobs: 10 | release: 11 | name: Version & Publish 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: write 15 | id-token: write # provenance 16 | pull-requests: write 17 | packages: read 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | 22 | # Install the PNPM version declared in package.json -> "packageManager": "pnpm@10.15.1" 23 | - name: Setup pnpm 24 | uses: pnpm/action-setup@v4 25 | # ⚠️ Do NOT pass 'version' here; it will read packageManager automatically 26 | 27 | # Write scoped .npmrc and wire the token so npm commands (incl. whoami) work 28 | - name: Setup Node (scoped registry) 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: 22 32 | registry-url: 'https://registry.npmjs.org/' 33 | scope: '@drzl' 34 | 35 | - name: Prove npm auth 36 | run: | 37 | npm config get registry 38 | npm whoami 39 | npm ping 40 | env: 41 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 42 | 43 | - name: Install deps 44 | run: pnpm install --frozen-lockfile 45 | 46 | - name: Build packages 47 | run: pnpm -r --sort --workspace-concurrency=1 --filter '{packages/*}' run build 48 | 49 | - name: Changesets (PR + publish on merge) 50 | uses: changesets/action@v1 51 | with: 52 | version: pnpm -w changeset:version 53 | commit: 'chore(release): publish' 54 | title: 'Version Packages' 55 | createGithubReleases: true 56 | publish: pnpm -w changeset publish 57 | env: 58 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 59 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 60 | NPM_CONFIG_PROVENANCE: true 61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | -------------------------------------------------------------------------------- /packages/generator-service/test/generator.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { ServiceGenerator } from '../src'; 3 | import type { Analysis } from '@drzl/analyzer'; 4 | import { promises as fs } from 'node:fs'; 5 | import path from 'node:path'; 6 | import os from 'node:os'; 7 | 8 | describe('@drzl/generator-service', () => { 9 | it('generates service types and class (stub)', async () => { 10 | const analysis: Analysis = { 11 | dialect: 'sqlite', 12 | tables: [ 13 | { 14 | name: 'users', 15 | tsName: 'users', 16 | columns: [ 17 | { 18 | name: 'id', 19 | tsType: 'number', 20 | dbType: 'INTEGER', 21 | nullable: false, 22 | hasDefault: true, 23 | isGenerated: true, 24 | }, 25 | { 26 | name: 'email', 27 | tsType: 'string', 28 | dbType: 'TEXT', 29 | nullable: false, 30 | hasDefault: false, 31 | isGenerated: false, 32 | }, 33 | ], 34 | unique: [], 35 | indexes: [], 36 | primaryKey: { columns: ['id'] }, 37 | } as any, 38 | ], 39 | enums: [], 40 | relations: [], 41 | issues: [], 42 | }; 43 | const outDir = await fs.mkdtemp(path.join(os.tmpdir(), 'drzl-svc-')); 44 | try { 45 | const gen = new ServiceGenerator(analysis); 46 | const files = await gen.generate({ outDir, dataAccess: 'stub' }); 47 | const svcFile = files.find((f) => /Service\.ts$/.test(f)); 48 | const typesFile = files.find((f) => /types\/users\.ts$/.test(f)); 49 | expect(svcFile).toBeTruthy(); 50 | expect(typesFile).toBeTruthy(); 51 | const svcContent = await fs.readFile(svcFile!, 'utf8'); 52 | expect(svcContent).toContain('class UserService'); 53 | expect(svcContent).toContain('static async getAll'); 54 | } finally { 55 | await fs.rm(outDir, { recursive: true, force: true }); 56 | } 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea or improvement 3 | title: '[Feature]: ' 4 | labels: [enhancement] 5 | assignees: [] 6 | body: 7 | - type: checkboxes 8 | id: checklist 9 | attributes: 10 | label: Pre-flight checklist 11 | description: Please confirm before submitting 12 | options: 13 | - label: I searched existing Issues and Discussions 14 | required: true 15 | - label: I’m on the latest version 16 | required: true 17 | - label: I read the docs and CONTRIBUTING 18 | required: true 19 | - label: I have starred the repository ⭐ 20 | required: false 21 | - label: I have bandwidth to contribute a PR for this 22 | required: false 23 | - label: I’m willing to sponsor this work 💖 (optional) 24 | required: false 25 | - type: textarea 26 | id: problem 27 | attributes: 28 | label: Problem 29 | description: What problem does this solve? Who benefits? 30 | placeholder: Describe the pain point/use case 31 | validations: 32 | required: true 33 | - type: textarea 34 | id: proposal 35 | attributes: 36 | label: Proposal 37 | description: Describe the solution you’d like. Include API sketches, CLI options, or examples. 38 | placeholder: | 39 | e.g. New CLI flag, config options, API signatures, sample code 40 | render: markdown 41 | validations: 42 | required: true 43 | - type: textarea 44 | id: alternatives 45 | attributes: 46 | label: Alternatives considered 47 | description: What else did you try? Why is this better? 48 | - type: checkboxes 49 | id: breaking 50 | attributes: 51 | label: Breaking change? 52 | options: 53 | - label: This feature introduces breaking changes 54 | required: false 55 | - type: textarea 56 | id: migration 57 | attributes: 58 | label: Migration strategy (if any) 59 | description: If breaking, outline migration notes 60 | - type: textarea 61 | id: impact 62 | attributes: 63 | label: Impact 64 | description: Who benefits and how? Any constraints? 65 | -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | # Repository Guidelines 2 | 3 | ## Project Structure & Modules 4 | 5 | - Root: pnpm workspace with TypeScript packages under `packages/*`. 6 | - Packages: `analyzer`, `cli`, `generator-*`, `template-*`, `validation-core` (each has `src/`, `test/`, `dist/`). 7 | - Docs: VitePress site in `docs/` (not published with packages). 8 | - Assets & scripts: `assets/`, `scripts/` (branding utilities). 9 | - Examples: `real-world-example/` for end-to-end usage. 10 | 11 | ## Build, Test, and Dev 12 | 13 | - Install deps: `pnpm install` 14 | - Build all: `pnpm build` or `pnpm -r run build` 15 | - Dev CLI: `pnpm dev` (runs `@drzl/cli` in watch/dev mode) 16 | - Tests: `pnpm test` (runs Vitest across workspaces) 17 | - Lint: `pnpm lint` (ESLint on `.ts`) 18 | - Format: `pnpm format` (Prettier write) 19 | - Docs: `pnpm docs:dev | docs:build | docs:preview` 20 | 21 | ## Coding Style & Naming 22 | 23 | - Language: TypeScript (ESM, Node >= 18.17; dev on Node 20+ preferred). 24 | - Formatting: Prettier; run `pnpm format` before committing. 25 | - Linting: ESLint (`eslint.config.js`, `@typescript-eslint/*`). Fix warnings where feasible. 26 | - Indentation: 2 spaces; limit line length to ~100–120 chars. 27 | - Names: kebab-case for packages (`generator-zod`), PascalCase for types, camelCase for functions/vars, UPPER_SNAKE for constants. 28 | 29 | ## Testing Guidelines 30 | 31 | - Framework: Vitest. 32 | - Structure: tests live in `packages/*/test` with `*.test.ts` files. 33 | - Running: `pnpm -r test`; to focus a package: `pnpm --filter @drzl/ test`. 34 | - Keep tests deterministic; use temp dirs for filesystem codegen tests. 35 | 36 | ## Commits & Pull Requests 37 | 38 | - Commits: Conventional Commits (e.g., `feat(cli): add init command`, `fix(analyzer): handle enums`). 39 | - Branches: `feature/`, `bugfix/`, `chore/`. 40 | - PRs: clear description, linked issue, rationale, screenshots/CLI output when relevant. Include docs/README updates for user-facing changes. 41 | - Checks: ensure `pnpm -r test`, `pnpm lint`, and `pnpm build` pass. 42 | - Releases: Changesets drive versioning; maintainers run `pnpm release`. 43 | 44 | ## Security & Configuration 45 | 46 | - No secrets in repo or tests. Use local env only. 47 | - Respect ESM: avoid `require`; use `import` and `type` imports where applicable. 48 | - Prefer small, composable functions; keep public APIs stable and documented in package READMEs. 49 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Report a reproducible problem 3 | title: '[Bug]: ' 4 | labels: [bug] 5 | assignees: [] 6 | body: 7 | - type: checkboxes 8 | id: checklist 9 | attributes: 10 | label: Pre-flight checklist 11 | description: Please confirm before submitting 12 | options: 13 | - label: I searched existing Issues and Discussions 14 | required: true 15 | - label: I’m using the latest released versions 16 | required: true 17 | - label: I can provide a minimal reproduction 18 | required: true 19 | - label: I have starred the repository ⭐ 20 | required: false 21 | - label: I’m willing to submit a PR to help fix this 22 | required: false 23 | - label: I’m willing to sponsor this work 💖 (optional) 24 | required: false 25 | - type: textarea 26 | id: summary 27 | attributes: 28 | label: Summary 29 | description: A clear, concise description of the issue 30 | placeholder: What happened? 31 | validations: 32 | required: true 33 | - type: textarea 34 | id: reproduction 35 | attributes: 36 | label: Reproduction 37 | description: Steps and a minimal repo/gist/snippet to reproduce 38 | placeholder: | 39 | 1. Go to ... 40 | 2. Run ... 41 | 3. See error ... 42 | render: text 43 | validations: 44 | required: true 45 | - type: textarea 46 | id: expected 47 | attributes: 48 | label: Expected behavior 49 | placeholder: What did you expect to happen? 50 | validations: 51 | required: true 52 | - type: textarea 53 | id: actual 54 | attributes: 55 | label: Actual behavior (logs/stacktraces) 56 | render: shell 57 | validations: 58 | required: true 59 | - type: input 60 | id: node 61 | attributes: 62 | label: Node version 63 | placeholder: e.g. 20.11.1 64 | - type: input 65 | id: pnpm 66 | attributes: 67 | label: pnpm version 68 | placeholder: e.g. 10.15.1 69 | - type: textarea 70 | id: versions 71 | attributes: 72 | label: DRZL package versions 73 | description: Paste relevant versions (from lockfile or `pnpm list`) 74 | render: text 75 | - type: textarea 76 | id: extra 77 | attributes: 78 | label: Additional context 79 | description: Anything else that might help 80 | -------------------------------------------------------------------------------- /docs/generators/service.md: -------------------------------------------------------------------------------- 1 | # Service Generator 2 | 3 | Generates typed CRUD service classes per table (Drizzle or stub). 4 | 5 | Key options: 6 | 7 | - `outDir`, `dataAccess`, `dbImportPath`, `schemaImportPath` 8 | - `databaseInjection`: make services accept a database instance (serverless‑friendly) 9 | 10 | See the [package README](https://github.com/use-drzl/drzl/blob/master/packages/generator-service/README.md) for details. 11 | 12 | ## Database Injection (serverless) 13 | 14 | Pass a database into service methods instead of importing a global singleton. 15 | 16 | ```ts 17 | export default defineConfig({ 18 | generators: [ 19 | { 20 | kind: 'service', 21 | path: 'src/services', 22 | dataAccess: 'drizzle', 23 | schemaImportPath: 'src/db/schema', 24 | databaseInjection: { 25 | enabled: true, 26 | databaseType: 'Database', 27 | databaseTypeImport: { name: 'Database', from: 'src/db/db' }, 28 | }, 29 | }, 30 | ], 31 | }); 32 | ``` 33 | 34 | Generated methods (example): 35 | 36 | ```ts 37 | import type { Database } from 'src/db/db'; 38 | 39 | export class UserService { 40 | static async getAll(db: Database) { 41 | /* ... */ 42 | } 43 | static async getById(db: Database, id: number) { 44 | /* ... */ 45 | } 46 | } 47 | ``` 48 | 49 | This aligns with Cloudflare Workers/Astro patterns where a db is created per request/context. 50 | 51 | ## Examples 52 | 53 | ### Drizzle mode 54 | 55 | ```ts 56 | export default defineConfig({ 57 | schema: 'src/db/schemas/index.ts', 58 | generators: [ 59 | { 60 | kind: 'service', 61 | path: 'src/services', 62 | dataAccess: 'drizzle', 63 | dbImportPath: 'src/db/connection', 64 | schemaImportPath: 'src/db/schemas', 65 | }, 66 | ], 67 | }); 68 | ``` 69 | 70 | Produces services using `table.$inferSelect` / `$inferInsert` types and CRUD methods. 71 | 72 | ### Stub mode 73 | 74 | ```ts 75 | export default defineConfig({ 76 | generators: [{ kind: 'service', path: 'src/services', dataAccess: 'stub' }], 77 | }); 78 | ``` 79 | 80 | Stubs return sample values for quick prototyping. 81 | 82 | ## Generated Output License 83 | 84 | - You own the generated output. DRZL grants you a worldwide, royalty‑free, irrevocable license to use, copy, modify, and distribute the generated files under your project’s license. 85 | - A short header is added by default. Configure via `outputHeader` in `drzl.config.ts`: 86 | - `outputHeader.enabled = false` to disable 87 | - `outputHeader.text = '...'` to customize 88 | 89 | ::: tip Need something else? 90 | If this generator doesn't cover what you need, DM me on X (https://x.com/omardulaimidev) and we can scope it together. 91 | ::: 92 | -------------------------------------------------------------------------------- /packages/analyzer/test/gel-types.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { SchemaAnalyzer } from '../src/index'; 3 | import { promises as fs } from 'node:fs'; 4 | import path from 'node:path'; 5 | 6 | describe('Analyzer Gel (EdgeDB) coarse type inference', () => { 7 | it('maps common Gel constructor names to ts types', async () => { 8 | const dir = path.resolve(__dirname, 'fixtures'); 9 | await fs.mkdir(dir, { recursive: true }); 10 | const schema = path.join(dir, 'gel-types.mjs'); 11 | const code = ` 12 | class GelInteger {} 13 | class GelSmallInt {} 14 | class GelInt53 {} 15 | class GelBigInt64 {} 16 | class GelText {} 17 | class GelUUID {} 18 | class GelJson {} 19 | class GelReal {} 20 | class GelDoublePrecision {} 21 | class GelDecimal {} 22 | class GelBytes {} 23 | class GelTimestamp {} 24 | class GelTimestampTz {} 25 | class GelLocalDateString {} 26 | class GelLocalTime {} 27 | class GelDateDuration {} 28 | class GelRelDuration {} 29 | class GelDuration {} 30 | 31 | const table = {}; 32 | table[Symbol.for('drizzle:Name')] = 'gel_table'; 33 | table[Symbol.for('drizzle:Columns')] = { 34 | i: new GelInteger(), 35 | si: new GelSmallInt(), 36 | i53: new GelInt53(), 37 | bi64: new GelBigInt64(), 38 | t: new GelText(), 39 | u: new GelUUID(), 40 | j: new GelJson(), 41 | r: new GelReal(), 42 | dp: new GelDoublePrecision(), 43 | dec: new GelDecimal(), 44 | b: new GelBytes(), 45 | ts: new GelTimestamp(), 46 | tstz: new GelTimestampTz(), 47 | ld: new GelLocalDateString(), 48 | lt: new GelLocalTime(), 49 | dd: new GelDateDuration(), 50 | rd: new GelRelDuration(), 51 | d: new GelDuration(), 52 | }; 53 | 54 | export { table as gel_table }; 55 | `; 56 | await fs.writeFile(schema, code, 'utf8'); 57 | const a = new SchemaAnalyzer(schema); 58 | const res = await a.analyze(); 59 | const t = res.tables.find((t) => t.name === 'gel_table'); 60 | expect(t).toBeTruthy(); 61 | const get = (n: string) => new Map(t!.columns.map((c) => [c.name, c.tsType])).get(n); 62 | expect(get('i')).toBe('number'); 63 | expect(get('si')).toBe('number'); 64 | expect(get('i53')).toBe('number'); 65 | expect(get('bi64')).toBe('bigint'); 66 | expect(get('t')).toBe('string'); 67 | expect(get('u')).toBe('string'); 68 | expect(get('j')).toBe('any'); 69 | expect(get('r')).toBe('number'); 70 | expect(get('dp')).toBe('number'); 71 | expect(get('dec')).toBe('string'); 72 | expect(get('b')).toBe('Uint8Array'); 73 | expect(get('ts')).toBe('string'); 74 | expect(get('tstz')).toBe('Date'); 75 | expect(get('ld')).toBe('string'); 76 | expect(get('lt')).toBe('string'); 77 | expect(get('dd')).toBe('string'); 78 | expect(get('rd')).toBe('string'); 79 | expect(get('d')).toBe('string'); 80 | }); 81 | }); 82 | 83 | -------------------------------------------------------------------------------- /packages/validation-core/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Analysis, Column } from '@drzl/analyzer'; 2 | 3 | // Minimal local Table shape for codegen logic and tests 4 | export interface Table { 5 | name: string; 6 | tsName: string; 7 | columns: Column[]; 8 | primaryKey?: { columns: string[] }; 9 | } 10 | 11 | export type ValidationLibrary = 'zod' | 'valibot' | 'arktype'; 12 | 13 | export interface FormatOptions { 14 | enabled?: boolean; 15 | engine?: 'auto' | 'prettier' | 'biome'; 16 | configPath?: string; 17 | } 18 | 19 | export interface ValidationGenerateOptions { 20 | outDir: string; 21 | format?: FormatOptions; 22 | fileSuffix?: string; // e.g. .zod.ts 23 | schemaSuffix?: string; // e.g. Schema 24 | coerceDates?: 'input' | 'all' | 'none'; 25 | emit?: { 26 | select?: boolean; 27 | insert?: boolean; 28 | update?: boolean; 29 | }; 30 | } 31 | 32 | export interface ValidationRenderer< 33 | TOptions extends ValidationGenerateOptions = ValidationGenerateOptions, 34 | > { 35 | readonly library: ValidationLibrary; 36 | renderTable(table: Table, opts?: TOptions): string; 37 | renderIndex?(analysis: Analysis, opts?: TOptions): string; 38 | generate(opts: TOptions): Promise; 39 | } 40 | 41 | export function isGeneratedColumn(c: Column, primaryKeyColumns: string[]): boolean { 42 | return c.isGenerated || primaryKeyColumns.includes(c.name); 43 | } 44 | 45 | export function insertColumns(table: Table): Column[] { 46 | const pkCols = table.primaryKey?.columns ?? []; 47 | return table.columns.filter((c) => !isGeneratedColumn(c, pkCols)); 48 | } 49 | 50 | export function updateColumns(table: Table): Column[] { 51 | const pkCols = table.primaryKey?.columns ?? []; 52 | return table.columns.filter((c) => !pkCols.includes(c.name)); 53 | } 54 | 55 | export function selectColumns(table: Table): Column[] { 56 | return table.columns; 57 | } 58 | 59 | export async function formatCode(code: string, filePath: string, fmt?: FormatOptions) { 60 | if (fmt && fmt.enabled === false) return code; 61 | const engine = fmt?.engine ?? 'auto'; 62 | try { 63 | if (engine === 'prettier' || engine === 'auto') { 64 | const prettier: any = await import('prettier'); 65 | const cfgRef = fmt?.configPath ?? filePath; 66 | const cfg = await prettier.resolveConfig(cfgRef).catch(() => null); 67 | return prettier.format(code, { ...(cfg ?? {}), parser: 'typescript', filepath: filePath }); 68 | } 69 | } catch {} 70 | try { 71 | if (engine === 'biome' || engine === 'auto') { 72 | const dynamicImport: any = Function('s', 'return import(s)'); 73 | const biome: any = await dynamicImport('@biomejs/biome').catch(() => null); 74 | if (biome?.formatContent) { 75 | const res = await biome.formatContent(code, { filePath }); 76 | return (res && (res.content || res.formatted)) ?? code; 77 | } 78 | } 79 | } catch {} 80 | return code; 81 | } 82 | 83 | // Re-export analyzer types for test/type convenience 84 | // (Keep local Table as exported type) 85 | -------------------------------------------------------------------------------- /packages/cli/src/sponsor.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; 3 | import path from 'node:path'; 4 | 5 | export interface SponsorMessageOptions { 6 | reason?: string; 7 | minIntervalMs?: number; 8 | force?: boolean; 9 | } 10 | 11 | interface SponsorCachePayload { 12 | runs: number; 13 | lastShownAt?: number; 14 | lastReason?: string; 15 | } 16 | 17 | const CACHE_DIR = path.join(process.cwd(), 'node_modules', '.cache', '@drzl'); 18 | const CACHE_FILE = path.join(CACHE_DIR, 'sponsor-message.json'); 19 | const DEFAULT_INTERVAL_MS = 1000 * 60 * 15; // 15 minutes 20 | let shownThisProcess = false; 21 | 22 | const tips = [ 23 | 'Pair DRZL watch mode with drizzle-kit to keep schema & API synced.', 24 | 'Templatize your ORPC routers to roll out new endpoints safely.', 25 | 'Need typed validators? Enable the zod, valibot, or arktype generators.', 26 | 'Use output headers to track generated files and trim noisy diffs.', 27 | ]; 28 | 29 | const green = (msg: string) => chalk.hex('#6ee7b7')(msg); 30 | const cyan = (msg: string) => chalk.cyan(msg); 31 | const gray = (msg: string) => chalk.gray(msg); 32 | 33 | export function maybeShowSponsorMessage({ 34 | reason = 'generate', 35 | minIntervalMs = DEFAULT_INTERVAL_MS, 36 | force = false, 37 | }: SponsorMessageOptions = {}) { 38 | const hideViaEnv = process.env.DRZL_HIDE_SPONSOR?.toLowerCase(); 39 | const hideRequested = hideViaEnv === '1' || hideViaEnv === 'true'; 40 | if (hideRequested || (process.env.CI && !force) || (shownThisProcess && !force)) return; 41 | 42 | try { 43 | mkdirSync(CACHE_DIR, { recursive: true }); 44 | const payload = readCache(); 45 | payload.runs += 1; 46 | 47 | const now = Date.now(); 48 | const shouldShow = force || now - (payload.lastShownAt ?? 0) >= minIntervalMs; 49 | 50 | if (shouldShow) { 51 | payload.lastShownAt = now; 52 | payload.lastReason = reason; 53 | } 54 | 55 | writeCache(payload); 56 | 57 | if (!shouldShow) return; 58 | 59 | shownThisProcess = true; 60 | const tip = tips[payload.runs % tips.length]; 61 | 62 | console.log( 63 | `\n${cyan(`🚀 DRZL finished a ${reason} run (#${payload.runs.toLocaleString()}).`)}\n\n` + 64 | `${green('✨ Sponsors keep DRZL shipping. Consider supporting ongoing dev:')}\n` + 65 | ` ${green('GitHub Sponsors')} ${gray('→ https://github.com/sponsors/omar-dulaimi')}\n\n` + 66 | `${green('Pro tip:')} ${tip}\n` 67 | ); 68 | } catch { 69 | // Swallow to avoid impacting generator success paths 70 | } 71 | } 72 | 73 | function readCache(): SponsorCachePayload { 74 | if (!existsSync(CACHE_FILE)) { 75 | return { runs: 0 }; 76 | } 77 | try { 78 | const data = JSON.parse(readFileSync(CACHE_FILE, 'utf8')) as SponsorCachePayload; 79 | if (typeof data.runs !== 'number') return { runs: 0 }; 80 | return data; 81 | } catch { 82 | return { runs: 0 }; 83 | } 84 | } 85 | 86 | function writeCache(payload: SponsorCachePayload) { 87 | writeFileSync(CACHE_FILE, JSON.stringify(payload, null, 2), 'utf8'); 88 | } 89 | -------------------------------------------------------------------------------- /packages/generator-arktype/test/generator.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { ArkTypeGenerator } from '../src'; 3 | import type { Analysis } from '@drzl/analyzer'; 4 | 5 | describe('@drzl/generator-arktype', () => { 6 | it('renders arktype schemas for a simple table', async () => { 7 | const analysis: Analysis = { 8 | dialect: 'sqlite', 9 | tables: [ 10 | { 11 | name: 'comments', 12 | tsName: 'comments', 13 | columns: [ 14 | { 15 | name: 'id', 16 | tsType: 'number', 17 | dbType: 'INTEGER', 18 | nullable: false, 19 | hasDefault: true, 20 | isGenerated: true, 21 | }, 22 | { 23 | name: 'body', 24 | tsType: 'string', 25 | dbType: 'TEXT', 26 | nullable: false, 27 | hasDefault: false, 28 | isGenerated: false, 29 | }, 30 | ], 31 | unique: [], 32 | indexes: [], 33 | } as any, 34 | ], 35 | enums: [], 36 | relations: [], 37 | issues: [], 38 | }; 39 | const gen = new ArkTypeGenerator(analysis); 40 | const code = gen.renderTable(analysis.tables[0]); 41 | expect(code).toContain("import { type } from 'arktype'"); 42 | expect(code).toContain('export const InsertcommentsSchema'); 43 | expect(code).toContain('export const UpdatecommentsSchema'); 44 | expect(code).toContain('export const SelectcommentsSchema'); 45 | }); 46 | 47 | it('renders enums while escaping quotes in generated output', () => { 48 | const analysis: Analysis = { 49 | dialect: 'postgres', 50 | tables: [ 51 | { 52 | name: 'users', 53 | tsName: 'users', 54 | columns: [ 55 | { 56 | name: 'role', 57 | tsType: 'string', 58 | dbType: 'TEXT', 59 | enumValues: ['admin', 'cashier', 'he said "hi"'], 60 | nullable: false, 61 | hasDefault: false, 62 | isGenerated: false, 63 | }, 64 | { 65 | name: 'status', 66 | tsType: 'string', 67 | dbType: 'TEXT', 68 | enumValues: ['pending', "needs'Escaping"], 69 | nullable: true, 70 | hasDefault: false, 71 | isGenerated: false, 72 | }, 73 | ], 74 | unique: [], 75 | indexes: [], 76 | } as any, 77 | ], 78 | enums: [], 79 | relations: [], 80 | issues: [], 81 | }; 82 | 83 | const gen = new ArkTypeGenerator(analysis); 84 | const code = gen.renderTable(analysis.tables[0]); 85 | 86 | const expectedRoleLine = ` ${JSON.stringify('role')}: ${JSON.stringify( 87 | "'admin' | 'cashier' | 'he said \"hi\"'" 88 | )},`; 89 | const expectedStatusLine = ` ${JSON.stringify('status')}: ${JSON.stringify( 90 | "('pending' | 'needs\\'Escaping' | null)?" 91 | )},`; 92 | 93 | expect(code).toContain(expectedRoleLine); 94 | expect(code).toContain(expectedStatusLine); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /packages/analyzer/test/mysql-types.spec.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'node:fs'; 2 | import path from 'node:path'; 3 | import { describe, expect, it } from 'vitest'; 4 | import { SchemaAnalyzer } from '../src/index'; 5 | 6 | // This test simulates MySQL column classes by name and ensures 7 | // the analyzer's coarse MySQL inference does not return `unknown`. 8 | describe('Analyzer MySQL coarse type inference', () => { 9 | it('maps common MySQL column constructors to ts types', async () => { 10 | const dir = path.resolve(__dirname, 'fixtures'); 11 | await fs.mkdir(dir, { recursive: true }); 12 | const schema = path.join(dir, 'mysql-types.mjs'); 13 | const code = ` 14 | // Simulate a Drizzle table-like export with MySQL column constructor names 15 | class MySqlVarchar {} 16 | class MySqlInt {} 17 | class MySqlBigInt53 {} 18 | class MySqlBigInt64 {} 19 | class MySqlBoolean {} 20 | class MySqlTimestamp {} 21 | class MySqlTimestampString {} 22 | class MySqlDate {} 23 | class MySqlDateString {} 24 | class MySqlDateTime {} 25 | class MySqlDateTimeString {} 26 | class MySqlTime {} 27 | class MySqlYear {} 28 | class MySqlJson {} 29 | class MySqlBlob {} 30 | class MySqlBinary {} 31 | class MySqlVarBinary {} 32 | class MySqlDouble {} 33 | class MySqlReal {} 34 | 35 | const table = {}; 36 | table[Symbol.for('drizzle:Name')] = 'users_table'; 37 | table[Symbol.for('drizzle:Columns')] = { 38 | id: new MySqlInt(), 39 | name: new MySqlVarchar(), 40 | age: new MySqlInt(), 41 | isActive: new MySqlBoolean(), 42 | createdAt: new MySqlTimestamp(), 43 | createdAtStr: new MySqlTimestampString(), 44 | dateCol: new MySqlDate(), 45 | dateStr: new MySqlDateString(), 46 | dtCol: new MySqlDateTime(), 47 | dtStr: new MySqlDateTimeString(), 48 | timeCol: new MySqlTime(), 49 | yearCol: new MySqlYear(), 50 | big53: new MySqlBigInt53(), 51 | big64: new MySqlBigInt64(), 52 | dbl: new MySqlDouble(), 53 | rl: new MySqlReal(), 54 | payload: new MySqlJson(), 55 | data: new MySqlBlob(), 56 | bin: new MySqlBinary(), 57 | vbin: new MySqlVarBinary(), 58 | }; 59 | 60 | export { table as users }; 61 | `; 62 | await fs.writeFile(schema, code, 'utf8'); 63 | const a = new SchemaAnalyzer(schema); 64 | const res = await a.analyze(); 65 | const t = res.tables.find((t) => t.name === 'users_table'); 66 | expect(t).toBeTruthy(); 67 | const map = new Map(t!.columns.map((c) => [c.name, c.tsType])); 68 | expect(map.get('id')).toBe('number'); 69 | expect(map.get('name')).toBe('string'); 70 | expect(map.get('age')).toBe('number'); 71 | expect(map.get('isActive')).toBe('boolean'); 72 | expect(map.get('createdAt')).toBe('Date'); 73 | expect(map.get('createdAtStr')).toBe('string'); 74 | expect(map.get('dateCol')).toBe('Date'); 75 | expect(map.get('dateStr')).toBe('string'); 76 | expect(map.get('dtCol')).toBe('Date'); 77 | expect(map.get('dtStr')).toBe('string'); 78 | expect(map.get('timeCol')).toBe('string'); 79 | expect(map.get('yearCol')).toBe('number'); 80 | expect(map.get('big53')).toBe('number'); 81 | expect(map.get('big64')).toBe('bigint'); 82 | expect(map.get('dbl')).toBe('number'); 83 | expect(map.get('rl')).toBe('number'); 84 | expect(map.get('payload')).toBe('any'); 85 | expect(map.get('data')).toBe('Uint8Array'); 86 | expect(map.get('bin')).toBe('Uint8Array'); 87 | expect(map.get('vbin')).toBe('Uint8Array'); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /docs/guide/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | Get up and running in seconds — no global installs required. 4 | 5 | Prereqs 6 | 7 | - Node.js ≥ 18.17 (LTS or newer) 8 | - A Drizzle schema (TypeScript) 9 | 10 | 1. Install the CLI 11 | 12 | Add DRZL CLI to your project (no globals needed): 13 | 14 | ::: code-group 15 | 16 | ```bash [pnpm] 17 | pnpm add -D @drzl/cli 18 | ``` 19 | 20 | ```bash [npm] 21 | npm i -D @drzl/cli 22 | ``` 23 | 24 | ```bash [yarn] 25 | yarn add -D @drzl/cli 26 | ``` 27 | 28 | ```bash [bun] 29 | bun add -d @drzl/cli 30 | ``` 31 | 32 | ::: 33 | 34 | 2. One complete example (oRPC + Zod + Service) 35 | 36 | Add a single config to generate Zod validators and oRPC routers that reuse them, plus typed services: 37 | 38 | ```ts 39 | // drzl.config.ts 40 | import { defineConfig } from '@drzl/cli/config'; 41 | 42 | export default defineConfig({ 43 | schema: 'src/db/schemas/index.ts', 44 | outDir: 'src/api', 45 | generators: [ 46 | // 1) Zod validators 47 | { kind: 'zod', path: 'src/validators/zod', schemaSuffix: 'Schema' }, 48 | 49 | // 2) Routers (oRPC adapter), reusing Zod schemas 50 | { 51 | kind: 'orpc', 52 | template: '@drzl/template-orpc-service', 53 | includeRelations: true, 54 | outputHeader: { enabled: true }, 55 | validation: { 56 | useShared: true, 57 | library: 'zod', 58 | importPath: 'src/validators/zod', 59 | schemaSuffix: 'Schema', 60 | }, 61 | }, 62 | // 3) Typed services (Drizzle-aware or stub) 63 | { 64 | kind: 'service', 65 | path: 'src/services', 66 | dataAccess: 'drizzle', // or 'stub' 67 | dbImportPath: 'src/db/connection', 68 | schemaImportPath: 'src/db/schemas', 69 | }, 70 | ], 71 | }); 72 | ``` 73 | 74 | 3. Install Templates 75 | 76 | The CLI comes with all the generators you need. You only need to install any templates you want to use. For this example, we'll install the oRPC service template: 77 | 78 | ::: code-group 79 | 80 | ```bash [pnpm] 81 | pnpm add -D @drzl/template-orpc-service 82 | ``` 83 | 84 | ```bash [npm] 85 | npm i -D @drzl/template-orpc-service 86 | ``` 87 | 88 | ```bash [yarn] 89 | yarn add -D @drzl/template-orpc-service 90 | ``` 91 | 92 | ```bash [bun] 93 | bun add -d @drzl/template-orpc-service 94 | ``` 95 | 96 | ::: 97 | 98 | 4. Generate: 99 | 100 | ::: code-group 101 | 102 | ```bash [pnpm] 103 | pnpm dlx @drzl/cli generate -c drzl.config.ts 104 | ``` 105 | 106 | ```bash [npm] 107 | npx @drzl/cli generate -c drzl.config.ts 108 | ``` 109 | 110 | ```bash [yarn] 111 | yarn dlx @drzl/cli generate -c drzl.config.ts 112 | ``` 113 | 114 | ```bash [bun] 115 | bunx @drzl/cli generate -c drzl.config.ts 116 | ``` 117 | 118 | ::: 119 | 120 | This writes validators to `src/validators/zod`, routers to `src/api`, and services to `src/services`. 121 | 122 | Notes 123 | 124 | - Router generation is adapter‑based (currently oRPC). Additional adapters (tRPC, Express, NestJS, Next.js, Prisma, etc.) can be added via templates. 125 | - For Zod/Valibot/ArkType or Service generators, install the matching package in your app (as shown above). 126 | - Config file formats supported: `drzl.config.ts`, `.mjs`, `.js`, `.json`. 127 | 128 | Next steps 129 | 130 | - CLI commands → [/cli](/cli) 131 | - Config reference → [/guide/configuration](/guide/configuration) 132 | - Adapters → [/adapters/overview](/adapters/overview) 133 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to DRZL 2 | 3 | Thanks for your interest in contributing! This guide explains how to set up your environment, propose changes, and submit high‑quality pull requests. 4 | 5 | ## Project Setup 6 | 7 | - Requirements: 8 | - Node.js 20+ 9 | - pnpm 10+ 10 | - Install: 11 | - `pnpm install` 12 | - Build, Test, Lint, Format: 13 | - `pnpm build` 14 | - `pnpm -r test` 15 | - `pnpm lint` 16 | - `pnpm format` 17 | 18 | ## Repo Structure (packages/) 19 | 20 | - `analyzer` — Drizzle schema analysis 21 | - `cli` — drzl CLI 22 | - `generator-*` — code generators (oRPC, service, zod, valibot, arktype) 23 | - `template-*` — oRPC templates 24 | - `validation-core` — shared validation codegen helpers 25 | 26 | ## Development Workflow 27 | 28 | 1. Create a feature branch from `master`: 29 | - `git checkout -b feature/short-desc` or `bugfix/issue-###` 30 | 2. Implement changes scoped to a single topic. 31 | 3. Run tests and lint locally (see commands above). 32 | 4. Update docs/readmes if behavior or APIs change. 33 | 5. Open a PR with a clear title and description. 34 | 35 | ### Commit Style (Conventional Commits) 36 | 37 | Use Conventional Commits for readable history and changelog automation: 38 | 39 | - `feat(scope): add new capability` 40 | - `fix(scope): resolve bug` 41 | - `docs(scope): update README` 42 | - `refactor(scope): code change with no behavior change` 43 | - `test(scope): add/adjust tests` 44 | - `chore(scope): tooling, build, deps` 45 | 46 | Examples: 47 | 48 | - `feat(orpc): inject .output() for arktype` 49 | - `fix(cli): apply analyzer defaults when omitted` 50 | 51 | ### Branch Naming 52 | 53 | - `feature/` 54 | - `bugfix/` 55 | - `chore/` 56 | 57 | ### Pull Request Checklist 58 | 59 | - [ ] Changes are focused and documented 60 | - [ ] Tests added or updated 61 | - [ ] `pnpm -r test` passes locally 62 | - [ ] `pnpm lint` passes (no new warnings/errors) 63 | - [ ] README/CHANGELOG updated where appropriate 64 | 65 | ## Testing Philosophy 66 | 67 | - Prefer unit‑level tests near the code under test. 68 | - Use temporary directories for any filesystem output and clean up after tests. 69 | - Keep tests independent and deterministic. 70 | 71 | ## Code Style 72 | 73 | - TypeScript strict mode is enabled; keep types precise and narrow. 74 | - Prefer small, composable functions and clear names. 75 | - Follow existing patterns in the package you are editing. 76 | 77 | ## Releasing / Versioning 78 | 79 | - The project uses SemVer for published packages. 80 | - Maintainers handle releases; contributors don’t need to publish. 81 | - If your change warrants a minor/major bump, call it out in the PR description. 82 | 83 | ## Reporting Issues / Proposals 84 | 85 | - Use descriptive titles and steps to reproduce (when applicable). 86 | - For feature proposals, outline the problem, the proposed solution, and alternatives considered. 87 | 88 | ## Sponsor-Wanted & Priority Issues 89 | 90 | - Issues tagged `sponsor-wanted` include a scoped brief, and desired outcome. If you’d like to fund one, comment or DM @omardulaimidev on X (https://x.com/omardulaimidev) so we can reserve it for you. 91 | - Issues tagged `priority` are high-impact items the maintainers plan to tackle next—sponsorship accelerates them. 92 | - Funded work always lands in this repo under Apache‑2.0 (no private forks or exclusivity). 93 | 94 | ## Code of Conduct 95 | 96 | Be respectful and inclusive. By participating, you agree to uphold a welcoming environment for everyone. If you encounter unacceptable behavior, please open an issue or contact the maintainers. 97 | -------------------------------------------------------------------------------- /packages/analyzer/test/pg-types.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { SchemaAnalyzer } from '../src/index'; 3 | import { promises as fs } from 'node:fs'; 4 | import path from 'node:path'; 5 | 6 | describe('Analyzer Postgres coarse type inference', () => { 7 | it('maps common Pg constructor names to ts types', async () => { 8 | const dir = path.resolve(__dirname, 'fixtures'); 9 | await fs.mkdir(dir, { recursive: true }); 10 | const schema = path.join(dir, 'pg-types.mjs'); 11 | const code = ` 12 | class PgInteger {} 13 | class PgSmallInt {} 14 | class PgBigInt {} 15 | class PgSerial {} 16 | class PgSmallSerial {} 17 | class PgBigSerial {} 18 | class PgText {} 19 | class PgVarchar {} 20 | class PgChar {} 21 | class PgUUID {} 22 | class PgInet {} 23 | class PgCidr {} 24 | class PgMacaddr {} 25 | class PgMacaddr8 {} 26 | class PgPoint {} 27 | class PgLine {} 28 | class PgJson {} 29 | class PgJsonb {} 30 | class PgReal {} 31 | class PgDoublePrecision {} 32 | class PgNumeric {} 33 | class PgDate {} 34 | class PgDateString {} 35 | class PgTimestamp {} 36 | class PgTimestampString {} 37 | class PgTimestamptz {} 38 | class PgTime {} 39 | class PgInterval {} 40 | 41 | const table = {}; 42 | table[Symbol.for('drizzle:Name')] = 'pg_table'; 43 | table[Symbol.for('drizzle:Columns')] = { 44 | i: new PgInteger(), 45 | si: new PgSmallInt(), 46 | bi: new PgBigInt(), 47 | s: new PgSerial(), 48 | ss: new PgSmallSerial(), 49 | bs: new PgBigSerial(), 50 | t: new PgText(), 51 | v: new PgVarchar(), 52 | c: new PgChar(), 53 | u: new PgUUID(), 54 | inet: new PgInet(), 55 | cidr: new PgCidr(), 56 | mac: new PgMacaddr(), 57 | mac8: new PgMacaddr8(), 58 | p: new PgPoint(), 59 | l: new PgLine(), 60 | j: new PgJson(), 61 | jb: new PgJsonb(), 62 | r: new PgReal(), 63 | d: new PgDoublePrecision(), 64 | n: new PgNumeric(), 65 | dateCol: new PgDate(), 66 | dateStr: new PgDateString(), 67 | ts: new PgTimestamp(), 68 | tsStr: new PgTimestampString(), 69 | tstz: new PgTimestamptz(), 70 | timeCol: new PgTime(), 71 | ivl: new PgInterval(), 72 | }; 73 | 74 | export { table as pg_table }; 75 | `; 76 | await fs.writeFile(schema, code, 'utf8'); 77 | const a = new SchemaAnalyzer(schema); 78 | const res = await a.analyze(); 79 | const t = res.tables.find((t) => t.name === 'pg_table'); 80 | expect(t).toBeTruthy(); 81 | const get = (n: string) => new Map(t!.columns.map((c) => [c.name, c.tsType])).get(n); 82 | expect(get('i')).toBe('number'); 83 | expect(get('si')).toBe('number'); 84 | expect(get('bi')).toBe('bigint'); 85 | expect(get('s')).toBe('number'); 86 | expect(get('ss')).toBe('number'); 87 | expect(get('bs')).toBe('number'); 88 | expect(get('t')).toBe('string'); 89 | expect(get('v')).toBe('string'); 90 | expect(get('c')).toBe('string'); 91 | expect(get('u')).toBe('string'); 92 | expect(get('inet')).toBe('string'); 93 | expect(get('cidr')).toBe('string'); 94 | expect(get('mac')).toBe('string'); 95 | expect(get('mac8')).toBe('string'); 96 | expect(get('p')).toBe('string'); 97 | expect(get('l')).toBe('string'); 98 | expect(get('j')).toBe('any'); 99 | expect(get('jb')).toBe('any'); 100 | expect(get('r')).toBe('number'); 101 | expect(get('d')).toBe('number'); 102 | expect(get('n')).toBe('number'); 103 | expect(get('dateCol')).toBe('Date'); 104 | expect(get('dateStr')).toBe('string'); 105 | expect(get('ts')).toBe('Date'); 106 | expect(get('tsStr')).toBe('string'); 107 | expect(get('tstz')).toBe('Date'); 108 | expect(get('timeCol')).toBe('string'); 109 | expect(get('ivl')).toBe('string'); 110 | }); 111 | }); 112 | 113 | -------------------------------------------------------------------------------- /packages/cli/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @drzl/cli 2 | 3 | ## 1.1.0 4 | 5 | ### Minor Changes 6 | 7 | - c48d79a: sponsor initiatives 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies [c48d79a] 12 | - @drzl/generator-arktype@1.2.0 13 | - @drzl/generator-service@1.1.0 14 | - @drzl/generator-valibot@1.1.0 15 | - @drzl/generator-orpc@1.1.0 16 | - @drzl/generator-zod@1.1.0 17 | - @drzl/analyzer@1.2.0 18 | 19 | ## 1.0.0 20 | 21 | ### Major Changes 22 | 23 | - 5da6f6b: support MySQL, SingleStore, and Gel; expand Postgres/SQLite; add tests (fixes #13) 24 | 25 | ### Patch Changes 26 | 27 | - Updated dependencies [5da6f6b] 28 | - @drzl/analyzer@1.0.0 29 | - @drzl/generator-arktype@1.0.0 30 | - @drzl/generator-orpc@1.0.0 31 | - @drzl/generator-service@1.0.0 32 | - @drzl/generator-valibot@1.0.0 33 | - @drzl/generator-zod@1.0.0 34 | 35 | ## 0.3.1 36 | 37 | ### Patch Changes 38 | 39 | - Updated dependencies [811dd61] 40 | - @drzl/generator-service@0.4.0 41 | - @drzl/generator-orpc@0.4.0 42 | 43 | ## 0.3.0 44 | 45 | ### Minor Changes 46 | 47 | - b2b8e35: fix(cli-config): improve watch and config loading 48 | 49 | ### Patch Changes 50 | 51 | - @drzl/analyzer@0.3.0 52 | - @drzl/generator-arktype@0.3.0 53 | - @drzl/generator-orpc@0.3.0 54 | - @drzl/generator-service@0.3.0 55 | - @drzl/generator-valibot@0.3.0 56 | - @drzl/generator-zod@0.3.0 57 | 58 | ## 0.2.0 59 | 60 | ### Minor Changes 61 | 62 | - f007329: fix(cli): correct config types and update readme 63 | 64 | ### Patch Changes 65 | 66 | - @drzl/analyzer@0.2.0 67 | - @drzl/generator-arktype@0.2.0 68 | - @drzl/generator-orpc@0.2.0 69 | - @drzl/generator-service@0.2.0 70 | - @drzl/generator-valibot@0.2.0 71 | - @drzl/generator-zod@0.2.0 72 | 73 | ## 0.1.0 74 | 75 | ### Minor Changes 76 | 77 | - 250f5fd: Ensure @drzl/cli builds and exports its config module so consumers can import it directly. 78 | 79 | ### Patch Changes 80 | 81 | - @drzl/analyzer@0.1.0 82 | - @drzl/generator-arktype@0.1.0 83 | - @drzl/generator-orpc@0.1.0 84 | - @drzl/generator-service@0.1.0 85 | - @drzl/generator-valibot@0.1.0 86 | - @drzl/generator-zod@0.1.0 87 | 88 | ## 0.0.3 89 | 90 | ### Patch Changes 91 | 92 | - 4227090: Fix missing generator dependencies and improve error messages 93 | - Add all generator packages (@drzl/generator-zod, @drzl/generator-service, @drzl/generator-valibot, @drzl/generator-arktype) as dependencies in CLI package.json 94 | - Update error handling to provide clearer installation instructions when generators are missing 95 | - Separate error details for better readability 96 | 97 | This resolves the "Cannot find package" errors when using generators other than ORPC. 98 | - @drzl/analyzer@0.0.3 99 | - @drzl/generator-arktype@0.0.3 100 | - @drzl/generator-orpc@0.0.3 101 | - @drzl/generator-service@0.0.3 102 | - @drzl/generator-valibot@0.0.3 103 | - @drzl/generator-zod@0.0.3 104 | 105 | ## 0.0.2 106 | 107 | ### Patch Changes 108 | 109 | - Fix missing generator dependencies and improve error messages 110 | - Add all generator packages (@drzl/generator-zod, @drzl/generator-service, @drzl/generator-valibot, @drzl/generator-arktype) as dependencies in CLI package.json 111 | - Update error handling to provide clearer installation instructions when generators are missing 112 | - Separate error details for better readability 113 | 114 | This resolves the "Cannot find package" errors when using generators other than ORPC. 115 | - @drzl/analyzer@0.0.2 116 | - @drzl/generator-arktype@0.0.2 117 | - @drzl/generator-orpc@0.0.2 118 | - @drzl/generator-service@0.0.2 119 | - @drzl/generator-valibot@0.0.2 120 | - @drzl/generator-zod@0.0.2 121 | 122 | ## 0.0.1 123 | 124 | ### Patch Changes 125 | 126 | - 6130ad2: Initial public release setup: lockstep versioning, CI publish, and branding. 127 | - @drzl/analyzer@0.0.1 128 | - @drzl/generator-orpc@0.0.1 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | DRZL – Zero‑friction codegen for Drizzle ORM 3 | 4 |

5 | 6 |
7 | 8 | # DRZL 9 | 10 | Zero‑friction codegen for Drizzle ORM. Analyze your schema. Generate validation, services, and routers — fast. 11 | 12 |
13 | 14 |

15 | CI 16 | npm 17 | pnpm 18 | typescript 19 | node 20 |

21 | 22 |
23 | 24 | ## Sponsor 25 | 26 | - GitHub Sponsors: https://github.com/sponsors/omar-dulaimi 27 | - Want something prioritized? Look for issues labeled `sponsor-wanted` (or open one) and DM @omardulaimidev so we can reserve it for you. 28 | - Custom template/generator/adapter requests happen via X DM. Paid work ships back into DRZL so everyone benefits. 29 | 30 | ## What’s Inside 31 | 32 | - Analyzer: turns Drizzle schemas into a normalized analysis model 33 | - Generators: Zod, Valibot, ArkType validation; typed CRUD services; router templates (oRPC) 34 | - Batteries: formatting, naming, reusable/shared schemas, relation support 35 | - Monorepo: pnpm workspace, lockstep releases with Changesets 36 | 37 | ## Install & Use 38 | 39 | - Install the CLI and init a config 40 | 41 | ```bash 42 | pnpm add @drzl/cli -D 43 | pnpm drzl init 44 | ``` 45 | 46 | - Generate code 47 | 48 | ```bash 49 | pnpm drzl generate -c drzl.config.ts 50 | ``` 51 | 52 | Minimal config 53 | 54 | ```ts 55 | // drzl.config.ts 56 | import { defineConfig } from '@drzl/cli/config'; 57 | 58 | export default defineConfig({ 59 | schema: 'src/db/schemas/index.ts', 60 | outDir: 'src/api', 61 | generators: [ 62 | { kind: 'zod', path: 'src/validators/zod' }, 63 | { kind: 'service', path: 'src/services', dataAccess: 'drizzle' }, 64 | { kind: 'orpc', template: '@drzl/template-orpc-service' }, 65 | ], 66 | }); 67 | ``` 68 | 69 | Runtime 70 | 71 | - ESM / ES2021 output 72 | - Node ≥ 18.17 (tested on Node 20+) 73 | 74 | ## Packages 75 | 76 | - `packages/analyzer` — schema analysis 77 | - `packages/cli` — CLI (`drzl`) 78 | - `packages/generator-orpc` — oRPC router generator 79 | - `packages/generator-service` — typed service generator 80 | - `packages/generator-zod` — Zod generator 81 | - `packages/generator-valibot` — Valibot generator 82 | - `packages/generator-arktype` — ArkType generator 83 | - `packages/validation-core` — shared validation utilities 84 | - `packages/template-orpc-service` — oRPC router template (service‑backed) 85 | - `packages/template-standard` — minimal oRPC router template 86 | 87 | See each package’s README for details. 88 | 89 | ## Development 90 | 91 | - Install: `pnpm install` 92 | - Build: `pnpm -r run build` 93 | - Test: `pnpm -r test` 94 | - Lint: `pnpm lint` 95 | 96 | ## Docs 97 | 98 | VitePress site lives in `docs/` (kept out of releases). Local dev: 99 | 100 | ```bash 101 | pnpm -C docs dev 102 | ``` 103 | 104 | ## Funded Features 105 | 106 | - _None yet — be the first!_ If you need a template, generator, or adapter that doesn’t exist yet, DM me on X (https://x.com/omardulaimidev) and we can scope a sponsored build. Funded work lands in this repo under Apache‑2.0 so everyone benefits. 107 | 108 | ## Contributing 109 | 110 | Contributions welcome — see [CONTRIBUTING.md](./CONTRIBUTING.md). 111 | 112 | ## License 113 | 114 | Apache-2.0 115 | -------------------------------------------------------------------------------- /packages/analyzer/test/singlestore-types.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { SchemaAnalyzer } from '../src/index'; 3 | import { promises as fs } from 'node:fs'; 4 | import path from 'node:path'; 5 | 6 | describe('Analyzer SingleStore coarse type inference', () => { 7 | it('maps common SingleStore constructor names to ts types', async () => { 8 | const dir = path.resolve(__dirname, 'fixtures'); 9 | await fs.mkdir(dir, { recursive: true }); 10 | const schema = path.join(dir, 'singlestore-types.mjs'); 11 | const code = ` 12 | class SingleStoreVarchar {} 13 | class SingleStoreInt {} 14 | class SingleStoreTinyInt {} 15 | class SingleStoreSmallInt {} 16 | class SingleStoreMediumInt {} 17 | class SingleStoreSerial {} 18 | class SingleStoreBigInt53 {} 19 | class SingleStoreBigInt64 {} 20 | class SingleStoreBoolean {} 21 | class SingleStoreDecimal {} 22 | class SingleStoreDouble {} 23 | class SingleStoreReal {} 24 | class SingleStoreDate {} 25 | class SingleStoreDateTime {} 26 | class SingleStoreTimestamp {} 27 | class SingleStoreDateString {} 28 | class SingleStoreDateTimeString {} 29 | class SingleStoreTimestampString {} 30 | class SingleStoreTime {} 31 | class SingleStoreYear {} 32 | class SingleStoreJson {} 33 | class SingleStoreBlob {} 34 | class SingleStoreBinary {} 35 | class SingleStoreVarBinary {} 36 | class SingleStoreVector {} 37 | 38 | const table = {}; 39 | table[Symbol.for('drizzle:Name')] = 'things'; 40 | table[Symbol.for('drizzle:Columns')] = { 41 | id: new SingleStoreInt(), 42 | tiny: new SingleStoreTinyInt(), 43 | small: new SingleStoreSmallInt(), 44 | medium: new SingleStoreMediumInt(), 45 | serial: new SingleStoreSerial(), 46 | big53: new SingleStoreBigInt53(), 47 | big64: new SingleStoreBigInt64(), 48 | flag: new SingleStoreBoolean(), 49 | price: new SingleStoreDecimal(), 50 | dbl: new SingleStoreDouble(), 51 | rl: new SingleStoreReal(), 52 | name: new SingleStoreVarchar(), 53 | dateCol: new SingleStoreDate(), 54 | dtCol: new SingleStoreDateTime(), 55 | tsCol: new SingleStoreTimestamp(), 56 | dateStr: new SingleStoreDateString(), 57 | dtStr: new SingleStoreDateTimeString(), 58 | tsStr: new SingleStoreTimestampString(), 59 | timeCol: new SingleStoreTime(), 60 | yearCol: new SingleStoreYear(), 61 | payload: new SingleStoreJson(), 62 | bin: new SingleStoreBinary(), 63 | vbin: new SingleStoreVarBinary(), 64 | blob: new SingleStoreBlob(), 65 | vec: new SingleStoreVector(), 66 | }; 67 | 68 | export { table as things }; 69 | `; 70 | await fs.writeFile(schema, code, 'utf8'); 71 | const a = new SchemaAnalyzer(schema); 72 | const res = await a.analyze(); 73 | const t = res.tables.find((t) => t.name === 'things'); 74 | expect(t).toBeTruthy(); 75 | const get = (n: string) => new Map(t!.columns.map((c) => [c.name, c.tsType])).get(n); 76 | expect(get('id')).toBe('number'); 77 | expect(get('tiny')).toBe('number'); 78 | expect(get('small')).toBe('number'); 79 | expect(get('medium')).toBe('number'); 80 | expect(get('serial')).toBe('number'); 81 | expect(get('big53')).toBe('number'); 82 | expect(get('big64')).toBe('bigint'); 83 | expect(get('flag')).toBe('boolean'); 84 | expect(get('price')).toBe('number'); 85 | expect(get('dbl')).toBe('number'); 86 | expect(get('rl')).toBe('number'); 87 | expect(get('name')).toBe('string'); 88 | expect(get('dateCol')).toBe('Date'); 89 | expect(get('dtCol')).toBe('Date'); 90 | expect(get('tsCol')).toBe('Date'); 91 | expect(get('dateStr')).toBe('string'); 92 | expect(get('dtStr')).toBe('string'); 93 | expect(get('tsStr')).toBe('string'); 94 | expect(get('timeCol')).toBe('string'); 95 | expect(get('yearCol')).toBe('number'); 96 | expect(get('payload')).toBe('any'); 97 | expect(get('bin')).toBe('Uint8Array'); 98 | expect(get('vbin')).toBe('Uint8Array'); 99 | expect(get('blob')).toBe('Uint8Array'); 100 | expect(get('vec')).toBe('any'); 101 | }); 102 | }); 103 | 104 | -------------------------------------------------------------------------------- /docs/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'DRZL', 3 | description: 'Developer tooling for Drizzle ORM', 4 | base: '/drzl/', 5 | head: [ 6 | ['link', { rel: 'icon', href: '/favicon.ico' }], 7 | ['link', { rel: 'apple-touch-icon', href: '/apple-touch-icon.png' }], 8 | ['link', { rel: 'icon', type: 'image/png', sizes: '192x192', href: '/icon-192.png' }], 9 | ['link', { rel: 'icon', type: 'image/png', sizes: '512x512', href: '/icon-512.png' }], 10 | ['meta', { property: 'og:image', content: '/social-card.png' }], 11 | ], 12 | themeConfig: { 13 | logo: { light: '/brand/logo.png', dark: '/brand/logo-dark.png' }, 14 | docFooter: { 15 | prev: '← Previous', 16 | next: 'Next →', 17 | }, 18 | outline: { 19 | level: [2, 3], 20 | label: 'On this page', 21 | }, 22 | nav: [ 23 | { text: 'Roadmap', link: '/roadmap/premium-templates' }, 24 | { text: 'Sponsor', link: '/sponsor' }, 25 | { 26 | text: 'Request Template', 27 | link: 'https://x.com/omardulaimidev', 28 | target: '_blank', 29 | rel: 'noreferrer', 30 | }, 31 | ], 32 | sidebar: [ 33 | { 34 | text: 'Guide', 35 | collapsed: false, 36 | items: [ 37 | { text: 'Getting Started', link: '/guide/getting-started' }, 38 | { text: 'Configuration', link: '/guide/configuration' }, 39 | ], 40 | }, 41 | { 42 | text: 'CLI', 43 | collapsed: false, 44 | items: [ 45 | { text: 'Overview', link: '/cli' }, 46 | { text: 'Init', link: '/cli/init' }, 47 | { text: 'Analyze', link: '/cli/analyze' }, 48 | { text: 'Generate', link: '/cli/generate' }, 49 | { text: 'Generate (oRPC)', link: '/cli/generate-orpc' }, 50 | { text: 'Watch', link: '/cli/watch' }, 51 | ], 52 | }, 53 | { 54 | text: 'Generators', 55 | collapsed: false, 56 | items: [ 57 | { text: 'oRPC', link: '/generators/orpc' }, 58 | { text: 'Service', link: '/generators/service' }, 59 | { text: 'Zod', link: '/generators/zod' }, 60 | { text: 'Valibot', link: '/generators/valibot' }, 61 | { text: 'ArkType', link: '/generators/arktype' }, 62 | { text: 'Adapters (Overview)', link: '/adapters/overview' }, 63 | { text: 'Router Adapters', link: '/adapters/router' }, 64 | ], 65 | }, 66 | { 67 | text: 'Templates', 68 | collapsed: false, 69 | items: [ 70 | { text: 'oRPC + Service', link: '/templates/orpc-service' }, 71 | { text: 'Standard', link: '/templates/standard' }, 72 | { text: 'Custom', link: '/templates/custom' }, 73 | ], 74 | }, 75 | { 76 | text: 'Packages', 77 | collapsed: false, 78 | items: [ 79 | { text: 'Analyzer', link: '/packages/analyzer' }, 80 | { text: 'Validation Core', link: '/packages/validation-core' }, 81 | ], 82 | }, 83 | { 84 | text: 'Examples', 85 | collapsed: false, 86 | items: [ 87 | { text: 'Relations', link: '/examples/relations' }, 88 | { text: 'Validation Mix', link: '/examples/validation-mix' }, 89 | ], 90 | }, 91 | { 92 | text: 'Roadmap', 93 | collapsed: false, 94 | items: [{ text: 'Premium Templates', link: '/roadmap/premium-templates' }], 95 | }, 96 | { 97 | text: 'Sponsor', 98 | collapsed: false, 99 | items: [{ text: 'Sponsor DRZL', link: '/sponsor' }], 100 | }, 101 | ], 102 | socialLinks: [ 103 | { icon: 'github', link: 'https://github.com/use-drzl/drzl' }, 104 | { icon: 'discord', link: 'https://github.com/use-drzl/drzl/discussions' }, 105 | ], 106 | footer: { 107 | message: 'Need a custom template or integration? DM @omardulaimidev on X.', 108 | copyright: 'Copyright © 2025 Omar Dulaimi · DRZL is Apache-2.0', 109 | }, 110 | }, 111 | }; 112 | -------------------------------------------------------------------------------- /packages/generator-orpc/test/generator.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { ORPCGenerator } from '../src'; 3 | import type { Analysis } from '@drzl/analyzer'; 4 | import { promises as fs } from 'node:fs'; 5 | import path from 'node:path'; 6 | import os from 'node:os'; 7 | 8 | describe('@drzl/generator-orpc', () => { 9 | it('generates router with shared Zod schemas', async () => { 10 | const analysis: Analysis = { 11 | dialect: 'sqlite', 12 | tables: [ 13 | { 14 | name: 'users', 15 | tsName: 'users', 16 | columns: [ 17 | { 18 | name: 'id', 19 | tsType: 'number', 20 | dbType: 'INTEGER', 21 | nullable: false, 22 | hasDefault: true, 23 | isGenerated: true, 24 | }, 25 | { 26 | name: 'email', 27 | tsType: 'string', 28 | dbType: 'TEXT', 29 | nullable: false, 30 | hasDefault: false, 31 | isGenerated: false, 32 | }, 33 | ], 34 | unique: [{ columns: ['email'] }], 35 | indexes: [], 36 | } as any, 37 | ], 38 | enums: [], 39 | relations: [], 40 | issues: [], 41 | }; 42 | 43 | const outDir = await fs.mkdtemp(path.join(os.tmpdir(), 'drzl-orpc-')); 44 | try { 45 | const gen = new ORPCGenerator(analysis); 46 | const { files } = await gen.generate({ outputDir: outDir, template: 'standard' }); 47 | // one router file + index barrel 48 | expect(files.length).toBe(2); 49 | const routerFile = files.find((f) => /users/i.test(path.basename(f))) ?? files[0]; 50 | const content = await fs.readFile(routerFile, 'utf8'); 51 | expect(content).toContain('export const Insertusers'); 52 | expect(content).toContain('export const Updateusers'); 53 | expect(content).toContain('export const Selectusers'); 54 | } finally { 55 | await fs.rm(outDir, { recursive: true, force: true }); 56 | } 57 | }); 58 | 59 | it('attaches output schemas for valibot and arktype', async () => { 60 | const analysis: Analysis = { 61 | dialect: 'sqlite', 62 | tables: [ 63 | { 64 | name: 'users', 65 | tsName: 'users', 66 | columns: [ 67 | { 68 | name: 'id', 69 | tsType: 'number', 70 | dbType: 'INTEGER', 71 | nullable: false, 72 | hasDefault: true, 73 | isGenerated: true, 74 | }, 75 | { 76 | name: 'email', 77 | tsType: 'string', 78 | dbType: 'TEXT', 79 | nullable: false, 80 | hasDefault: false, 81 | isGenerated: false, 82 | }, 83 | ], 84 | unique: [], 85 | indexes: [], 86 | } as any, 87 | ], 88 | enums: [], 89 | relations: [], 90 | issues: [], 91 | }; 92 | const outDir = await fs.mkdtemp(path.join(os.tmpdir(), 'drzl-orpc-out-')); 93 | try { 94 | // valibot 95 | let gen = new ORPCGenerator(analysis); 96 | let res = await gen.generate({ 97 | outputDir: outDir, 98 | template: 'standard', 99 | validation: { library: 'valibot' }, 100 | }); 101 | let routerFile = res.files.find((f) => /users/i.test(path.basename(f))) ?? res.files[0]; 102 | let content = await fs.readFile(routerFile, 'utf8'); 103 | expect(content).toContain('.output(v.'); 104 | // arktype 105 | gen = new ORPCGenerator(analysis); 106 | res = await gen.generate({ 107 | outputDir: outDir, 108 | template: 'standard', 109 | validation: { library: 'arktype' }, 110 | }); 111 | routerFile = res.files.find((f) => /users/i.test(path.basename(f))) ?? res.files[0]; 112 | content = await fs.readFile(routerFile, 'utf8'); 113 | expect(content).toContain('.output(SelectusersSchema'); 114 | } finally { 115 | await fs.rm(outDir, { recursive: true, force: true }); 116 | } 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /packages/template-standard/src/index.ts: -------------------------------------------------------------------------------- 1 | // Minimal local Table shape to avoid cross-package DTS complexity 2 | interface Table { 3 | name: string; 4 | tsName: string; 5 | } 6 | 7 | const cap = (s: string) => s.charAt(0).toUpperCase() + s.slice(1); 8 | 9 | export interface ProcedureSpec { 10 | name: string; 11 | varName: string; 12 | code: string; 13 | } 14 | export interface ORPCTemplateHooks { 15 | filePath( 16 | table: Table, 17 | ctx: { 18 | outDir: string; 19 | naming?: { routerSuffix?: string; procedureCase?: 'camel' | 'kebab' | 'snake' }; 20 | } 21 | ): string; 22 | routerName( 23 | table: Table, 24 | ctx: { naming?: { routerSuffix?: string; procedureCase?: 'camel' | 'kebab' | 'snake' } } 25 | ): string; 26 | procedures(table: Table): ProcedureSpec[]; 27 | imports?(tables: Table[]): string; 28 | prelude?(tables: Table[]): string; 29 | header?(table: Table): string; 30 | } 31 | 32 | const template: ORPCTemplateHooks = { 33 | filePath: (table, ctx) => { 34 | const suffix = ctx.naming?.routerSuffix ?? ''; 35 | const base = `${table.tsName}${suffix}`; 36 | const procCase = ctx.naming?.procedureCase; 37 | const toCase = (s: string) => { 38 | if (!procCase) return s; 39 | const parts = s 40 | .replace(/([a-z0-9])([A-Z])/g, '$1 $2') 41 | .replace(/[_-]/g, ' ') 42 | .split(/\s+/); 43 | if (procCase === 'camel') 44 | return parts 45 | .map((p, i) => 46 | i === 0 ? p.toLowerCase() : p[0].toUpperCase() + p.slice(1).toLowerCase() 47 | ) 48 | .join(''); 49 | if (procCase === 'kebab') return parts.map((p) => p.toLowerCase()).join('-'); 50 | if (procCase === 'snake') return parts.map((p) => p.toLowerCase()).join('_'); 51 | return s; 52 | }; 53 | const fileBase = toCase(base); 54 | return `${ctx.outDir}/${fileBase}.ts`; 55 | }, 56 | routerName: (table, ctx) => { 57 | const suffix = ctx.naming?.routerSuffix ?? ''; 58 | const base = `${table.tsName}${suffix}`; 59 | const procCase = ctx.naming?.procedureCase; 60 | const toCase = (s: string) => { 61 | if (!procCase) return s; 62 | const parts = s 63 | .replace(/([a-z0-9])([A-Z])/g, '$1 $2') 64 | .replace(/[_-]/g, ' ') 65 | .split(/\s+/); 66 | // Kebab is invalid for identifiers; treat as camel here 67 | if (procCase === 'camel' || procCase === 'kebab') 68 | return parts 69 | .map((p, i) => 70 | i === 0 ? p.toLowerCase() : p[0].toUpperCase() + p.slice(1).toLowerCase() 71 | ) 72 | .join(''); 73 | if (procCase === 'snake') return parts.map((p) => p.toLowerCase()).join('_'); 74 | return s; 75 | }; 76 | return toCase(base); 77 | }, 78 | imports: () => `import { os } from '@orpc/server'\nimport { z } from 'zod'`, 79 | prelude: () => '', 80 | header: (table) => `// Router for table: ${table.name}`, 81 | procedures: (table) => { 82 | const T = cap(table.tsName); 83 | const make = (proc: string, varName: string, code: string): ProcedureSpec => ({ 84 | name: proc, 85 | varName, 86 | code, 87 | }); 88 | const listVar = `list${T}`; 89 | const getVar = `get${T}`; 90 | const createVar = `create${T}`; 91 | const updateVar = `update${T}`; 92 | const deleteVar = `delete${T}`; 93 | return [ 94 | make('list', listVar, `const ${listVar} = os.handler(async () => { return []; });`), 95 | make( 96 | 'get', 97 | getVar, 98 | `const ${getVar} = os.input(z.object({ id: z.number() })).handler(async ({ input: _input }) => { return null; });` 99 | ), 100 | make( 101 | 'create', 102 | createVar, 103 | `const ${createVar} = os.input(z.any()).handler(async ({ input: _input }) => { return _input; });` 104 | ), 105 | make( 106 | 'update', 107 | updateVar, 108 | `const ${updateVar} = os.input(z.object({ id: z.number(), data: z.any() })).handler(async ({ input: _input }) => { return _input.data; });` 109 | ), 110 | make( 111 | 'delete', 112 | deleteVar, 113 | `const ${deleteVar} = os.input(z.object({ id: z.number() })).handler(async ({ input: _input }) => { return true; });` 114 | ), 115 | ]; 116 | }, 117 | }; 118 | 119 | export default template; 120 | -------------------------------------------------------------------------------- /docs/generators/orpc.md: -------------------------------------------------------------------------------- 1 | # oRPC Generator 2 | 3 | Generates oRPC routers per table, with optional validation reuse (Zod, Valibot, ArkType). 4 | 5 | ## Options 6 | 7 | ```ts 8 | interface GenerateOptions { 9 | outputDir: string; 10 | template?: 'standard' | 'minimal' | string; 11 | includeRelations?: boolean; 12 | naming?: { routerSuffix?: string; procedureCase?: 'camel' | 'kebab' | 'snake' }; 13 | format?: { enabled?: boolean; engine?: 'auto' | 'prettier' | 'biome'; configPath?: string }; 14 | templateOptions?: Record; 15 | validation?: { 16 | useShared?: boolean; 17 | library?: 'zod' | 'valibot' | 'arktype'; 18 | importPath?: string; 19 | schemaSuffix?: string; 20 | }; 21 | databaseInjection?: { 22 | enabled?: boolean; 23 | databaseType?: string; 24 | databaseTypeImport?: { name: string; from: string }; 25 | }; 26 | servicesDir?: string; 27 | } 28 | ``` 29 | 30 | ## Database injection 31 | 32 | When used with `@drzl/template-orpc-service`, the generator wires a `dbMiddleware` and passes `context.db` to your services. Configure the context type via `databaseInjection`. 33 | 34 | ```ts 35 | validation: { library: 'valibot' }, 36 | databaseInjection: { 37 | enabled: true, 38 | databaseType: 'Database', 39 | databaseTypeImport: { name: 'Database', from: 'src/db/db' }, 40 | }, 41 | servicesDir: 'src/services', 42 | ``` 43 | 44 | In the generated router: 45 | 46 | ```ts 47 | import type { Database } from 'src/db/db'; 48 | 49 | export const dbMiddleware = os.$context<{ db?: Database }>().middleware(/* ... */); 50 | ``` 51 | 52 | ## Validation reuse 53 | 54 | When `validation.useShared` is true, the generator imports `Insert
Schema`, `Update
Schema`, and `Select
Schema` from your `validation.importPath` and wires them into handlers (inputs and outputs) based on the selected `library`. 55 | 56 | ### Output typing 57 | 58 | The generator attaches `.output(...)` to each procedure. For example (Zod): 59 | 60 | ```ts 61 | // list 62 | os.output(z.array(SelectusersSchema)).handler(...) 63 | // get 64 | os.output(SelectusersSchema.nullable()).handler(...) 65 | // create/update 66 | os.output(SelectusersSchema).handler(...) 67 | // delete 68 | os.output(z.boolean()).handler(...) 69 | ``` 70 | 71 | Valibot uses `v.array(...)` and `v.nullable(...)`; ArkType uses `SelectSchema.array()` and `SelectSchema.or('null')`. 72 | 73 | ## Example 74 | 75 | ```ts 76 | // drzl.config.ts 77 | export default defineConfig({ 78 | schema: 'src/db/schemas/index.ts', 79 | outDir: 'src/api', 80 | generators: [ 81 | { 82 | kind: 'orpc', 83 | template: '@drzl/template-orpc-service', 84 | includeRelations: true, 85 | naming: { routerSuffix: 'Router', procedureCase: 'kebab' }, 86 | validation: { 87 | useShared: true, 88 | library: 'zod', 89 | importPath: 'src/validators/zod', 90 | schemaSuffix: 'Schema', 91 | }, 92 | }, 93 | ], 94 | }); 95 | ``` 96 | 97 | Run: 98 | 99 | ```bash 100 | drzl generate -c drzl.config.ts 101 | ``` 102 | 103 | Routers and an index barrel are generated at `outDir`. 104 | 105 | ## Template hooks API 106 | 107 | Templates expose a small API used by the generator. 108 | 109 | ```ts 110 | interface ORPCTemplateHooks { 111 | filePath(table, ctx): string; 112 | routerName(table, ctx): string; 113 | procedures(table): Array<{ name: string; varName: string; code: string }>; 114 | imports?(tables, ctx): string; 115 | prelude?(tables, ctx): string; 116 | header?(table): string; 117 | } 118 | ``` 119 | 120 | - `filePath`: absolute output path for a table’s router 121 | - `routerName`: exported const name of the router 122 | - `procedures`: code snippets for each handler variable (`varName`) and exported key (`name`) 123 | - `imports`: extra imports at file top 124 | - `prelude`: code emitted after imports (utility helpers, etc.) 125 | - `header`: a banner/comment string at the top of the file 126 | 127 | See also: [oRPC + Service Template](/templates/orpc-service) and [Standard Template](/templates/standard) 128 | 129 | ## Generated Output License 130 | 131 | - You own the generated output. DRZL grants you a worldwide, royalty‑free, irrevocable license to use, copy, modify, and distribute the generated files under your project’s license. 132 | - A short header is added by default. Configure via `outputHeader` in `drzl.config.ts`: 133 | - `outputHeader.enabled = false` to disable 134 | - `outputHeader.text = '...'` to customize 135 | 136 | ::: tip Need something else? 137 | If this generator doesn't cover what you need, DM me on X (https://x.com/omardulaimidev) and we can scope it together. 138 | ::: 139 | -------------------------------------------------------------------------------- /docs/cli.md: -------------------------------------------------------------------------------- 1 | # CLI Overview 2 | 3 | The `drzl` CLI analyzes Drizzle schemas and runs generators. 4 | 5 | Quick help: 6 | 7 | ::: code-group 8 | 9 | ```bash [pnpm] 10 | pnpm dlx @drzl/cli --help 11 | ``` 12 | 13 | ```bash [npm] 14 | npx @drzl/cli --help 15 | ``` 16 | 17 | ```bash [yarn] 18 | yarn dlx @drzl/cli --help 19 | ``` 20 | 21 | ```bash [bun] 22 | bunx @drzl/cli --help 23 | ``` 24 | 25 | ::: 26 | 27 | Jump to commands: 28 | 29 | - Init: set up a starter config → [/cli/init](/cli/init) 30 | - Analyze: inspect a schema → [/cli/analyze](/cli/analyze) 31 | - Generate: run configured generators → [/cli/generate](/cli/generate) 32 | - Generate (oRPC): quick oRPC without config → [/cli/generate-orpc](/cli/generate-orpc) 33 | - Watch: watch schema and regenerate → [/cli/watch](/cli/watch) 34 | 35 | ## Commands & Options 36 | 37 | ### analyze 38 | 39 | Analyze a Drizzle schema (TypeScript) and output a normalized Analysis. 40 | 41 | Usage (by package manager): 42 | 43 | ::: code-group 44 | 45 | ```bash [pnpm] 46 | pnpm dlx @drzl/cli analyze [--relations] [--validate] [--out FILE] [--json] 47 | ``` 48 | 49 | ```bash [npm] 50 | npx @drzl/cli analyze [--relations] [--validate] [--out FILE] [--json] 51 | ``` 52 | 53 | ```bash [yarn] 54 | yarn dlx @drzl/cli analyze [--relations] [--validate] [--out FILE] [--json] 55 | ``` 56 | 57 | ```bash [bun] 58 | bunx @drzl/cli analyze [--relations] [--validate] [--out FILE] [--json] 59 | ``` 60 | 61 | ::: 62 | 63 | Options: 64 | 65 | - `--relations` (default true): include relation inference 66 | - `--validate` (default true): validate constraints 67 | - `--out `: write JSON to file 68 | - `--json` (default false): print JSON to stdout (overrides `--out`) 69 | 70 | Exits non‑zero when errors are found in `issues`. 71 | 72 | ### generate 73 | 74 | Run configured generators from `drzl.config.*`. 75 | 76 | Usage: 77 | 78 | ::: code-group 79 | 80 | ```bash [pnpm] 81 | pnpm dlx @drzl/cli generate -c drzl.config.ts 82 | ``` 83 | 84 | ```bash [npm] 85 | npx @drzl/cli generate -c drzl.config.ts 86 | ``` 87 | 88 | ```bash [yarn] 89 | yarn dlx @drzl/cli generate -c drzl.config.ts 90 | ``` 91 | 92 | ```bash [bun] 93 | bunx @drzl/cli generate -c drzl.config.ts 94 | ``` 95 | 96 | ::: 97 | 98 | Options: 99 | 100 | - `-c, --config `: path to config file 101 | 102 | Behavior: 103 | 104 | - Analyzes your schema 105 | - Runs each generator in `generators[]`, printing a file summary per kind 106 | 107 | ### generate:orpc 108 | 109 | Quickly generate oRPC routers without a config. 110 | 111 | Usage: 112 | 113 | ::: code-group 114 | 115 | ```bash [pnpm] 116 | pnpm dlx @drzl/cli generate:orpc -o src/api --template standard --includeRelations 117 | ``` 118 | 119 | ```bash [npm] 120 | npx @drzl/cli generate:orpc -o src/api --template standard --includeRelations 121 | ``` 122 | 123 | ```bash [yarn] 124 | yarn dlx @drzl/cli generate:orpc -o src/api --template standard --includeRelations 125 | ``` 126 | 127 | ```bash [bun] 128 | bunx @drzl/cli generate:orpc -o src/api --template standard --includeRelations 129 | ``` 130 | 131 | ::: 132 | 133 | Options: 134 | 135 | - `-o, --outDir ` (default `src/api`) 136 | - `--template ` (default `standard`) — can be `standard` or a custom path 137 | - `--includeRelations` — include relation endpoints 138 | 139 | ### watch 140 | 141 | Watch schema (and template paths) and regenerate on changes. 142 | 143 | Usage: 144 | 145 | ::: code-group 146 | 147 | ```bash [pnpm] 148 | pnpm dlx @drzl/cli watch -c drzl.config.ts --pipeline all --debounce 200 [--json] 149 | ``` 150 | 151 | ```bash [npm] 152 | npx @drzl/cli watch -c drzl.config.ts --pipeline all --debounce 200 [--json] 153 | ``` 154 | 155 | ```bash [yarn] 156 | yarn dlx @drzl/cli watch -c drzl.config.ts --pipeline all --debounce 200 [--json] 157 | ``` 158 | 159 | ```bash [bun] 160 | bunx @drzl/cli watch -c drzl.config.ts --pipeline all --debounce 200 [--json] 161 | ``` 162 | 163 | ::: 164 | 165 | Options: 166 | 167 | - `-c, --config ` 168 | - `--pipeline `: `all | analyze | generate-orpc` (default `all`) 169 | - `--debounce `: debounce milliseconds (default `200`) 170 | - `--json`: emit structured JSON logs 171 | 172 | ### init 173 | 174 | Scaffold a minimal `drzl.config.ts` in the current directory. 175 | 176 | ::: code-group 177 | 178 | ```bash [pnpm] 179 | pnpm dlx @drzl/cli init 180 | ``` 181 | 182 | ```bash [npm] 183 | npx @drzl/cli init 184 | ``` 185 | 186 | ```bash [yarn] 187 | yarn dlx @drzl/cli init 188 | ``` 189 | 190 | ```bash [bun] 191 | bunx @drzl/cli init 192 | ``` 193 | 194 | ::: 195 | 196 | --- 197 | 198 | See also: 199 | 200 | - Config reference: [/guide/configuration](/guide/configuration) 201 | - Generators: 202 | - [/generators/orpc](/generators/orpc) 203 | - [/generators/service](/generators/service) 204 | - [/generators/zod](/generators/zod) 205 | - [/generators/valibot](/generators/valibot) 206 | - [/generators/arktype](/generators/arktype) 207 | -------------------------------------------------------------------------------- /packages/generator-arktype/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Analysis, Table, Column } from '@drzl/analyzer'; 2 | import type { ValidationRenderer, ValidationGenerateOptions } from '@drzl/validation-core'; 3 | import { insertColumns, updateColumns, selectColumns, formatCode } from '@drzl/validation-core'; 4 | 5 | type Mode = 'insert' | 'update' | 'select'; 6 | 7 | function atDateType( 8 | mode: Mode, 9 | coerceDates: NonNullable 10 | ): string { 11 | if (coerceDates === 'none') return 'Date'; 12 | if (coerceDates === 'all') return 'Date | string'; 13 | // 'input' 14 | return mode === 'select' ? 'Date' : 'Date | string'; 15 | } 16 | 17 | function atTypeForColumn( 18 | c: Column, 19 | mode: Mode, 20 | coerceDates: NonNullable 21 | ): string { 22 | if (c.enumValues && c.enumValues.length) { 23 | return c.enumValues.map((v) => `'${v.replace(/'/g, "\\'")}'`).join(' | '); 24 | } 25 | switch (c.tsType) { 26 | case 'string': 27 | return 'string'; 28 | case 'number': 29 | return 'number'; 30 | case 'bigint': 31 | return 'bigint'; 32 | case 'boolean': 33 | return 'boolean'; 34 | case 'Date': 35 | return atDateType(mode, coerceDates); 36 | case 'Uint8Array': 37 | return 'Uint8Array'; 38 | case 'any': 39 | return 'unknown'; 40 | default: 41 | return 'unknown'; 42 | } 43 | } 44 | 45 | function atField( 46 | c: Column, 47 | mode: Mode, 48 | coerceDates: NonNullable 49 | ): string { 50 | let t = atTypeForColumn(c, mode, coerceDates); 51 | if (c.nullable) t = `(${t} | null)`; 52 | if (mode !== 'select') { 53 | if (mode === 'update' || c.nullable || c.hasDefault) t = `${t}?`; 54 | } 55 | return t; 56 | } 57 | 58 | function renderObjectShape( 59 | cols: Column[], 60 | mode: Mode, 61 | coerceDates: NonNullable 62 | ) { 63 | return cols 64 | .map( 65 | (c) => 66 | ` ${JSON.stringify(c.name)}: ${JSON.stringify(atField(c, mode, coerceDates))},` 67 | ) 68 | .join('\n'); 69 | } 70 | 71 | function renderTableSchemas( 72 | table: Table, 73 | suffix = 'Schema', 74 | coerceDates: NonNullable 75 | ) { 76 | const T = table.tsName; 77 | const insertCols = insertColumns(table); 78 | const updateCols = updateColumns(table); 79 | const selectCols = selectColumns(table); 80 | const bodyInsert = renderObjectShape(insertCols, 'insert', coerceDates); 81 | const bodyUpdate = renderObjectShape(updateCols, 'update', coerceDates); 82 | const bodySelect = renderObjectShape(selectCols, 'select', coerceDates); 83 | return `import { type } from 'arktype'; 84 | 85 | export const Insert${T}${suffix} = type({ 86 | ${bodyInsert} 87 | }); 88 | 89 | export const Update${T}${suffix} = type({ 90 | ${bodyUpdate} 91 | }); 92 | 93 | export const Select${T}${suffix} = type({ 94 | ${bodySelect} 95 | }); 96 | 97 | export type Insert${T}Input = typeof Insert${T}${suffix}["infer"]; 98 | export type Update${T}Input = typeof Update${T}${suffix}["infer"]; 99 | export type Select${T}Output = typeof Select${T}${suffix}["infer"]; 100 | `; 101 | } 102 | 103 | export interface ArkTypeGenerateOptions extends ValidationGenerateOptions { 104 | outputHeader?: { enabled?: boolean; text?: string }; 105 | } 106 | 107 | export class ArkTypeGenerator implements ValidationRenderer { 108 | readonly library = 'arktype' as const; 109 | constructor(private analysis: Analysis) {} 110 | 111 | async generate(opts: ArkTypeGenerateOptions) { 112 | const fs = await import('node:fs/promises'); 113 | const path = await import('node:path'); 114 | const out = path.resolve(process.cwd(), opts.outDir); 115 | const files: string[] = []; 116 | await fs.mkdir(out, { recursive: true }); 117 | const suffix = opts.schemaSuffix ?? 'Schema'; 118 | const coerceDates = opts.coerceDates ?? 'input'; 119 | const fileSuffix = opts.fileSuffix ?? '.arktype.ts'; 120 | for (const table of this.analysis.tables) { 121 | const filePath = path.join(out, `${table.tsName}${fileSuffix}`); 122 | const code = renderTableSchemas(table, suffix, coerceDates); 123 | const formatted = await formatCode( 124 | buildHeader(opts.outputHeader) + code, 125 | filePath, 126 | opts.format 127 | ); 128 | await fs.writeFile(filePath, formatted, 'utf8'); 129 | files.push(filePath); 130 | } 131 | const indexPath = path.join(out, 'index.ts'); 132 | const indexCode = this.defaultIndex(this.analysis, opts); 133 | const indexFormatted = await formatCode( 134 | buildHeader(opts.outputHeader) + indexCode, 135 | indexPath, 136 | opts.format 137 | ); 138 | await fs.writeFile(indexPath, indexFormatted, 'utf8'); 139 | files.push(indexPath); 140 | return files; 141 | } 142 | 143 | renderTable(table: Table, opts?: ArkTypeGenerateOptions) { 144 | return renderTableSchemas(table, opts?.schemaSuffix ?? 'Schema', opts?.coerceDates ?? 'input'); 145 | } 146 | 147 | private defaultIndex(analysis: Analysis, _opts: ArkTypeGenerateOptions) { 148 | const exports = analysis.tables.map((t) => `export * from './${t.tsName}.arktype';`).join('\n'); 149 | return exports + '\n'; 150 | } 151 | } 152 | 153 | export default ArkTypeGenerator; 154 | 155 | function buildHeader(h?: { enabled?: boolean; text?: string }) { 156 | if (h && h.enabled === false) return ''; 157 | const text = h?.text?.trim(); 158 | const lines = text 159 | ? text.split(/\r?\n/).map((l) => `// ${l}`) 160 | : [ 161 | '// Generated by DRZL (@drzl/*)', 162 | "// Generated output is granted to you under your project's license.", 163 | '// You may use, copy, modify, and distribute without attribution.', 164 | ]; 165 | return lines.join('\n') + '\n\n'; 166 | } 167 | -------------------------------------------------------------------------------- /packages/generator-zod/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Analysis, Column, Table } from '@drzl/analyzer'; 2 | import type { ValidationGenerateOptions, ValidationRenderer } from '@drzl/validation-core'; 3 | import { formatCode, insertColumns, selectColumns, updateColumns } from '@drzl/validation-core'; 4 | 5 | type Mode = 'insert' | 'update' | 'select'; 6 | 7 | function zodExprForColumn( 8 | c: Column, 9 | mode: Mode, 10 | coerceDates: NonNullable 11 | ): string { 12 | if (c.enumValues && c.enumValues.length) { 13 | const vals = c.enumValues.map((v) => `'${v.replace(/'/g, "\\'")}'`).join(', '); 14 | return `z.enum([${vals}] as const)`; 15 | } 16 | switch (c.tsType) { 17 | case 'string': 18 | return 'z.string()'; 19 | case 'number': 20 | return c.dbType === 'INTEGER' ? 'z.number().int()' : 'z.number()'; 21 | case 'bigint': 22 | return 'z.bigint()'; 23 | case 'boolean': 24 | return 'z.boolean()'; 25 | case 'Date': 26 | if (coerceDates === 'all') return 'z.coerce.date()'; 27 | if (coerceDates === 'none') return 'z.date()'; 28 | // 'input' 29 | return mode === 'select' ? 'z.date()' : 'z.coerce.date()'; 30 | case 'Uint8Array': 31 | return 'z.instanceof(Uint8Array)'; 32 | case 'any': 33 | return 'z.any()'; 34 | default: 35 | return 'z.unknown()'; 36 | } 37 | } 38 | 39 | function zodField( 40 | c: Column, 41 | mode: Mode, 42 | coerceDates: NonNullable 43 | ): string { 44 | let expr = zodExprForColumn(c, mode, coerceDates); 45 | // For selects, nullable columns should allow null values 46 | if (c.nullable) { 47 | expr = `${expr}.nullable()`; 48 | } 49 | if (mode === 'insert') { 50 | // Omit generated columns at callsite; for remaining fields, 51 | // allow optional when nullable or has default. 52 | if (c.nullable || c.hasDefault) expr = `${expr}.optional()`; 53 | } else if (mode === 'update') { 54 | // All update fields are optional; preserve nullability 55 | expr = `${expr}.optional()`; 56 | } 57 | return expr; 58 | } 59 | 60 | function renderObjectShape( 61 | cols: Column[], 62 | mode: Mode, 63 | coerceDates: NonNullable 64 | ) { 65 | return cols 66 | .map((c) => ` ${JSON.stringify(c.name)}: ${zodField(c, mode, coerceDates)},`) 67 | .join('\n'); 68 | } 69 | 70 | function renderTableSchemas( 71 | table: Table, 72 | suffix = 'Schema', 73 | coerceDates: NonNullable 74 | ) { 75 | const T = table.tsName; 76 | const insertCols = insertColumns(table); 77 | const updateCols = updateColumns(table); 78 | const selectCols = selectColumns(table); 79 | const bodyInsert = renderObjectShape(insertCols, 'insert', coerceDates); 80 | const bodyUpdate = renderObjectShape(updateCols, 'update', coerceDates); 81 | const bodySelect = renderObjectShape(selectCols, 'select', coerceDates); 82 | return `import { z } from 'zod'; 83 | 84 | export const Insert${T}${suffix} = z.object({ 85 | ${bodyInsert} 86 | }); 87 | 88 | export const Update${T}${suffix} = z.object({ 89 | ${bodyUpdate} 90 | }); 91 | 92 | export const Select${T}${suffix} = z.object({ 93 | ${bodySelect} 94 | }); 95 | 96 | export type Insert${T}Input = z.input; 97 | export type Update${T}Input = z.input; 98 | export type Select${T}Output = z.output; 99 | `; 100 | } 101 | 102 | export interface ZodGenerateOptions extends ValidationGenerateOptions { 103 | outputHeader?: { enabled?: boolean; text?: string }; 104 | } 105 | 106 | export class ZodGenerator implements ValidationRenderer { 107 | readonly library = 'zod' as const; 108 | constructor(private analysis: Analysis) {} 109 | 110 | async generate(opts: ZodGenerateOptions) { 111 | const fs = await import('node:fs/promises'); 112 | const path = await import('node:path'); 113 | const out = path.resolve(process.cwd(), opts.outDir); 114 | const files: string[] = []; 115 | await fs.mkdir(out, { recursive: true }); 116 | const suffix = opts.schemaSuffix ?? 'Schema'; 117 | const coerceDates = opts.coerceDates ?? 'input'; 118 | const fileSuffix = opts.fileSuffix ?? '.zod.ts'; 119 | for (const table of this.analysis.tables) { 120 | const filePath = path.join(out, `${table.tsName}${fileSuffix}`); 121 | const code = renderTableSchemas(table, suffix, coerceDates); 122 | const formatted = await formatCode( 123 | buildHeader(opts.outputHeader) + code, 124 | filePath, 125 | opts.format 126 | ); 127 | await fs.writeFile(filePath, formatted, 'utf8'); 128 | files.push(filePath); 129 | } 130 | // Index barrel 131 | const indexPath = path.join(out, 'index.ts'); 132 | const indexCode = 133 | this.renderIndex?.(this.analysis, opts) ?? this.defaultIndex(this.analysis, opts); 134 | const indexFormatted = await formatCode( 135 | buildHeader(opts.outputHeader) + indexCode, 136 | indexPath, 137 | opts.format 138 | ); 139 | await fs.writeFile(indexPath, indexFormatted, 'utf8'); 140 | files.push(indexPath); 141 | return files; 142 | } 143 | 144 | renderTable(table: Table, opts?: ZodGenerateOptions) { 145 | return renderTableSchemas(table, opts?.schemaSuffix ?? 'Schema', opts?.coerceDates ?? 'input'); 146 | } 147 | 148 | renderIndex?(analysis: Analysis, opts?: ZodGenerateOptions): string; 149 | 150 | private defaultIndex(analysis: Analysis, _opts: ZodGenerateOptions) { 151 | const exports = analysis.tables.map((t) => `export * from './${t.tsName}.zod';`).join('\n'); 152 | return exports + '\n'; 153 | } 154 | } 155 | 156 | export default ZodGenerator; 157 | 158 | function buildHeader(h?: { enabled?: boolean; text?: string }) { 159 | if (h && h.enabled === false) return ''; 160 | const text = h?.text?.trim(); 161 | const lines = text 162 | ? text.split(/\r?\n/).map((l) => `// ${l}`) 163 | : [ 164 | '// Generated by DRZL (@drzl/*)', 165 | "// Generated output is granted to you under your project's license.", 166 | '// You may use, copy, modify, and distribute without attribution.', 167 | ]; 168 | return lines.join('\n') + '\n\n'; 169 | } 170 | -------------------------------------------------------------------------------- /packages/generator-valibot/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Analysis, Table, Column } from '@drzl/analyzer'; 2 | import type { ValidationRenderer, ValidationGenerateOptions } from '@drzl/validation-core'; 3 | import { insertColumns, updateColumns, selectColumns, formatCode } from '@drzl/validation-core'; 4 | 5 | type Mode = 'insert' | 'update' | 'select'; 6 | 7 | function vDateExpr( 8 | mode: Mode, 9 | coerceDates: NonNullable 10 | ): string { 11 | if (coerceDates === 'none') return 'v.date()'; 12 | const coercer = `v.pipe(v.string(), v.transform((s) => new Date(s)))`; 13 | if (coerceDates === 'all') return `v.union([v.date(), ${coercer}])`; 14 | // 'input' 15 | return mode === 'select' ? 'v.date()' : `v.union([v.date(), ${coercer}])`; 16 | } 17 | 18 | function vExprForColumn( 19 | c: Column, 20 | mode: Mode, 21 | coerceDates: NonNullable 22 | ): string { 23 | if (c.enumValues && c.enumValues.length) { 24 | const vals = c.enumValues.map((v) => `'${v.replace(/'/g, "\\'")}'`).join(', '); 25 | // picklist is a common valibot helper for string enums 26 | return `v.picklist([${vals}] as const)`; 27 | } 28 | switch (c.tsType) { 29 | case 'string': 30 | return 'v.string()'; 31 | case 'number': 32 | return 'v.number()'; 33 | case 'bigint': 34 | return 'v.bigint()'; 35 | case 'boolean': 36 | return 'v.boolean()'; 37 | case 'Date': 38 | return vDateExpr(mode, coerceDates); 39 | case 'Uint8Array': 40 | return 'v.instance(Uint8Array)'; 41 | case 'any': 42 | return 'v.any()'; 43 | default: 44 | return 'v.unknown()'; 45 | } 46 | } 47 | 48 | function vField( 49 | c: Column, 50 | mode: Mode, 51 | coerceDates: NonNullable 52 | ): string { 53 | let expr = vExprForColumn(c, mode, coerceDates); 54 | if (c.nullable) expr = `v.nullable(${expr})`; 55 | if (mode !== 'select') { 56 | // optional for insert when nullable/hasDefault, and for all fields in update 57 | if (mode === 'update' || c.nullable || c.hasDefault) expr = `v.optional(${expr})`; 58 | } 59 | return expr; 60 | } 61 | 62 | function renderObjectShape( 63 | cols: Column[], 64 | mode: Mode, 65 | coerceDates: NonNullable 66 | ) { 67 | return cols 68 | .map((c) => ` ${JSON.stringify(c.name)}: ${vField(c, mode, coerceDates)},`) 69 | .join('\n'); 70 | } 71 | 72 | function renderTableSchemas( 73 | table: Table, 74 | suffix = 'Schema', 75 | coerceDates: NonNullable 76 | ) { 77 | const T = table.tsName; 78 | const insertCols = insertColumns(table); 79 | const updateCols = updateColumns(table); 80 | const selectCols = selectColumns(table); 81 | const bodyInsert = renderObjectShape(insertCols, 'insert', coerceDates); 82 | const bodyUpdate = renderObjectShape(updateCols, 'update', coerceDates); 83 | const bodySelect = renderObjectShape(selectCols, 'select', coerceDates); 84 | return `import * as v from 'valibot'; 85 | import type { InferInput, InferOutput } from 'valibot'; 86 | 87 | export const Insert${T}${suffix} = v.object({ 88 | ${bodyInsert} 89 | }); 90 | 91 | export const Update${T}${suffix} = v.object({ 92 | ${bodyUpdate} 93 | }); 94 | 95 | export const Select${T}${suffix} = v.object({ 96 | ${bodySelect} 97 | }); 98 | 99 | export type Insert${T}Input = InferInput; 100 | export type Update${T}Input = InferInput; 101 | export type Select${T}Output = InferOutput; 102 | `; 103 | } 104 | 105 | export interface ValibotGenerateOptions extends ValidationGenerateOptions { 106 | outputHeader?: { enabled?: boolean; text?: string }; 107 | } 108 | 109 | export class ValibotGenerator implements ValidationRenderer { 110 | readonly library = 'valibot' as const; 111 | constructor(private analysis: Analysis) {} 112 | 113 | async generate(opts: ValibotGenerateOptions) { 114 | const fs = await import('node:fs/promises'); 115 | const path = await import('node:path'); 116 | const out = path.resolve(process.cwd(), opts.outDir); 117 | const files: string[] = []; 118 | await fs.mkdir(out, { recursive: true }); 119 | const suffix = opts.schemaSuffix ?? 'Schema'; 120 | const coerceDates = opts.coerceDates ?? 'input'; 121 | const fileSuffix = opts.fileSuffix ?? '.valibot.ts'; 122 | for (const table of this.analysis.tables) { 123 | const filePath = path.join(out, `${table.tsName}${fileSuffix}`); 124 | const code = renderTableSchemas(table, suffix, coerceDates); 125 | const formatted = await formatCode( 126 | buildHeader(opts.outputHeader) + code, 127 | filePath, 128 | opts.format 129 | ); 130 | await fs.writeFile(filePath, formatted, 'utf8'); 131 | files.push(filePath); 132 | } 133 | const indexPath = path.join(out, 'index.ts'); 134 | const indexCode = this.defaultIndex(this.analysis, opts); 135 | const indexFormatted = await formatCode( 136 | buildHeader(opts.outputHeader) + indexCode, 137 | indexPath, 138 | opts.format 139 | ); 140 | await fs.writeFile(indexPath, indexFormatted, 'utf8'); 141 | files.push(indexPath); 142 | return files; 143 | } 144 | 145 | renderTable(table: Table, opts?: ValibotGenerateOptions) { 146 | return renderTableSchemas(table, opts?.schemaSuffix ?? 'Schema', opts?.coerceDates ?? 'input'); 147 | } 148 | 149 | private defaultIndex(analysis: Analysis, _opts: ValibotGenerateOptions) { 150 | const exports = analysis.tables.map((t) => `export * from './${t.tsName}.valibot';`).join('\n'); 151 | return exports + '\n'; 152 | } 153 | } 154 | 155 | export default ValibotGenerator; 156 | 157 | function buildHeader(h?: { enabled?: boolean; text?: string }) { 158 | if (h && h.enabled === false) return ''; 159 | const text = h?.text?.trim(); 160 | const lines = text 161 | ? text.split(/\r?\n/).map((l) => `// ${l}`) 162 | : [ 163 | '// Generated by DRZL (@drzl/*)', 164 | "// Generated output is granted to you under your project's license.", 165 | '// You may use, copy, modify, and distribute without attribution.', 166 | ]; 167 | return lines.join('\n') + '\n\n'; 168 | } 169 | -------------------------------------------------------------------------------- /packages/cli/src/config.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs'; 2 | import { createRequire } from 'node:module'; 3 | import * as path from 'node:path'; 4 | import { z } from 'zod'; 5 | 6 | export const NamingSchema = z 7 | .object({ 8 | routerSuffix: z.string().default('Router'), 9 | procedureCase: z.enum(['camel', 'kebab', 'snake']).default('camel'), 10 | }) 11 | .partial(); 12 | 13 | export const GeneratorSchema = z.object({ 14 | kind: z.enum(['orpc', 'service', 'zod', 'valibot', 'arktype']), 15 | template: z.string().optional(), 16 | includeRelations: z.boolean().optional(), 17 | naming: NamingSchema.optional(), 18 | outputHeader: z 19 | .object({ 20 | enabled: z.boolean().default(true).optional(), 21 | text: z.string().optional(), 22 | }) 23 | .optional(), 24 | format: z 25 | .object({ 26 | enabled: z.boolean().default(true).optional(), 27 | engine: z.enum(['auto', 'prettier', 'biome']).default('auto').optional(), 28 | configPath: z.string().optional(), 29 | }) 30 | .optional(), 31 | // service generator specific options 32 | path: z.string().optional(), 33 | dataAccess: z.enum(['stub', 'drizzle']).default('stub').optional(), 34 | dbImportPath: z.string().optional(), 35 | schemaImportPath: z.string().optional(), 36 | // zod/valibot/arktype generator specific options 37 | schemaSuffix: z.string().optional(), 38 | fileSuffix: z.string().optional(), 39 | // orpc validation sharing 40 | validation: z 41 | .object({ 42 | useShared: z.boolean().default(false).optional(), 43 | library: z.enum(['zod', 'valibot', 'arktype']).default('zod').optional(), 44 | importPath: z.string().optional(), 45 | schemaSuffix: z.string().optional(), 46 | }) 47 | .optional(), 48 | // template options 49 | templateOptions: z.record(z.string(), z.any()).optional(), 50 | }); 51 | 52 | export const AnalyzerSchema = z.object({ 53 | includeRelations: z.boolean().default(true), 54 | validateConstraints: z.boolean().default(true), 55 | includeHeuristicRelations: z.boolean().default(false), 56 | }); 57 | 58 | export const ConfigSchema = z.object({ 59 | schema: z.string(), 60 | outDir: z.string().default('src/api'), 61 | analyzer: AnalyzerSchema.default({ 62 | includeRelations: true, 63 | validateConstraints: true, 64 | includeHeuristicRelations: false, 65 | }), 66 | generators: z 67 | .array(GeneratorSchema) 68 | .min(1) 69 | .default([{ kind: 'orpc' } as any]), 70 | }); 71 | 72 | // ✨ Separate input vs output types 73 | export type DrzlConfigInput = z.input; 74 | export type DrzlConfig = z.output; 75 | 76 | export function defineConfig(cfg: T): T { 77 | return cfg; 78 | } 79 | 80 | export async function loadConfig(customPath?: string): Promise { 81 | const fsp = await import('node:fs/promises'); 82 | 83 | const candidates = customPath 84 | ? [customPath] 85 | : [ 86 | 'drzl.config.ts', 87 | 'drzl.config.mjs', 88 | 'drzl.config.js', 89 | 'drzl.config.cjs', 90 | 'drzl.config.json', 91 | ]; 92 | 93 | for (const c of candidates) { 94 | const p = path.resolve(process.cwd(), c); 95 | try { 96 | await fsp.access(p); 97 | } catch { 98 | continue; 99 | } 100 | 101 | const ext = path.extname(p).toLowerCase(); 102 | 103 | // JSON: read directly 104 | if (ext === '.json') { 105 | const raw = JSON.parse(await fsp.readFile(p, 'utf8')); 106 | return ConfigSchema.parse(raw); 107 | } 108 | 109 | // Everything else (TS/JS/MJS/CJS) -> Jiti with cache-busting 110 | const { createJiti } = await import('jiti'); 111 | const stat = await fsp.stat(p); 112 | 113 | // Passing __filename is safe in CJS; fallback to cwd if not defined. 114 | const base = 115 | typeof __filename !== 'undefined' ? __filename : path.join(process.cwd(), 'index.js'); 116 | 117 | const jiti = createJiti(base, { 118 | moduleCache: false, // re-evaluate each time 119 | fsCache: true, // keep transform cache 120 | cacheVersion: String(stat.mtimeMs), // bump on edit 121 | interopDefault: true, 122 | tryNative: false, // <— prevent native import of .ts 123 | // debug: true, 124 | }) as any; 125 | 126 | const mod = await jiti.import(p); 127 | const raw = mod?.default ?? mod; 128 | return ConfigSchema.parse(raw); 129 | } 130 | 131 | return null; 132 | } 133 | 134 | /** Absolute output dirs for all generators (to ignore in watcher). */ 135 | export function computeGeneratorOutputDirs(cfg: DrzlConfig, cwd = process.cwd()): string[] { 136 | const abs = (p: string) => path.resolve(cwd, p); 137 | const dirs = new Set(); 138 | dirs.add(abs(cfg.outDir)); // orpc 139 | for (const g of cfg.generators) { 140 | if (g.kind === 'service') dirs.add(abs(g.path ?? 'src/services')); 141 | if (g.kind === 'zod') dirs.add(abs(g.path ?? 'src/validators/zod')); 142 | if (g.kind === 'valibot') dirs.add(abs(g.path ?? 'src/validators/valibot')); 143 | if (g.kind === 'arktype') dirs.add(abs(g.path ?? 'src/validators/arktype')); 144 | } 145 | return [...dirs]; 146 | } 147 | 148 | /** Resolve custom template directories (local path or installed package). */ 149 | export function resolveTemplateDirsSync(cfg: DrzlConfig, cwd = process.cwd()): string[] { 150 | const results: string[] = []; 151 | const req = createRequire( 152 | typeof __filename !== 'undefined' ? __filename : path.join(process.cwd(), 'index.js') 153 | ); 154 | 155 | for (const g of cfg.generators) { 156 | const t = g.template; 157 | if (!t || t === 'standard' || t === 'minimal') continue; 158 | 159 | // Try package resolution relative to cwd 160 | let pkgDir: string | null = null; 161 | try { 162 | const pkg = req.resolve(`${t}/package.json`, { paths: [cwd] as any }); 163 | pkgDir = path.dirname(pkg); 164 | } catch {} 165 | 166 | if (pkgDir) { 167 | results.push(pkgDir); 168 | continue; 169 | } 170 | 171 | // Local path-like template 172 | if (/[./\\]/.test(t)) { 173 | const abs = path.resolve(cwd, t); 174 | if (fs.existsSync(abs)) results.push(abs); 175 | } 176 | } 177 | 178 | return Array.from(new Set(results)); 179 | } 180 | 181 | /** Build watch targets (exclude output dirs; watcher will ignore those). */ 182 | export function computeWatchTargets(cfg: DrzlConfig, cwd = process.cwd()): string[] { 183 | const abs = (p: string) => path.resolve(cwd, p); 184 | const schemaAbs = abs(cfg.schema); 185 | const targets = new Set([ 186 | path.join(path.dirname(schemaAbs), '**/*.{ts,tsx,js}'), 187 | abs('drzl.config.ts'), 188 | abs('drzl.config.js'), 189 | abs('drzl.config.mjs'), 190 | abs('drzl.config.cjs'), 191 | ]); 192 | for (const t of resolveTemplateDirsSync(cfg, cwd)) targets.add(t); 193 | return [...targets]; 194 | } 195 | -------------------------------------------------------------------------------- /scripts/brand-gen.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // Generate DRZL brand assets from a source image using sharp. 3 | // Usage: node scripts/brand-gen.mjs assets/brand/source.jpg 4 | 5 | import fs from 'node:fs/promises'; 6 | import path from 'node:path'; 7 | import sharp from 'sharp'; 8 | 9 | const args = process.argv.slice(2); 10 | const input = args[0] || 'assets/brand/source.jpg'; 11 | const getArg = (k, def) => { 12 | const m = args.find((a) => a.startsWith(`--${k}=`)); 13 | if (!m) return def; 14 | const v = m.split('=')[1]; 15 | const n = Number(v); 16 | return Number.isFinite(n) ? n : v; 17 | }; 18 | const mode = getArg('mode', 'icon'); // 'icon' or 'lockup' 19 | const topPct = getArg('top', 0.15); // 15% top offset default 20 | const heightPct = getArg('height', 0.7); // 70% default 21 | const bgArg = getArg('bg', null); // optional hex like #e8e6e7 22 | const bannerTitle = getArg('bannerTitle', 'DRZL'); 23 | const bannerSubtitle = getArg('bannerSubtitle', 'Zero‑friction codegen for Drizzle ORM'); 24 | const bannerOut = getArg('bannerOut', 'docs/public/banner.png'); 25 | const outBrand = 'docs/public/brand'; 26 | const outPublic = 'docs/public'; 27 | 28 | await fs.mkdir(outBrand, { recursive: true }); 29 | await fs.mkdir(outPublic, { recursive: true }); 30 | 31 | async function save(pipeline, file) { 32 | const out = path.join(outPublic, file); 33 | await pipeline.toFile(out); 34 | return out; 35 | } 36 | 37 | async function saveBrand(pipeline, file) { 38 | const out = path.join(outBrand, file); 39 | await pipeline.toFile(out); 40 | return out; 41 | } 42 | 43 | async function main() { 44 | try { 45 | const src = sharp(input); 46 | // Use source dims; optional trim caused issues on some libvips builds 47 | const meta = await src.metadata(); 48 | const w = meta.width ?? 1024; 49 | const h = meta.height ?? 1024; 50 | 51 | // Create a mark by cropping a centered band (keeps full symbol, avoids bottom wordmark) 52 | let region; 53 | if (mode === 'lockup') { 54 | region = { left: 0, top: 0, width: w, height: h }; 55 | } else { 56 | const cropH = Math.round(h * heightPct); // portion of height 57 | const top = Math.max(0, Math.round(h * topPct)); 58 | region = { left: 0, top, width: w, height: Math.min(cropH, h - top) }; 59 | } 60 | const mark = sharp(input).extract(region); 61 | 62 | // Fit the mark into a square canvas with transparent background, max dimension 1024 63 | const markSquare = mark 64 | .png() 65 | .resize({ 66 | width: 1024, 67 | height: 1024, 68 | fit: 'contain', 69 | background: { r: 0, g: 0, b: 0, alpha: 0 }, 70 | }); 71 | 72 | // Base logo (for light theme) 73 | const baseBuf = await markSquare.clone().png().toBuffer(); 74 | await sharp(baseBuf).toFile(path.join(outBrand, 'logo.png')); 75 | // Dark-theme logo: invert colors while keeping alpha 76 | await sharp(baseBuf).negate({ alpha: false }).toFile(path.join(outBrand, 'logo-dark.png')); 77 | 78 | // Favicons / app icons 79 | await save(markSquare.clone().resize(512, 512), 'icon-512.png'); 80 | await save(markSquare.clone().resize(192, 192), 'icon-192.png'); 81 | await save(markSquare.clone().resize(180, 180), 'apple-touch-icon.png'); 82 | await save(markSquare.clone().resize(48, 48), 'favicon-48.png'); 83 | await save(markSquare.clone().resize(32, 32), 'favicon-32.png'); 84 | await save(markSquare.clone().resize(16, 16), 'favicon-16.png'); 85 | try { 86 | await save(markSquare.clone().resize(32, 32).toFormat('ico'), 'favicon.ico'); 87 | } catch (_) { 88 | // If ICO unsupported, skip; PNG favicon links will cover most cases 89 | } 90 | 91 | // Social card 1280x640 (centered mark) 92 | // Background color for social card 93 | let bg = null; 94 | if (bgArg) { 95 | const normalized = String(bgArg).startsWith('#') ? String(bgArg) : `#${bgArg}`; 96 | bg = normalized; 97 | } else { 98 | // Sample from source image (fallback) 99 | const sample = { 100 | left: Math.round(w * 0.05), 101 | top: Math.round(h * 0.05), 102 | width: Math.max(4, Math.round(w * 0.2)), 103 | height: Math.max(4, Math.round(h * 0.2)), 104 | }; 105 | const stats = await sharp(input).extract(sample).stats(); 106 | const toHex = (n) => 107 | Math.max(0, Math.min(255, Math.round(n))) 108 | .toString(16) 109 | .padStart(2, '0'); 110 | bg = `#${toHex(stats.channels[0].mean)}${toHex(stats.channels[1].mean)}${toHex(stats.channels[2].mean)}`; 111 | } 112 | const social = sharp({ create: { width: 1800, height: 480, channels: 4, background: bg } }); 113 | const markForSocial = await markSquare.clone().resize(420, 420).toBuffer(); 114 | await social 115 | .composite([{ input: markForSocial, gravity: 'center' }]) 116 | .png() 117 | .toFile(path.join(outPublic, 'social-card.png')); 118 | 119 | // Hero banner with gradient sky + globe grid and headline 120 | const bannerW = 1800, 121 | bannerH = 480; 122 | const cx = bannerW / 2, 123 | cy = bannerH + 220; 124 | const rings = Array.from({ length: 8 }, (_, i) => { 125 | const rx = 600 + i * 160; 126 | const ry = 180 + i * 60; 127 | return ``; 128 | }).join('\n'); 129 | const meridians = [-30, -18, 0, 18, 30] 130 | .map((deg) => { 131 | return ``; 132 | }) 133 | .join('\n'); 134 | const svg = ` 135 | \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n ${rings}\n ${meridians}\n \n \n GET STARTED WITH DRZL\n \n \n Next‑gen codegen for Drizzle ORM\n \n \n Analyze schemas. Generate services, routers & validation.\n \n`; 136 | await sharp(Buffer.from(svg)).png().toFile(bannerOut); 137 | 138 | console.log('Brand assets generated in docs/public and docs/public/brand'); 139 | } catch (err) { 140 | console.error('Brand generation failed:', err?.message || err); 141 | process.exit(1); 142 | } 143 | } 144 | 145 | await main(); 146 | -------------------------------------------------------------------------------- /packages/template-orpc-service/src/index.ts: -------------------------------------------------------------------------------- 1 | // Minimal local Table shape to avoid cross-package DTS complexity 2 | interface Table { 3 | name: string; 4 | tsName: string; 5 | } 6 | 7 | const cap = (s: string) => s.charAt(0).toUpperCase() + s.slice(1); 8 | const singularize = (s: string) => 9 | s.endsWith('ies') ? s.slice(0, -3) + 'y' : s.endsWith('s') ? s.slice(0, -1) : s; 10 | 11 | export interface ProcedureSpec { 12 | name: string; 13 | varName: string; 14 | code: string; 15 | } 16 | export interface ORPCTemplateHooks { 17 | filePath( 18 | table: Table, 19 | ctx: { 20 | outDir: string; 21 | naming?: { routerSuffix?: string; procedureCase?: 'camel' | 'kebab' | 'snake' }; 22 | } 23 | ): string; 24 | routerName( 25 | table: Table, 26 | ctx: { naming?: { routerSuffix?: string; procedureCase?: 'camel' | 'kebab' | 'snake' } } 27 | ): string; 28 | procedures( 29 | table: Table, 30 | ctx?: { 31 | databaseInjection?: { 32 | enabled?: boolean; 33 | databaseType?: string; 34 | databaseTypeImport?: { name: string; from: string }; 35 | }; 36 | } 37 | ): ProcedureSpec[]; 38 | imports?( 39 | tables: Table[], 40 | ctx?: { 41 | outDir: string; 42 | servicesDir?: string; 43 | databaseInjection?: { 44 | enabled?: boolean; 45 | databaseType?: string; 46 | databaseTypeImport?: { name: string; from: string }; 47 | }; 48 | } 49 | ): string; 50 | header?(table: Table): string; 51 | } 52 | 53 | const servicesDirDefault = 'src/services'; 54 | 55 | import path from 'node:path'; 56 | 57 | const template: ORPCTemplateHooks = { 58 | filePath: (table, ctx) => { 59 | const suffix = ctx.naming?.routerSuffix ?? ''; 60 | const base = `${table.tsName}${suffix}`; 61 | const toCase = (s: string) => { 62 | const parts = s 63 | .replace(/([a-z0-9])([A-Z])/g, '$1 $2') 64 | .replace(/[_-]/g, ' ') 65 | .split(/\s+/); 66 | const c = ctx.naming?.procedureCase ?? 'camel'; 67 | if (c === 'kebab') return parts.map((p) => p.toLowerCase()).join('-'); 68 | if (c === 'snake') return parts.map((p) => p.toLowerCase()).join('_'); 69 | return parts 70 | .map((p, i) => (i === 0 ? p.toLowerCase() : p[0].toUpperCase() + p.slice(1).toLowerCase())) 71 | .join(''); 72 | }; 73 | return `${ctx.outDir}/${toCase(base)}.ts`; 74 | }, 75 | routerName: (table, ctx) => { 76 | const suffix = ctx.naming?.routerSuffix ?? ''; 77 | const base = `${table.tsName}${suffix}`; 78 | const toCase = (s: string) => { 79 | const parts = s 80 | .replace(/([a-z0-9])([A-Z])/g, '$1 $2') 81 | .replace(/[_-]/g, ' ') 82 | .split(/\s+/); 83 | const c = ctx.naming?.procedureCase ?? 'camel'; 84 | if (c === 'snake') return parts.map((p) => p.toLowerCase()).join('_'); 85 | // kebab invalid for identifiers -> camel 86 | return parts 87 | .map((p, i) => (i === 0 ? p.toLowerCase() : p[0].toUpperCase() + p.slice(1).toLowerCase())) 88 | .join(''); 89 | }; 90 | return toCase(base); 91 | }, 92 | imports: (tables, ctx) => { 93 | const t = tables[0]; 94 | const singular = singularize(t.tsName); 95 | const Service = `${cap(singular)}Service`; 96 | const outDir = ctx?.outDir ?? 'src/api'; 97 | const servicesDir = (ctx as any)?.servicesDir ?? servicesDirDefault; 98 | const rel = path.relative(outDir, servicesDir) || '.'; 99 | 100 | const isInjectionMode = ctx?.databaseInjection?.enabled === true; 101 | const dbType = ctx?.databaseInjection?.databaseType ?? 'any'; 102 | 103 | if (isInjectionMode) { 104 | const typeImport = ctx?.databaseInjection?.databaseTypeImport 105 | ? `\nimport type { ${ctx.databaseInjection.databaseTypeImport.name} } from '${ctx.databaseInjection.databaseTypeImport.from}';` 106 | : ''; 107 | return `import { os, ORPCError } from '@orpc/server' 108 | import { z } from 'zod' 109 | import { ${Service} } from '${rel}/${singular}Service' 110 | ${typeImport} 111 | 112 | export const dbMiddleware = os 113 | .$context<{ db?: ${dbType} }>() 114 | .middleware(async ({ context, next }) => { 115 | if (!context.db) { 116 | console.error('No database provided in context'); 117 | throw new ORPCError('INTERNAL_SERVER_ERROR'); 118 | } 119 | return next({ 120 | context: { 121 | db: context.db 122 | } 123 | }); 124 | });`; 125 | } else { 126 | return `import { os } from '@orpc/server'\nimport { z } from 'zod'\nimport { ${Service} } from '${rel}/${singular}Service'`; 127 | } 128 | }, 129 | header: (table) => `// Router for table: ${table.name}`, 130 | procedures: (table, ctx) => { 131 | const T = cap(table.tsName); 132 | const singular = singularize(table.tsName); 133 | const Service = `${cap(singular)}Service`; 134 | const isInjectionMode = ctx?.databaseInjection?.enabled === true; 135 | 136 | const make = (proc: string, varName: string, code: string): ProcedureSpec => ({ 137 | name: proc, 138 | varName, 139 | code, 140 | }); 141 | 142 | if (isInjectionMode) { 143 | // Database injection mode - use middleware and context 144 | return [ 145 | make( 146 | 'list', 147 | `list${T}`, 148 | `const list${T} = os\n .use(dbMiddleware)\n .handler(async ({ context }) => {\n return await ${Service}.getAll(context.db);\n });` 149 | ), 150 | make( 151 | 'get', 152 | `get${T}`, 153 | `const get${T} = os\n .use(dbMiddleware)\n .input(z.object({ id: z.number() }))\n .handler(async ({ context, input }) => {\n return await ${Service}.getById(context.db, input.id);\n });` 154 | ), 155 | make( 156 | 'create', 157 | `create${T}`, 158 | `const create${T} = os\n .use(dbMiddleware)\n .input(z.any())\n .handler(async ({ context, input }) => {\n return await ${Service}.create(context.db, input);\n });` 159 | ), 160 | make( 161 | 'update', 162 | `update${T}`, 163 | `const update${T} = os\n .use(dbMiddleware)\n .input(z.object({ id: z.number(), data: z.any() }))\n .handler(async ({ context, input }) => {\n return await ${Service}.update(context.db, input.id, input.data);\n });` 164 | ), 165 | make( 166 | 'delete', 167 | `delete${T}`, 168 | `const delete${T} = os\n .use(dbMiddleware)\n .input(z.object({ id: z.number() }))\n .handler(async ({ context, input }) => {\n return await ${Service}.delete(context.db, input.id);\n });` 169 | ), 170 | ]; 171 | } else { 172 | // Traditional mode - backward compatibility 173 | return [ 174 | make( 175 | 'list', 176 | `list${T}`, 177 | `const list${T} = os.handler(async () => {\n return await ${Service}.getAll();\n});` 178 | ), 179 | make( 180 | 'get', 181 | `get${T}`, 182 | `const get${T} = os\n .input(z.object({ id: z.number() }))\n .handler(async ({ input }) => {\n return await ${Service}.getById(input.id);\n });` 183 | ), 184 | make( 185 | 'create', 186 | `create${T}`, 187 | `const create${T} = os\n .input(z.any())\n .handler(async ({ input }) => {\n return await ${Service}.create(input);\n });` 188 | ), 189 | make( 190 | 'update', 191 | `update${T}`, 192 | `const update${T} = os\n .input(z.object({ id: z.number(), data: z.any() }))\n .handler(async ({ input }) => {\n return await ${Service}.update(input.id, input.data);\n });` 193 | ), 194 | make( 195 | 'delete', 196 | `delete${T}`, 197 | `const delete${T} = os\n .input(z.object({ id: z.number() }))\n .handler(async ({ input }) => {\n return await ${Service}.delete(input.id);\n });` 198 | ), 199 | ]; 200 | } 201 | }, 202 | }; 203 | 204 | export default template; 205 | --------------------------------------------------------------------------------