schematic-pg 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1019 -0
- package/dist/api/auth/errors.d.ts +6 -0
- package/dist/api/auth/errors.js +12 -0
- package/dist/api/auth/jwt-resolver.d.ts +7 -0
- package/dist/api/auth/jwt-resolver.js +59 -0
- package/dist/api/auth/middleware.d.ts +4 -0
- package/dist/api/auth/middleware.js +10 -0
- package/dist/api/auth/policy.d.ts +7 -0
- package/dist/api/auth/policy.js +95 -0
- package/dist/api/auth/template.d.ts +2 -0
- package/dist/api/auth/template.js +24 -0
- package/dist/api/auth/types.d.ts +12 -0
- package/dist/api/auth/types.js +1 -0
- package/dist/api/middleware/db.d.ts +8 -0
- package/dist/api/middleware/db.js +12 -0
- package/dist/api/middleware/errors.d.ts +5 -0
- package/dist/api/middleware/errors.js +27 -0
- package/dist/api/middleware/validate.d.ts +23 -0
- package/dist/api/middleware/validate.js +13 -0
- package/dist/api/types.d.ts +8 -0
- package/dist/api/types.js +1 -0
- package/dist/api/utils/route-naming.d.ts +3 -0
- package/dist/api/utils/route-naming.js +25 -0
- package/dist/api-generator/app-generator.d.ts +11 -0
- package/dist/api-generator/app-generator.js +79 -0
- package/dist/api-generator/custom-route-scanner.d.ts +6 -0
- package/dist/api-generator/custom-route-scanner.js +42 -0
- package/dist/api-generator/generate-api-cli.d.ts +1 -0
- package/dist/api-generator/generate-api-cli.js +28 -0
- package/dist/api-generator/index.d.ts +11 -0
- package/dist/api-generator/index.js +15 -0
- package/dist/api-generator/policy-generator.d.ts +9 -0
- package/dist/api-generator/policy-generator.js +33 -0
- package/dist/api-generator/route-generator.d.ts +20 -0
- package/dist/api-generator/route-generator.js +198 -0
- package/dist/api-generator/utils/policy.d.ts +18 -0
- package/dist/api-generator/utils/policy.js +72 -0
- package/dist/api-generator/zod-schema-generator.d.ts +16 -0
- package/dist/api-generator/zod-schema-generator.js +145 -0
- package/dist/cli/db.d.ts +4 -0
- package/dist/cli/db.js +144 -0
- package/dist/cli/dev.d.ts +1 -0
- package/dist/cli/dev.js +10 -0
- package/dist/cli/generate.d.ts +4 -0
- package/dist/cli/generate.js +50 -0
- package/dist/cli/init.d.ts +1 -0
- package/dist/cli/init.js +57 -0
- package/dist/cli/paths.d.ts +5 -0
- package/dist/cli/paths.js +10 -0
- package/dist/cli/templates.d.ts +6 -0
- package/dist/cli/templates.js +85 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +81 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/db/bootstrap-cli.d.ts +1 -0
- package/dist/db/bootstrap-cli.js +17 -0
- package/dist/db/bootstrap.d.ts +3 -0
- package/dist/db/bootstrap.js +16 -0
- package/dist/db/cli.d.ts +1 -0
- package/dist/db/cli.js +20 -0
- package/dist/db/client.d.ts +8 -0
- package/dist/db/client.js +23 -0
- package/dist/db/config.d.ts +1 -0
- package/dist/db/config.js +10 -0
- package/dist/db/db-client-generator.d.ts +13 -0
- package/dist/db/db-client-generator.js +70 -0
- package/dist/db/diff-cli.d.ts +1 -0
- package/dist/db/diff-cli.js +46 -0
- package/dist/db/diff.d.ts +9 -0
- package/dist/db/diff.js +30 -0
- package/dist/db/errors.d.ts +34 -0
- package/dist/db/errors.js +88 -0
- package/dist/db/generate-client-cli.d.ts +1 -0
- package/dist/db/generate-client-cli.js +21 -0
- package/dist/db/index.d.ts +19 -0
- package/dist/db/index.js +17 -0
- package/dist/db/load-env.d.ts +3 -0
- package/dist/db/load-env.js +19 -0
- package/dist/db/migrate-cli.d.ts +1 -0
- package/dist/db/migrate-cli.js +88 -0
- package/dist/db/migrate.d.ts +3 -0
- package/dist/db/migrate.js +32 -0
- package/dist/db/migrations.d.ts +17 -0
- package/dist/db/migrations.js +81 -0
- package/dist/db/model-client.d.ts +36 -0
- package/dist/db/model-client.js +83 -0
- package/dist/db/model-meta.d.ts +36 -0
- package/dist/db/model-meta.js +57 -0
- package/dist/db/query-builder.d.ts +33 -0
- package/dist/db/query-builder.js +97 -0
- package/dist/db/reset-database.d.ts +4 -0
- package/dist/db/reset-database.js +9 -0
- package/dist/db/row-mapper.d.ts +3 -0
- package/dist/db/row-mapper.js +41 -0
- package/dist/db/schema-state.d.ts +7 -0
- package/dist/db/schema-state.js +32 -0
- package/dist/db/type-generator.d.ts +19 -0
- package/dist/db/type-generator.js +136 -0
- package/dist/db/utils/naming.d.ts +6 -0
- package/dist/db/utils/naming.js +23 -0
- package/dist/db/where-translator.d.ts +20 -0
- package/dist/db/where-translator.js +141 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/routes/health.d.ts +4 -0
- package/dist/routes/health.js +4 -0
- package/dist/schema-dsl/ast.d.ts +108 -0
- package/dist/schema-dsl/ast.js +1 -0
- package/dist/schema-dsl/cli.d.ts +1 -0
- package/dist/schema-dsl/cli.js +25 -0
- package/dist/schema-dsl/index.d.ts +8 -0
- package/dist/schema-dsl/index.js +16 -0
- package/dist/schema-dsl/inspect.d.ts +1 -0
- package/dist/schema-dsl/inspect.js +9 -0
- package/dist/schema-dsl/lexer.d.ts +31 -0
- package/dist/schema-dsl/lexer.js +216 -0
- package/dist/schema-dsl/parser.d.ts +49 -0
- package/dist/schema-dsl/parser.js +372 -0
- package/dist/schema-dsl/tokens.d.ts +30 -0
- package/dist/schema-dsl/tokens.js +35 -0
- package/dist/sql-generator/cli.d.ts +1 -0
- package/dist/sql-generator/cli.js +7 -0
- package/dist/sql-generator/generators/drop-tables.d.ts +2 -0
- package/dist/sql-generator/generators/drop-tables.js +8 -0
- package/dist/sql-generator/generators/enums.d.ts +4 -0
- package/dist/sql-generator/generators/enums.js +16 -0
- package/dist/sql-generator/generators/extensions.d.ts +4 -0
- package/dist/sql-generator/generators/extensions.js +11 -0
- package/dist/sql-generator/generators/foreign-keys.d.ts +4 -0
- package/dist/sql-generator/generators/foreign-keys.js +23 -0
- package/dist/sql-generator/generators/indexes.d.ts +13 -0
- package/dist/sql-generator/generators/indexes.js +39 -0
- package/dist/sql-generator/generators/tables.d.ts +4 -0
- package/dist/sql-generator/generators/tables.js +65 -0
- package/dist/sql-generator/generators/triggers.d.ts +6 -0
- package/dist/sql-generator/generators/triggers.js +47 -0
- package/dist/sql-generator/index.d.ts +5 -0
- package/dist/sql-generator/index.js +3 -0
- package/dist/sql-generator/migration-planner.d.ts +15 -0
- package/dist/sql-generator/migration-planner.js +207 -0
- package/dist/sql-generator/migration-sql-generator.d.ts +9 -0
- package/dist/sql-generator/migration-sql-generator.js +181 -0
- package/dist/sql-generator/migration-types.d.ts +86 -0
- package/dist/sql-generator/migration-types.js +1 -0
- package/dist/sql-generator/sql-generator.d.ts +6 -0
- package/dist/sql-generator/sql-generator.js +26 -0
- package/dist/sql-generator/utils/ast-helpers.d.ts +58 -0
- package/dist/sql-generator/utils/ast-helpers.js +252 -0
- package/dist/sql-generator/utils/format.d.ts +2 -0
- package/dist/sql-generator/utils/format.js +21 -0
- package/dist/sql-generator/utils/snake-case.d.ts +3 -0
- package/dist/sql-generator/utils/snake-case.js +96 -0
- package/dist/sql-generator/utils/type-mapper.d.ts +2 -0
- package/dist/sql-generator/utils/type-mapper.js +39 -0
- package/dist/sql-generator/utils/value-formatter.d.ts +4 -0
- package/dist/sql-generator/utils/value-formatter.js +41 -0
- package/dist/types/generated-db.stub.d.ts +2 -0
- package/dist/types/generated-db.stub.js +3 -0
- package/dist/types/generated-policies.stub.d.ts +7 -0
- package/dist/types/generated-policies.stub.js +1 -0
- package/package.json +86 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { generateAppFile } from './app-generator.js';
|
|
2
|
+
import { generatePoliciesFile } from './policy-generator.js';
|
|
3
|
+
import { generateRouteFiles } from './route-generator.js';
|
|
4
|
+
import { generateValidationSchemas } from './zod-schema-generator.js';
|
|
5
|
+
export function generateApiFiles(schema, options) {
|
|
6
|
+
const appOptions = options?.customRoutesDir
|
|
7
|
+
? { customRoutesDir: options.customRoutesDir }
|
|
8
|
+
: undefined;
|
|
9
|
+
return {
|
|
10
|
+
app: generateAppFile(schema, appOptions),
|
|
11
|
+
policies: generatePoliciesFile(schema),
|
|
12
|
+
validation: generateValidationSchemas(schema),
|
|
13
|
+
routes: generateRouteFiles(schema),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Schema } from '../schema-dsl/ast.js';
|
|
2
|
+
import { type NormalizedPolicy, type PolicyOperation } from './utils/policy.js';
|
|
3
|
+
export declare class PolicyGenerator {
|
|
4
|
+
private readonly schema;
|
|
5
|
+
constructor(schema: Schema);
|
|
6
|
+
generate(): string;
|
|
7
|
+
}
|
|
8
|
+
export declare function generatePoliciesFile(schema: Schema): string;
|
|
9
|
+
export type { NormalizedPolicy, PolicyOperation };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { hasPolicies, normalizePolicies, serializePolicy, } from './utils/policy.js';
|
|
2
|
+
export class PolicyGenerator {
|
|
3
|
+
schema;
|
|
4
|
+
constructor(schema) {
|
|
5
|
+
this.schema = schema;
|
|
6
|
+
}
|
|
7
|
+
generate() {
|
|
8
|
+
const modelsWithPolicies = this.schema.models.filter(hasPolicies);
|
|
9
|
+
const policyEntries = modelsWithPolicies.map((model) => {
|
|
10
|
+
const policies = normalizePolicies(model);
|
|
11
|
+
const serializedPolicies = policies.map((policy) => serializePolicy(policy)).join(',\n ');
|
|
12
|
+
return ` ${model.name}: [\n ${serializedPolicies},\n ]`;
|
|
13
|
+
});
|
|
14
|
+
return [
|
|
15
|
+
'// Auto-generated by PolicyGenerator. Do not edit manually.',
|
|
16
|
+
"export type PolicyOperation = 'select' | 'insert' | 'update' | 'delete';",
|
|
17
|
+
'',
|
|
18
|
+
'export interface NormalizedPolicy {',
|
|
19
|
+
' role: string;',
|
|
20
|
+
" operations: PolicyOperation[] | 'all';",
|
|
21
|
+
' where?: string;',
|
|
22
|
+
'}',
|
|
23
|
+
'',
|
|
24
|
+
'export const POLICIES: Record<string, NormalizedPolicy[]> = {',
|
|
25
|
+
policyEntries.join(',\n'),
|
|
26
|
+
'};',
|
|
27
|
+
'',
|
|
28
|
+
].join('\n');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function generatePoliciesFile(schema) {
|
|
32
|
+
return new PolicyGenerator(schema).generate();
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Model, Schema } from '../schema-dsl/ast.js';
|
|
2
|
+
export declare class RouteGenerator {
|
|
3
|
+
private readonly model;
|
|
4
|
+
private readonly schema;
|
|
5
|
+
constructor(model: Model, schema: Schema);
|
|
6
|
+
generate(): string;
|
|
7
|
+
private generateListRoute;
|
|
8
|
+
private generateGetRoute;
|
|
9
|
+
private generateCreateRoute;
|
|
10
|
+
private generateUpdateRoute;
|
|
11
|
+
private generateDeleteRoute;
|
|
12
|
+
getRouteFileName(): string;
|
|
13
|
+
getRouteBasePath(): string;
|
|
14
|
+
}
|
|
15
|
+
export declare function generateRouteFiles(schema: Schema): Map<string, string>;
|
|
16
|
+
export declare function getRouteMountEntries(schema: Schema): {
|
|
17
|
+
basePath: string;
|
|
18
|
+
fileName: string;
|
|
19
|
+
importName: string;
|
|
20
|
+
}[];
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { PACKAGE_NAME } from '../constants.js';
|
|
2
|
+
import { getPrimaryKey } from '../sql-generator/utils/ast-helpers.js';
|
|
3
|
+
import { getClientExportName } from '../db/type-generator.js';
|
|
4
|
+
import { toRouteBasePath, toRouteFileName, toRouteImportName } from '../api/utils/route-naming.js';
|
|
5
|
+
import { hasPolicies } from './utils/policy.js';
|
|
6
|
+
export class RouteGenerator {
|
|
7
|
+
model;
|
|
8
|
+
schema;
|
|
9
|
+
constructor(model, schema) {
|
|
10
|
+
this.model = model;
|
|
11
|
+
this.schema = schema;
|
|
12
|
+
}
|
|
13
|
+
generate() {
|
|
14
|
+
const clientKey = getClientExportName(this.model.name);
|
|
15
|
+
const primaryKey = getPrimaryKey(this.model);
|
|
16
|
+
if (!primaryKey) {
|
|
17
|
+
throw new Error(`Model ${this.model.name} has no primary key`);
|
|
18
|
+
}
|
|
19
|
+
const pathParams = primaryKey.fields.map((field) => `:${field}`).join('/');
|
|
20
|
+
const whereFromParams = primaryKey.fields.map((field) => `${field}: params.${field}`).join(', ');
|
|
21
|
+
const paramSchemaName = `${this.model.name}ParamSchema`;
|
|
22
|
+
const modelHasPolicies = hasPolicies(this.model);
|
|
23
|
+
return [
|
|
24
|
+
'// Auto-generated by RouteGenerator. Do not edit manually.',
|
|
25
|
+
"import { Hono } from 'hono';",
|
|
26
|
+
`import type { AppEnv } from '${PACKAGE_NAME}/api/types';`,
|
|
27
|
+
`import { validateJson, validateParam } from '${PACKAGE_NAME}/api/middleware/validate';`,
|
|
28
|
+
`import { notFoundResponse } from '${PACKAGE_NAME}/api/middleware/errors';`,
|
|
29
|
+
...(modelHasPolicies
|
|
30
|
+
? [
|
|
31
|
+
`import { assertPolicy, mergeWhere, resolvePolicyWhere } from '${PACKAGE_NAME}/api/auth/policy';`,
|
|
32
|
+
]
|
|
33
|
+
: []),
|
|
34
|
+
`import {`,
|
|
35
|
+
` ${this.model.name}CreateSchema,`,
|
|
36
|
+
` ${this.model.name}UpdateSchema,`,
|
|
37
|
+
` ${paramSchemaName},`,
|
|
38
|
+
`} from '../schemas/validation.js';`,
|
|
39
|
+
'',
|
|
40
|
+
'const router = new Hono<AppEnv>();',
|
|
41
|
+
'',
|
|
42
|
+
...this.generateListRoute(clientKey, modelHasPolicies),
|
|
43
|
+
'',
|
|
44
|
+
...this.generateGetRoute(clientKey, pathParams, paramSchemaName, whereFromParams, modelHasPolicies),
|
|
45
|
+
'',
|
|
46
|
+
...this.generateCreateRoute(clientKey, modelHasPolicies),
|
|
47
|
+
'',
|
|
48
|
+
...this.generateUpdateRoute(clientKey, pathParams, paramSchemaName, whereFromParams, modelHasPolicies),
|
|
49
|
+
'',
|
|
50
|
+
...this.generateDeleteRoute(clientKey, pathParams, paramSchemaName, whereFromParams, modelHasPolicies),
|
|
51
|
+
'',
|
|
52
|
+
'export default router;',
|
|
53
|
+
'',
|
|
54
|
+
].join('\n');
|
|
55
|
+
}
|
|
56
|
+
generateListRoute(clientKey, modelHasPolicies) {
|
|
57
|
+
if (!modelHasPolicies) {
|
|
58
|
+
return [
|
|
59
|
+
"router.get('/', async (c) => {",
|
|
60
|
+
' const db = c.get(\'db\');',
|
|
61
|
+
` const rows = await db.${clientKey}.findMany();`,
|
|
62
|
+
' return c.json(rows);',
|
|
63
|
+
'});',
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
return [
|
|
67
|
+
"router.get('/', async (c) => {",
|
|
68
|
+
' const db = c.get(\'db\');',
|
|
69
|
+
' const auth = c.get(\'auth\');',
|
|
70
|
+
` const policy = assertPolicy('${this.model.name}', auth.role, 'select');`,
|
|
71
|
+
' const policyWhere = resolvePolicyWhere(policy, auth);',
|
|
72
|
+
` const rows = await db.${clientKey}.findMany({ where: policyWhere });`,
|
|
73
|
+
' return c.json(rows);',
|
|
74
|
+
'});',
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
generateGetRoute(clientKey, pathParams, paramSchemaName, whereFromParams, modelHasPolicies) {
|
|
78
|
+
if (!modelHasPolicies) {
|
|
79
|
+
return [
|
|
80
|
+
`router.get('/${pathParams}', validateParam(${paramSchemaName}), async (c) => {`,
|
|
81
|
+
' const db = c.get(\'db\');',
|
|
82
|
+
' const params = c.req.valid(\'param\');',
|
|
83
|
+
` const row = await db.${clientKey}.findUnique({ ${whereFromParams} });`,
|
|
84
|
+
' if (!row) {',
|
|
85
|
+
' return notFoundResponse(c);',
|
|
86
|
+
' }',
|
|
87
|
+
' return c.json(row);',
|
|
88
|
+
'});',
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
return [
|
|
92
|
+
`router.get('/${pathParams}', validateParam(${paramSchemaName}), async (c) => {`,
|
|
93
|
+
' const db = c.get(\'db\');',
|
|
94
|
+
' const auth = c.get(\'auth\');',
|
|
95
|
+
` const policy = assertPolicy('${this.model.name}', auth.role, 'select');`,
|
|
96
|
+
' const policyWhere = resolvePolicyWhere(policy, auth);',
|
|
97
|
+
' const params = c.req.valid(\'param\');',
|
|
98
|
+
` const row = await db.${clientKey}.findUnique(mergeWhere({ ${whereFromParams} }, policyWhere));`,
|
|
99
|
+
' if (!row) {',
|
|
100
|
+
' return notFoundResponse(c);',
|
|
101
|
+
' }',
|
|
102
|
+
' return c.json(row);',
|
|
103
|
+
'});',
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
generateCreateRoute(clientKey, modelHasPolicies) {
|
|
107
|
+
if (!modelHasPolicies) {
|
|
108
|
+
return [
|
|
109
|
+
`router.post('/', validateJson(${this.model.name}CreateSchema), async (c) => {`,
|
|
110
|
+
' const db = c.get(\'db\');',
|
|
111
|
+
' const body = c.req.valid(\'json\');',
|
|
112
|
+
` const row = await db.${clientKey}.create(body);`,
|
|
113
|
+
' return c.json(row, 201);',
|
|
114
|
+
'});',
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
return [
|
|
118
|
+
`router.post('/', validateJson(${this.model.name}CreateSchema), async (c) => {`,
|
|
119
|
+
' const db = c.get(\'db\');',
|
|
120
|
+
' const auth = c.get(\'auth\');',
|
|
121
|
+
` assertPolicy('${this.model.name}', auth.role, 'insert');`,
|
|
122
|
+
' const body = c.req.valid(\'json\');',
|
|
123
|
+
` const row = await db.${clientKey}.create(body);`,
|
|
124
|
+
' return c.json(row, 201);',
|
|
125
|
+
'});',
|
|
126
|
+
];
|
|
127
|
+
}
|
|
128
|
+
generateUpdateRoute(clientKey, pathParams, paramSchemaName, whereFromParams, modelHasPolicies) {
|
|
129
|
+
if (!modelHasPolicies) {
|
|
130
|
+
return [
|
|
131
|
+
`router.put('/${pathParams}', validateParam(${paramSchemaName}), validateJson(${this.model.name}UpdateSchema), async (c) => {`,
|
|
132
|
+
' const db = c.get(\'db\');',
|
|
133
|
+
' const params = c.req.valid(\'param\');',
|
|
134
|
+
' const body = c.req.valid(\'json\');',
|
|
135
|
+
` const row = await db.${clientKey}.update({ where: { ${whereFromParams} }, data: body });`,
|
|
136
|
+
' return c.json(row);',
|
|
137
|
+
'});',
|
|
138
|
+
];
|
|
139
|
+
}
|
|
140
|
+
return [
|
|
141
|
+
`router.put('/${pathParams}', validateParam(${paramSchemaName}), validateJson(${this.model.name}UpdateSchema), async (c) => {`,
|
|
142
|
+
' const db = c.get(\'db\');',
|
|
143
|
+
' const auth = c.get(\'auth\');',
|
|
144
|
+
` const policy = assertPolicy('${this.model.name}', auth.role, 'update');`,
|
|
145
|
+
' const policyWhere = resolvePolicyWhere(policy, auth);',
|
|
146
|
+
' const params = c.req.valid(\'param\');',
|
|
147
|
+
' const body = c.req.valid(\'json\');',
|
|
148
|
+
` const row = await db.${clientKey}.update({ where: mergeWhere({ ${whereFromParams} }, policyWhere), data: body });`,
|
|
149
|
+
' return c.json(row);',
|
|
150
|
+
'});',
|
|
151
|
+
];
|
|
152
|
+
}
|
|
153
|
+
generateDeleteRoute(clientKey, pathParams, paramSchemaName, whereFromParams, modelHasPolicies) {
|
|
154
|
+
if (!modelHasPolicies) {
|
|
155
|
+
return [
|
|
156
|
+
`router.delete('/${pathParams}', validateParam(${paramSchemaName}), async (c) => {`,
|
|
157
|
+
' const db = c.get(\'db\');',
|
|
158
|
+
' const params = c.req.valid(\'param\');',
|
|
159
|
+
` const row = await db.${clientKey}.delete({ ${whereFromParams} });`,
|
|
160
|
+
' return c.json(row);',
|
|
161
|
+
'});',
|
|
162
|
+
];
|
|
163
|
+
}
|
|
164
|
+
return [
|
|
165
|
+
`router.delete('/${pathParams}', validateParam(${paramSchemaName}), async (c) => {`,
|
|
166
|
+
' const db = c.get(\'db\');',
|
|
167
|
+
' const auth = c.get(\'auth\');',
|
|
168
|
+
` const policy = assertPolicy('${this.model.name}', auth.role, 'delete');`,
|
|
169
|
+
' const policyWhere = resolvePolicyWhere(policy, auth);',
|
|
170
|
+
' const params = c.req.valid(\'param\');',
|
|
171
|
+
` const row = await db.${clientKey}.delete(mergeWhere({ ${whereFromParams} }, policyWhere));`,
|
|
172
|
+
' return c.json(row);',
|
|
173
|
+
'});',
|
|
174
|
+
];
|
|
175
|
+
}
|
|
176
|
+
getRouteFileName() {
|
|
177
|
+
return toRouteFileName(this.model.name);
|
|
178
|
+
}
|
|
179
|
+
getRouteBasePath() {
|
|
180
|
+
return toRouteBasePath(this.model.name);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
export function generateRouteFiles(schema) {
|
|
184
|
+
const files = new Map();
|
|
185
|
+
for (const model of schema.models) {
|
|
186
|
+
const generator = new RouteGenerator(model, schema);
|
|
187
|
+
files.set(generator.getRouteFileName(), generator.generate());
|
|
188
|
+
}
|
|
189
|
+
return files;
|
|
190
|
+
}
|
|
191
|
+
export function getRouteMountEntries(schema) {
|
|
192
|
+
return schema.models.map((model) => {
|
|
193
|
+
const basePath = toRouteBasePath(model.name);
|
|
194
|
+
const fileName = toRouteFileName(model.name);
|
|
195
|
+
const importName = toRouteImportName(basePath);
|
|
196
|
+
return { basePath, fileName, importName };
|
|
197
|
+
});
|
|
198
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Model } from '../../schema-dsl/ast.js';
|
|
2
|
+
export declare const PUBLIC_ROLE = "PUBLIC";
|
|
3
|
+
export type PolicyOperation = 'select' | 'insert' | 'update' | 'delete';
|
|
4
|
+
export declare const POLICY_OPERATIONS: PolicyOperation[];
|
|
5
|
+
export declare const OP_BY_METHOD: {
|
|
6
|
+
readonly GET: "select";
|
|
7
|
+
readonly POST: "insert";
|
|
8
|
+
readonly PUT: "update";
|
|
9
|
+
readonly DELETE: "delete";
|
|
10
|
+
};
|
|
11
|
+
export interface NormalizedPolicy {
|
|
12
|
+
role: string;
|
|
13
|
+
operations: PolicyOperation[] | 'all';
|
|
14
|
+
where?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function normalizePolicies(model: Model): NormalizedPolicy[];
|
|
17
|
+
export declare function hasPolicies(model: Model): boolean;
|
|
18
|
+
export declare function serializePolicy(policy: NormalizedPolicy): string;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { assertKeyValueArgs, getKvPair, getOptionalKvPair } from '../../sql-generator/utils/ast-helpers.js';
|
|
2
|
+
export const PUBLIC_ROLE = 'PUBLIC';
|
|
3
|
+
export const POLICY_OPERATIONS = ['select', 'insert', 'update', 'delete'];
|
|
4
|
+
export const OP_BY_METHOD = {
|
|
5
|
+
GET: 'select',
|
|
6
|
+
POST: 'insert',
|
|
7
|
+
PUT: 'update',
|
|
8
|
+
DELETE: 'delete',
|
|
9
|
+
};
|
|
10
|
+
export function normalizePolicies(model) {
|
|
11
|
+
return model.attributes
|
|
12
|
+
.filter((attribute) => attribute.name === 'policy')
|
|
13
|
+
.map((attribute) => normalizePolicyAttribute(attribute));
|
|
14
|
+
}
|
|
15
|
+
export function hasPolicies(model) {
|
|
16
|
+
return model.attributes.some((attribute) => attribute.name === 'policy');
|
|
17
|
+
}
|
|
18
|
+
function normalizePolicyAttribute(attribute) {
|
|
19
|
+
const args = assertKeyValueArgs(attribute.args);
|
|
20
|
+
const role = parseIdentifierValue(getKvPair(args, 'role').value, 'role');
|
|
21
|
+
const allow = parseAllowOperations(getKvPair(args, 'allow').value);
|
|
22
|
+
const wherePair = getOptionalKvPair(args, 'where');
|
|
23
|
+
const policy = { role, operations: allow };
|
|
24
|
+
if (wherePair) {
|
|
25
|
+
policy.where = parseStringValue(wherePair.value, 'where');
|
|
26
|
+
}
|
|
27
|
+
return policy;
|
|
28
|
+
}
|
|
29
|
+
function parseAllowOperations(value) {
|
|
30
|
+
if (value.kind === 'Identifier') {
|
|
31
|
+
if (value.name === 'all') {
|
|
32
|
+
return 'all';
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`Unknown policy allow value "${value.name}"`);
|
|
35
|
+
}
|
|
36
|
+
if (value.kind === 'ArrayLiteral') {
|
|
37
|
+
return value.elements.map((element) => parsePolicyOperation(element));
|
|
38
|
+
}
|
|
39
|
+
throw new Error('Policy allow must be "all" or an array of operations');
|
|
40
|
+
}
|
|
41
|
+
function parsePolicyOperation(value) {
|
|
42
|
+
if (value.kind !== 'Identifier') {
|
|
43
|
+
throw new Error('Policy operation must be an identifier');
|
|
44
|
+
}
|
|
45
|
+
const operation = value.name.toLowerCase();
|
|
46
|
+
if (!isPolicyOperation(operation)) {
|
|
47
|
+
throw new Error(`Unknown policy operation "${value.name}"`);
|
|
48
|
+
}
|
|
49
|
+
return operation;
|
|
50
|
+
}
|
|
51
|
+
function parseIdentifierValue(value, fieldName) {
|
|
52
|
+
if (value.kind !== 'Identifier') {
|
|
53
|
+
throw new Error(`Policy ${fieldName} must be an identifier`);
|
|
54
|
+
}
|
|
55
|
+
return value.name;
|
|
56
|
+
}
|
|
57
|
+
function parseStringValue(value, fieldName) {
|
|
58
|
+
if (value.kind !== 'StringLiteral') {
|
|
59
|
+
throw new Error(`Policy ${fieldName} must be a string`);
|
|
60
|
+
}
|
|
61
|
+
return value.value;
|
|
62
|
+
}
|
|
63
|
+
function isPolicyOperation(value) {
|
|
64
|
+
return POLICY_OPERATIONS.includes(value);
|
|
65
|
+
}
|
|
66
|
+
export function serializePolicy(policy) {
|
|
67
|
+
const operations = policy.operations === 'all'
|
|
68
|
+
? "'all'"
|
|
69
|
+
: `[${policy.operations.map((operation) => `'${operation}'`).join(', ')}]`;
|
|
70
|
+
const where = policy.where ? `, where: ${JSON.stringify(policy.where)}` : '';
|
|
71
|
+
return `{ role: '${policy.role}', operations: ${operations}${where} }`;
|
|
72
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Schema } from '../schema-dsl/ast.js';
|
|
2
|
+
export declare class ZodSchemaGenerator {
|
|
3
|
+
private readonly schema;
|
|
4
|
+
constructor(schema: Schema);
|
|
5
|
+
generate(): string;
|
|
6
|
+
private generateModelSchemas;
|
|
7
|
+
private generateObjectField;
|
|
8
|
+
private generateParamField;
|
|
9
|
+
private toParamZodType;
|
|
10
|
+
private toZodType;
|
|
11
|
+
private mapBaseZodType;
|
|
12
|
+
private applyValidationRules;
|
|
13
|
+
private escapeRegexLiteral;
|
|
14
|
+
private escapeStringLiteral;
|
|
15
|
+
}
|
|
16
|
+
export declare function generateValidationSchemas(schema: Schema): string;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { fieldHasAttribute, getFieldAttribute, getModelNames, getOptionalKvPair, getPrimaryKey, getStoredFields, } from '../sql-generator/utils/ast-helpers.js';
|
|
2
|
+
export class ZodSchemaGenerator {
|
|
3
|
+
schema;
|
|
4
|
+
constructor(schema) {
|
|
5
|
+
this.schema = schema;
|
|
6
|
+
}
|
|
7
|
+
generate() {
|
|
8
|
+
const modelBlocks = this.schema.models.flatMap((model) => this.generateModelSchemas(model));
|
|
9
|
+
return [
|
|
10
|
+
'// Auto-generated by ZodSchemaGenerator. Do not edit manually.',
|
|
11
|
+
"import { z } from 'zod';",
|
|
12
|
+
'',
|
|
13
|
+
...modelBlocks,
|
|
14
|
+
].join('\n');
|
|
15
|
+
}
|
|
16
|
+
generateModelSchemas(model) {
|
|
17
|
+
const modelNames = getModelNames(this.schema);
|
|
18
|
+
const storedFields = getStoredFields(model, modelNames);
|
|
19
|
+
const primaryKey = getPrimaryKey(model);
|
|
20
|
+
const pkFields = new Set(primaryKey?.fields ?? []);
|
|
21
|
+
const createFields = storedFields.filter((field) => !pkFields.has(field.name));
|
|
22
|
+
const updateFields = storedFields.filter((field) => !pkFields.has(field.name));
|
|
23
|
+
const pkFieldDefs = storedFields.filter((field) => pkFields.has(field.name));
|
|
24
|
+
const createLines = createFields.map((field) => this.generateObjectField(field, 'create'));
|
|
25
|
+
const updateLines = updateFields.map((field) => this.generateObjectField(field, 'update'));
|
|
26
|
+
const paramLines = pkFieldDefs.map((field) => this.generateParamField(field));
|
|
27
|
+
const blocks = [
|
|
28
|
+
`export const ${model.name}CreateSchema = z.object({`,
|
|
29
|
+
...createLines.map((line) => ` ${line},`),
|
|
30
|
+
`});\n`,
|
|
31
|
+
`export const ${model.name}UpdateSchema = z.object({`,
|
|
32
|
+
...updateLines.map((line) => ` ${line},`),
|
|
33
|
+
`});\n`,
|
|
34
|
+
];
|
|
35
|
+
if (paramLines.length > 0) {
|
|
36
|
+
blocks.push(`export const ${model.name}ParamSchema = z.object({`, ...paramLines.map((line) => ` ${line},`), `});\n`);
|
|
37
|
+
}
|
|
38
|
+
return blocks;
|
|
39
|
+
}
|
|
40
|
+
generateObjectField(field, mode) {
|
|
41
|
+
const zodType = this.toZodType(field, mode);
|
|
42
|
+
const fieldName = `${field.name}: ${zodType}`;
|
|
43
|
+
if (mode === 'update') {
|
|
44
|
+
return fieldName;
|
|
45
|
+
}
|
|
46
|
+
const isOptional = field.type.optional || fieldHasAttribute(field, 'default');
|
|
47
|
+
if (isOptional) {
|
|
48
|
+
return `${field.name}: ${zodType}.optional()`;
|
|
49
|
+
}
|
|
50
|
+
return fieldName;
|
|
51
|
+
}
|
|
52
|
+
generateParamField(field) {
|
|
53
|
+
const zodType = this.toParamZodType(field);
|
|
54
|
+
return `${field.name}: ${zodType}`;
|
|
55
|
+
}
|
|
56
|
+
toParamZodType(field) {
|
|
57
|
+
switch (field.type.name) {
|
|
58
|
+
case 'UUID':
|
|
59
|
+
return 'z.string().uuid()';
|
|
60
|
+
case 'INTEGER':
|
|
61
|
+
case 'SERIAL':
|
|
62
|
+
case 'SMALLINT':
|
|
63
|
+
return 'z.coerce.number().int()';
|
|
64
|
+
default:
|
|
65
|
+
return 'z.string()';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
toZodType(field, mode) {
|
|
69
|
+
let zodType = this.mapBaseZodType(field.type, field);
|
|
70
|
+
zodType = this.applyValidationRules(field, zodType);
|
|
71
|
+
if (field.type.optional) {
|
|
72
|
+
zodType = `${zodType}.nullable()`;
|
|
73
|
+
}
|
|
74
|
+
if (mode === 'update') {
|
|
75
|
+
zodType = `${zodType}.optional()`;
|
|
76
|
+
}
|
|
77
|
+
return zodType;
|
|
78
|
+
}
|
|
79
|
+
mapBaseZodType(type, field) {
|
|
80
|
+
const enumType = this.schema.enums.find((enumDef) => enumDef.name === type.name);
|
|
81
|
+
if (enumType) {
|
|
82
|
+
const values = enumType.values.map((value) => `'${value}'`).join(', ');
|
|
83
|
+
return `z.enum([${values}])`;
|
|
84
|
+
}
|
|
85
|
+
if (type.array && type.name === 'TEXT') {
|
|
86
|
+
return 'z.array(z.string())';
|
|
87
|
+
}
|
|
88
|
+
switch (type.name) {
|
|
89
|
+
case 'UUID':
|
|
90
|
+
case 'VARCHAR':
|
|
91
|
+
case 'TEXT':
|
|
92
|
+
return 'z.string()';
|
|
93
|
+
case 'INTEGER':
|
|
94
|
+
case 'SERIAL':
|
|
95
|
+
case 'SMALLINT':
|
|
96
|
+
return 'z.number().int()';
|
|
97
|
+
case 'BOOLEAN':
|
|
98
|
+
return 'z.boolean()';
|
|
99
|
+
case 'TIMESTAMP':
|
|
100
|
+
return 'z.coerce.date()';
|
|
101
|
+
case 'DECIMAL':
|
|
102
|
+
return 'z.string()';
|
|
103
|
+
case 'JSONB':
|
|
104
|
+
return 'z.record(z.unknown())';
|
|
105
|
+
case 'POINT':
|
|
106
|
+
return 'z.unknown()';
|
|
107
|
+
default:
|
|
108
|
+
return 'z.unknown()';
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
applyValidationRules(field, zodType) {
|
|
112
|
+
const regexAttr = getFieldAttribute(field, 'regex');
|
|
113
|
+
if (regexAttr?.args?.kind === 'KeyValueArgs') {
|
|
114
|
+
const pattern = getOptionalKvPair(regexAttr.args, 'pattern');
|
|
115
|
+
const message = getOptionalKvPair(regexAttr.args, 'message');
|
|
116
|
+
if (pattern?.value.kind === 'StringLiteral' && message?.value.kind === 'StringLiteral') {
|
|
117
|
+
const escapedPattern = this.escapeRegexLiteral(pattern.value.value);
|
|
118
|
+
const escapedMessage = this.escapeStringLiteral(message.value.value);
|
|
119
|
+
zodType = `${zodType}.regex(/${escapedPattern}/, { message: '${escapedMessage}' })`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const rangeAttr = getFieldAttribute(field, 'range');
|
|
123
|
+
if (rangeAttr?.args?.kind === 'KeyValueArgs') {
|
|
124
|
+
const min = getOptionalKvPair(rangeAttr.args, 'min');
|
|
125
|
+
const max = getOptionalKvPair(rangeAttr.args, 'max');
|
|
126
|
+
const message = getOptionalKvPair(rangeAttr.args, 'message');
|
|
127
|
+
if (min?.value.kind === 'NumberLiteral' &&
|
|
128
|
+
max?.value.kind === 'NumberLiteral' &&
|
|
129
|
+
message?.value.kind === 'StringLiteral') {
|
|
130
|
+
const escapedMessage = this.escapeStringLiteral(message.value.value);
|
|
131
|
+
zodType = `${zodType}.min(${min.value.value}, { message: '${escapedMessage}' }).max(${max.value.value}, { message: '${escapedMessage}' })`;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return zodType;
|
|
135
|
+
}
|
|
136
|
+
escapeRegexLiteral(pattern) {
|
|
137
|
+
return pattern.replace(/\//g, '\\/');
|
|
138
|
+
}
|
|
139
|
+
escapeStringLiteral(value) {
|
|
140
|
+
return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
export function generateValidationSchemas(schema) {
|
|
144
|
+
return new ZodSchemaGenerator(schema).generate();
|
|
145
|
+
}
|
package/dist/cli/db.d.ts
ADDED