schematic-pg 0.1.6 → 0.1.8
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/README.md +256 -11
- package/dist/api/hooks/define.d.ts +10 -0
- package/dist/api/hooks/define.js +3 -0
- package/dist/api/hooks/index.d.ts +4 -0
- package/dist/api/hooks/index.js +2 -0
- package/dist/api/hooks/registry.d.ts +8 -0
- package/dist/api/hooks/registry.js +94 -0
- package/dist/api/hooks/types.d.ts +48 -0
- package/dist/api/hooks/types.js +1 -0
- package/dist/api/utils/include-query.d.ts +6 -0
- package/dist/api/utils/include-query.js +75 -0
- package/dist/api/utils/read-query.d.ts +12 -0
- package/dist/api/utils/read-query.js +10 -0
- package/dist/api/utils/response-shape.d.ts +2 -0
- package/dist/api/utils/response-shape.js +28 -0
- package/dist/api-generator/app-generator.js +3 -0
- package/dist/api-generator/hook-scanner.d.ts +11 -0
- package/dist/api-generator/hook-scanner.js +36 -0
- package/dist/api-generator/hooks-generator.d.ts +2 -0
- package/dist/api-generator/hooks-generator.js +25 -0
- package/dist/api-generator/index.d.ts +2 -0
- package/dist/api-generator/index.js +7 -1
- package/dist/api-generator/route-generator.d.ts +4 -2
- package/dist/api-generator/route-generator.js +132 -82
- package/dist/api-generator/utils/api-fields.d.ts +6 -0
- package/dist/api-generator/utils/api-fields.js +32 -0
- package/dist/api-generator/zod-schema-generator.d.ts +3 -0
- package/dist/api-generator/zod-schema-generator.js +66 -8
- package/dist/cli/dev.js +5 -36
- package/dist/cli/generate.js +1 -0
- package/dist/cli/hooks.d.ts +6 -0
- package/dist/cli/hooks.js +85 -0
- package/dist/cli/init.js +6 -1
- package/dist/cli/paths.d.ts +1 -0
- package/dist/cli/paths.js +1 -0
- package/dist/cli/server.d.ts +5 -0
- package/dist/cli/server.js +60 -0
- package/dist/cli/start.d.ts +7 -0
- package/dist/cli/start.js +35 -0
- package/dist/cli/templates.d.ts +2 -1
- package/dist/cli/templates.js +37 -1
- package/dist/cli.js +10 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/package.json +7 -1
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare function shapeResponse<T extends Record<string, unknown>>(row: T, modelName: string, omitByModel: Readonly<Record<string, readonly string[]>>, relationTargets: Readonly<Record<string, Readonly<Record<string, string>>>>): T;
|
|
2
|
+
export declare function shapeResponseMany<T extends Record<string, unknown>>(rows: T[], modelName: string, omitByModel: Readonly<Record<string, readonly string[]>>, relationTargets: Readonly<Record<string, Readonly<Record<string, string>>>>): T[];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function shapeResponse(row, modelName, omitByModel, relationTargets) {
|
|
2
|
+
const omitted = omitByModel[modelName] ?? [];
|
|
3
|
+
const relations = relationTargets[modelName] ?? {};
|
|
4
|
+
const result = { ...row };
|
|
5
|
+
for (const field of omitted) {
|
|
6
|
+
delete result[field];
|
|
7
|
+
}
|
|
8
|
+
for (const [relationName, targetModel] of Object.entries(relations)) {
|
|
9
|
+
if (!(relationName in result)) {
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
const value = result[relationName];
|
|
13
|
+
if (value === null || value === undefined) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (Array.isArray(value)) {
|
|
17
|
+
result[relationName] = value.map((entry) => shapeResponse(entry, targetModel, omitByModel, relationTargets));
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (typeof value === 'object') {
|
|
21
|
+
result[relationName] = shapeResponse(value, targetModel, omitByModel, relationTargets);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
export function shapeResponseMany(rows, modelName, omitByModel, relationTargets) {
|
|
27
|
+
return rows.map((row) => shapeResponse(row, modelName, omitByModel, relationTargets));
|
|
28
|
+
}
|
|
@@ -39,7 +39,9 @@ export class AppGenerator {
|
|
|
39
39
|
routeImports,
|
|
40
40
|
"import { createDbClient } from './db.js';",
|
|
41
41
|
"import { POLICIES } from './policies.js';",
|
|
42
|
+
"import { HOOKS } from './hooks.js';",
|
|
42
43
|
`import { configurePolicies } from '${PACKAGE_NAME}/api/auth/policy';`,
|
|
44
|
+
`import { configureHooks } from '${PACKAGE_NAME}/api/hooks';`,
|
|
43
45
|
`import { createAuthMiddleware } from '${PACKAGE_NAME}/api/auth/middleware';`,
|
|
44
46
|
`import { createJwtResolver } from '${PACKAGE_NAME}/api/auth/jwt-resolver';`,
|
|
45
47
|
`import type { AuthResolver } from '${PACKAGE_NAME}/api/auth/types';`,
|
|
@@ -48,6 +50,7 @@ export class AppGenerator {
|
|
|
48
50
|
`import type { AppEnv } from '${PACKAGE_NAME}/api/types';`,
|
|
49
51
|
'',
|
|
50
52
|
'configurePolicies(POLICIES);',
|
|
53
|
+
'configureHooks(HOOKS);',
|
|
51
54
|
'',
|
|
52
55
|
'export interface CreateAppOptions {',
|
|
53
56
|
' pool?: Pool;',
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Schema } from '../schema-dsl/ast.js';
|
|
2
|
+
export interface HookMountEntry {
|
|
3
|
+
modelName: string;
|
|
4
|
+
importName: string;
|
|
5
|
+
importPath: string;
|
|
6
|
+
}
|
|
7
|
+
export interface HookDiscoveryResult {
|
|
8
|
+
entries: HookMountEntry[];
|
|
9
|
+
modelsWithHooks: Set<string>;
|
|
10
|
+
}
|
|
11
|
+
export declare function discoverHooks(hooksDir: string, schema: Schema): HookDiscoveryResult;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
2
|
+
function isHookFile(filename) {
|
|
3
|
+
return (filename.endsWith('.ts') &&
|
|
4
|
+
!filename.endsWith('.test.ts') &&
|
|
5
|
+
!filename.endsWith('.d.ts') &&
|
|
6
|
+
!filename.startsWith('_'));
|
|
7
|
+
}
|
|
8
|
+
function toHookImportName(modelName) {
|
|
9
|
+
return `${modelName.charAt(0).toLowerCase()}${modelName.slice(1)}Hooks`;
|
|
10
|
+
}
|
|
11
|
+
export function discoverHooks(hooksDir, schema) {
|
|
12
|
+
if (!existsSync(hooksDir)) {
|
|
13
|
+
return { entries: [], modelsWithHooks: new Set() };
|
|
14
|
+
}
|
|
15
|
+
const modelNames = new Set(schema.models.map((model) => model.name));
|
|
16
|
+
const entries = [];
|
|
17
|
+
const modelsWithHooks = new Set();
|
|
18
|
+
for (const filename of readdirSync(hooksDir)) {
|
|
19
|
+
if (!isHookFile(filename)) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const modelName = filename.replace(/\.ts$/, '');
|
|
23
|
+
if (!modelNames.has(modelName)) {
|
|
24
|
+
console.warn(`Skipping hook file "${filename}": no matching model in schema`);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
entries.push({
|
|
28
|
+
modelName,
|
|
29
|
+
importName: toHookImportName(modelName),
|
|
30
|
+
importPath: `../src/hooks/${modelName}.js`,
|
|
31
|
+
});
|
|
32
|
+
modelsWithHooks.add(modelName);
|
|
33
|
+
}
|
|
34
|
+
entries.sort((left, right) => left.modelName.localeCompare(right.modelName));
|
|
35
|
+
return { entries, modelsWithHooks };
|
|
36
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function generateHooksFile(entries) {
|
|
2
|
+
if (entries.length === 0) {
|
|
3
|
+
return [
|
|
4
|
+
'// Auto-generated by HooksGenerator. Do not edit manually.',
|
|
5
|
+
'',
|
|
6
|
+
'export const HOOKS = {};',
|
|
7
|
+
'',
|
|
8
|
+
].join('\n');
|
|
9
|
+
}
|
|
10
|
+
const imports = entries
|
|
11
|
+
.map((entry) => `import ${entry.importName} from '${entry.importPath}';`)
|
|
12
|
+
.join('\n');
|
|
13
|
+
const registryEntries = entries
|
|
14
|
+
.map((entry) => ` ${entry.modelName}: ${entry.importName},`)
|
|
15
|
+
.join('\n');
|
|
16
|
+
return [
|
|
17
|
+
'// Auto-generated by HooksGenerator. Do not edit manually.',
|
|
18
|
+
imports,
|
|
19
|
+
'',
|
|
20
|
+
'export const HOOKS = {',
|
|
21
|
+
registryEntries,
|
|
22
|
+
'};',
|
|
23
|
+
'',
|
|
24
|
+
].join('\n');
|
|
25
|
+
}
|
|
@@ -3,9 +3,11 @@ export interface GeneratedApiFiles {
|
|
|
3
3
|
app: string;
|
|
4
4
|
policies: string;
|
|
5
5
|
validation: string;
|
|
6
|
+
hooks: string;
|
|
6
7
|
routes: Map<string, string>;
|
|
7
8
|
}
|
|
8
9
|
export interface GenerateApiFilesOptions {
|
|
9
10
|
customRoutesDir?: string;
|
|
11
|
+
hooksDir?: string;
|
|
10
12
|
}
|
|
11
13
|
export declare function generateApiFiles(schema: Schema, options?: GenerateApiFilesOptions): GeneratedApiFiles;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { DEFAULT_HOOKS_DIR } from '../cli/paths.js';
|
|
1
2
|
import { generateAppFile } from './app-generator.js';
|
|
3
|
+
import { discoverHooks } from './hook-scanner.js';
|
|
4
|
+
import { generateHooksFile } from './hooks-generator.js';
|
|
2
5
|
import { generatePoliciesFile } from './policy-generator.js';
|
|
3
6
|
import { generateRouteFiles } from './route-generator.js';
|
|
4
7
|
import { generateValidationSchemas } from './zod-schema-generator.js';
|
|
@@ -6,10 +9,13 @@ export function generateApiFiles(schema, options) {
|
|
|
6
9
|
const appOptions = options?.customRoutesDir
|
|
7
10
|
? { customRoutesDir: options.customRoutesDir }
|
|
8
11
|
: undefined;
|
|
12
|
+
const hooksDir = options?.hooksDir ?? DEFAULT_HOOKS_DIR;
|
|
13
|
+
const { entries: hookEntries, modelsWithHooks } = discoverHooks(hooksDir, schema);
|
|
9
14
|
return {
|
|
10
15
|
app: generateAppFile(schema, appOptions),
|
|
11
16
|
policies: generatePoliciesFile(schema),
|
|
12
17
|
validation: generateValidationSchemas(schema),
|
|
13
|
-
|
|
18
|
+
hooks: generateHooksFile(hookEntries),
|
|
19
|
+
routes: generateRouteFiles(schema, modelsWithHooks),
|
|
14
20
|
};
|
|
15
21
|
}
|
|
@@ -2,10 +2,12 @@ import type { Model, Schema } from '../schema-dsl/ast.js';
|
|
|
2
2
|
export declare class RouteGenerator {
|
|
3
3
|
private readonly model;
|
|
4
4
|
private readonly schema;
|
|
5
|
-
|
|
5
|
+
private readonly modelsWithHooks;
|
|
6
|
+
constructor(model: Model, schema: Schema, modelsWithHooks?: ReadonlySet<string>);
|
|
6
7
|
generate(): string;
|
|
7
8
|
private jsonRow;
|
|
8
9
|
private jsonRows;
|
|
10
|
+
private mutationJsonRow;
|
|
9
11
|
private generateListRoute;
|
|
10
12
|
private generateGetRoute;
|
|
11
13
|
private generateCreateRoute;
|
|
@@ -14,7 +16,7 @@ export declare class RouteGenerator {
|
|
|
14
16
|
getRouteFileName(): string;
|
|
15
17
|
getRouteBasePath(): string;
|
|
16
18
|
}
|
|
17
|
-
export declare function generateRouteFiles(schema: Schema): Map<string, string>;
|
|
19
|
+
export declare function generateRouteFiles(schema: Schema, modelsWithHooks?: ReadonlySet<string>): Map<string, string>;
|
|
18
20
|
export declare function getRouteMountEntries(schema: Schema): {
|
|
19
21
|
basePath: string;
|
|
20
22
|
fileName: string;
|
|
@@ -7,9 +7,11 @@ import { hasPolicies } from './utils/policy.js';
|
|
|
7
7
|
export class RouteGenerator {
|
|
8
8
|
model;
|
|
9
9
|
schema;
|
|
10
|
-
|
|
10
|
+
modelsWithHooks;
|
|
11
|
+
constructor(model, schema, modelsWithHooks = new Set()) {
|
|
11
12
|
this.model = model;
|
|
12
13
|
this.schema = schema;
|
|
14
|
+
this.modelsWithHooks = modelsWithHooks;
|
|
13
15
|
}
|
|
14
16
|
generate() {
|
|
15
17
|
const clientKey = getClientExportName(this.model.name);
|
|
@@ -21,7 +23,9 @@ export class RouteGenerator {
|
|
|
21
23
|
const whereFromParams = primaryKey.fields.map((field) => `${field}: params.${field}`).join(', ');
|
|
22
24
|
const paramSchemaName = `${this.model.name}ParamSchema`;
|
|
23
25
|
const listQuerySchemaName = `${this.model.name}ListQuerySchema`;
|
|
26
|
+
const getQuerySchemaName = `${this.model.name}GetQuerySchema`;
|
|
24
27
|
const modelHasPolicies = hasPolicies(this.model);
|
|
28
|
+
const modelHasHooks = this.modelsWithHooks.has(this.model.name);
|
|
25
29
|
const constantPrefix = toModelConstantPrefix(this.model.name);
|
|
26
30
|
return [
|
|
27
31
|
'// Auto-generated by RouteGenerator. Do not edit manually.',
|
|
@@ -29,65 +33,89 @@ export class RouteGenerator {
|
|
|
29
33
|
`import type { AppEnv } from '${PACKAGE_NAME}/api/types';`,
|
|
30
34
|
`import { validateJson, validateParam, validateQuery } from '${PACKAGE_NAME}/api/middleware/validate';`,
|
|
31
35
|
`import { notFoundResponse } from '${PACKAGE_NAME}/api/middleware/errors';`,
|
|
32
|
-
`import {
|
|
33
|
-
`import {
|
|
36
|
+
`import { buildReadQuery } from '${PACKAGE_NAME}/api/utils/read-query';`,
|
|
37
|
+
`import { parseIncludeQuery } from '${PACKAGE_NAME}/api/utils/include-query';`,
|
|
38
|
+
`import { omitFields } from '${PACKAGE_NAME}/api/utils/omit-fields';`,
|
|
39
|
+
`import { shapeResponse, shapeResponseMany } from '${PACKAGE_NAME}/api/utils/response-shape';`,
|
|
34
40
|
...(modelHasPolicies
|
|
35
41
|
? [
|
|
36
42
|
`import { assertPolicy, mergeWhere, resolvePolicyWhere } from '${PACKAGE_NAME}/api/auth/policy';`,
|
|
37
43
|
]
|
|
38
44
|
: []),
|
|
45
|
+
...(modelHasHooks
|
|
46
|
+
? [
|
|
47
|
+
`import { cancelledResponse, createHookContext, runAfterHooks, runBeforeHooks } from '${PACKAGE_NAME}/api/hooks';`,
|
|
48
|
+
]
|
|
49
|
+
: []),
|
|
39
50
|
`import {`,
|
|
40
51
|
` ${this.model.name}CreateSchema,`,
|
|
41
52
|
` ${this.model.name}UpdateSchema,`,
|
|
42
53
|
` ${paramSchemaName},`,
|
|
43
54
|
` ${listQuerySchemaName},`,
|
|
55
|
+
` ${getQuerySchemaName},`,
|
|
44
56
|
` ${constantPrefix}_LIST_QUERY_FIELDS,`,
|
|
57
|
+
` ${constantPrefix}_INCLUDABLE_RELATIONS,`,
|
|
45
58
|
` ${constantPrefix}_OMIT_FIELDS,`,
|
|
46
59
|
` ${constantPrefix}_SORTABLE_FIELDS,`,
|
|
60
|
+
` API_OMIT_FIELDS_BY_MODEL,`,
|
|
61
|
+
` API_RELATION_TARGETS,`,
|
|
47
62
|
`} from '../schemas/validation.js';`,
|
|
48
63
|
'',
|
|
49
64
|
'const router = new Hono<AppEnv>();',
|
|
50
65
|
'',
|
|
51
66
|
...this.generateListRoute(clientKey, modelHasPolicies, listQuerySchemaName, constantPrefix),
|
|
52
67
|
'',
|
|
53
|
-
...this.generateGetRoute(clientKey, pathParams, paramSchemaName, whereFromParams, modelHasPolicies, constantPrefix),
|
|
68
|
+
...this.generateGetRoute(clientKey, pathParams, paramSchemaName, getQuerySchemaName, whereFromParams, modelHasPolicies, constantPrefix),
|
|
54
69
|
'',
|
|
55
|
-
...this.generateCreateRoute(clientKey, modelHasPolicies, constantPrefix),
|
|
70
|
+
...this.generateCreateRoute(clientKey, modelHasPolicies, modelHasHooks, constantPrefix),
|
|
56
71
|
'',
|
|
57
|
-
...this.generateUpdateRoute(clientKey, pathParams, paramSchemaName, whereFromParams, modelHasPolicies, constantPrefix),
|
|
72
|
+
...this.generateUpdateRoute(clientKey, pathParams, paramSchemaName, whereFromParams, modelHasPolicies, modelHasHooks, constantPrefix),
|
|
58
73
|
'',
|
|
59
|
-
...this.generateDeleteRoute(clientKey, pathParams, paramSchemaName, whereFromParams, modelHasPolicies, constantPrefix),
|
|
74
|
+
...this.generateDeleteRoute(clientKey, pathParams, paramSchemaName, whereFromParams, modelHasPolicies, modelHasHooks, constantPrefix),
|
|
60
75
|
'',
|
|
61
76
|
'export default router;',
|
|
62
77
|
'',
|
|
63
78
|
].join('\n');
|
|
64
79
|
}
|
|
65
|
-
jsonRow(variableName, constantPrefix, statusCode) {
|
|
66
|
-
const payload = `
|
|
80
|
+
jsonRow(variableName, modelName, constantPrefix, statusCode) {
|
|
81
|
+
const payload = `shapeResponse(${variableName}, '${modelName}', API_OMIT_FIELDS_BY_MODEL, API_RELATION_TARGETS)`;
|
|
67
82
|
if (statusCode === undefined) {
|
|
68
83
|
return `c.json(${payload})`;
|
|
69
84
|
}
|
|
70
85
|
return `c.json(${payload}, ${statusCode})`;
|
|
71
86
|
}
|
|
72
|
-
jsonRows(variableName,
|
|
73
|
-
return `c.json(
|
|
87
|
+
jsonRows(variableName, modelName) {
|
|
88
|
+
return `c.json(shapeResponseMany(${variableName}, '${modelName}', API_OMIT_FIELDS_BY_MODEL, API_RELATION_TARGETS))`;
|
|
89
|
+
}
|
|
90
|
+
mutationJsonRow(variableName, constantPrefix, statusCode) {
|
|
91
|
+
const payload = `omitFields(${variableName}, ${constantPrefix}_OMIT_FIELDS)`;
|
|
92
|
+
if (statusCode === undefined) {
|
|
93
|
+
return `c.json(${payload})`;
|
|
94
|
+
}
|
|
95
|
+
return `c.json(${payload}, ${statusCode})`;
|
|
74
96
|
}
|
|
75
97
|
generateListRoute(clientKey, modelHasPolicies, listQuerySchemaName, constantPrefix) {
|
|
76
98
|
const listQueryBlock = [
|
|
77
99
|
` const query = c.req.valid('query');`,
|
|
78
|
-
` const { where, orderBy, take, skip } =
|
|
100
|
+
` const { where, orderBy, take, skip, include } = buildReadQuery(`,
|
|
79
101
|
` query,`,
|
|
80
102
|
` ${constantPrefix}_LIST_QUERY_FIELDS,`,
|
|
81
103
|
` ${constantPrefix}_SORTABLE_FIELDS,`,
|
|
104
|
+
` ${constantPrefix}_INCLUDABLE_RELATIONS,`,
|
|
82
105
|
` );`,
|
|
83
106
|
];
|
|
107
|
+
const findManyArgs = ['where', 'orderBy', 'take', 'skip', 'include']
|
|
108
|
+
.map((key) => ` ${key},`)
|
|
109
|
+
.join('\n');
|
|
84
110
|
if (!modelHasPolicies) {
|
|
85
111
|
return [
|
|
86
112
|
`router.get('/', validateQuery(${listQuerySchemaName}), async (c) => {`,
|
|
87
113
|
' const db = c.get(\'db\');',
|
|
88
114
|
...listQueryBlock,
|
|
89
|
-
` const rows = await db.${clientKey}.findMany({
|
|
90
|
-
|
|
115
|
+
` const rows = await db.${clientKey}.findMany({`,
|
|
116
|
+
findManyArgs,
|
|
117
|
+
' });',
|
|
118
|
+
` return ${this.jsonRows('rows', this.model.name)};`,
|
|
91
119
|
'});',
|
|
92
120
|
];
|
|
93
121
|
}
|
|
@@ -103,109 +131,131 @@ export class RouteGenerator {
|
|
|
103
131
|
' orderBy,',
|
|
104
132
|
' take,',
|
|
105
133
|
' skip,',
|
|
134
|
+
' include,',
|
|
106
135
|
' });',
|
|
107
|
-
` return ${this.jsonRows('rows',
|
|
136
|
+
` return ${this.jsonRows('rows', this.model.name)};`,
|
|
108
137
|
'});',
|
|
109
138
|
];
|
|
110
139
|
}
|
|
111
|
-
generateGetRoute(clientKey, pathParams, paramSchemaName, whereFromParams, modelHasPolicies, constantPrefix) {
|
|
140
|
+
generateGetRoute(clientKey, pathParams, paramSchemaName, getQuerySchemaName, whereFromParams, modelHasPolicies, constantPrefix) {
|
|
141
|
+
const includeBlock = [
|
|
142
|
+
' const query = c.req.valid(\'query\');',
|
|
143
|
+
` const include = query.include`,
|
|
144
|
+
` ? parseIncludeQuery(query.include, ${constantPrefix}_INCLUDABLE_RELATIONS)`,
|
|
145
|
+
' : undefined;',
|
|
146
|
+
];
|
|
112
147
|
if (!modelHasPolicies) {
|
|
113
148
|
return [
|
|
114
|
-
`router.get('/${pathParams}', validateParam(${paramSchemaName}), async (c) => {`,
|
|
149
|
+
`router.get('/${pathParams}', validateParam(${paramSchemaName}), validateQuery(${getQuerySchemaName}), async (c) => {`,
|
|
115
150
|
' const db = c.get(\'db\');',
|
|
116
151
|
' const params = c.req.valid(\'param\');',
|
|
117
|
-
|
|
152
|
+
...includeBlock,
|
|
153
|
+
` const row = await db.${clientKey}.findUnique({ ${whereFromParams} }, { include });`,
|
|
118
154
|
' if (!row) {',
|
|
119
155
|
' return notFoundResponse(c);',
|
|
120
156
|
' }',
|
|
121
|
-
` return ${this.jsonRow('row', constantPrefix)};`,
|
|
157
|
+
` return ${this.jsonRow('row', this.model.name, constantPrefix)};`,
|
|
122
158
|
'});',
|
|
123
159
|
];
|
|
124
160
|
}
|
|
125
161
|
return [
|
|
126
|
-
`router.get('/${pathParams}', validateParam(${paramSchemaName}), async (c) => {`,
|
|
162
|
+
`router.get('/${pathParams}', validateParam(${paramSchemaName}), validateQuery(${getQuerySchemaName}), async (c) => {`,
|
|
127
163
|
' const db = c.get(\'db\');',
|
|
128
164
|
' const auth = c.get(\'auth\');',
|
|
129
165
|
` const policy = assertPolicy('${this.model.name}', auth.role, 'select');`,
|
|
130
166
|
' const policyWhere = resolvePolicyWhere(policy, auth);',
|
|
131
167
|
' const params = c.req.valid(\'param\');',
|
|
132
|
-
|
|
168
|
+
...includeBlock,
|
|
169
|
+
` const row = await db.${clientKey}.findUnique(mergeWhere({ ${whereFromParams} }, policyWhere), { include });`,
|
|
133
170
|
' if (!row) {',
|
|
134
171
|
' return notFoundResponse(c);',
|
|
135
172
|
' }',
|
|
136
|
-
` return ${this.jsonRow('row', constantPrefix)};`,
|
|
173
|
+
` return ${this.jsonRow('row', this.model.name, constantPrefix)};`,
|
|
137
174
|
'});',
|
|
138
175
|
];
|
|
139
176
|
}
|
|
140
|
-
generateCreateRoute(clientKey, modelHasPolicies, constantPrefix) {
|
|
141
|
-
|
|
142
|
-
return [
|
|
143
|
-
`router.post('/', validateJson(${this.model.name}CreateSchema), async (c) => {`,
|
|
144
|
-
' const db = c.get(\'db\');',
|
|
145
|
-
' const body = c.req.valid(\'json\');',
|
|
146
|
-
` const row = await db.${clientKey}.create(body);`,
|
|
147
|
-
` return ${this.jsonRow('row', constantPrefix, 201)};`,
|
|
148
|
-
'});',
|
|
149
|
-
];
|
|
150
|
-
}
|
|
151
|
-
return [
|
|
177
|
+
generateCreateRoute(clientKey, modelHasPolicies, modelHasHooks, constantPrefix) {
|
|
178
|
+
const lines = [
|
|
152
179
|
`router.post('/', validateJson(${this.model.name}CreateSchema), async (c) => {`,
|
|
153
180
|
' const db = c.get(\'db\');',
|
|
154
|
-
' const auth = c.get(\'auth\');',
|
|
155
|
-
` assertPolicy('${this.model.name}', auth.role, 'insert');`,
|
|
156
|
-
' const body = c.req.valid(\'json\');',
|
|
157
|
-
` const row = await db.${clientKey}.create(body);`,
|
|
158
|
-
` return ${this.jsonRow('row', constantPrefix, 201)};`,
|
|
159
|
-
'});',
|
|
160
181
|
];
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (!modelHasPolicies) {
|
|
164
|
-
return [
|
|
165
|
-
`router.put('/${pathParams}', validateParam(${paramSchemaName}), validateJson(${this.model.name}UpdateSchema), async (c) => {`,
|
|
166
|
-
' const db = c.get(\'db\');',
|
|
167
|
-
' const params = c.req.valid(\'param\');',
|
|
168
|
-
' const body = c.req.valid(\'json\');',
|
|
169
|
-
` const row = await db.${clientKey}.update({ where: { ${whereFromParams} }, data: body });`,
|
|
170
|
-
` return ${this.jsonRow('row', constantPrefix)};`,
|
|
171
|
-
'});',
|
|
172
|
-
];
|
|
182
|
+
if (modelHasPolicies || modelHasHooks) {
|
|
183
|
+
lines.push(' const auth = c.get(\'auth\');');
|
|
173
184
|
}
|
|
174
|
-
|
|
185
|
+
if (modelHasPolicies) {
|
|
186
|
+
lines.push(` assertPolicy('${this.model.name}', auth.role, 'insert');`);
|
|
187
|
+
}
|
|
188
|
+
lines.push(' const body = c.req.valid(\'json\');');
|
|
189
|
+
if (modelHasHooks) {
|
|
190
|
+
lines.push(` const hookCtx = createHookContext({ c, db, auth, model: '${this.model.name}', operation: 'create', data: body });`, ` const gate = await runBeforeHooks('${this.model.name}', 'create', hookCtx);`, ' if (!gate.proceed) return gate.response ?? cancelledResponse(c);', ` const row = await db.${clientKey}.create(hookCtx.data);`, ' hookCtx.result = row;', ` await runAfterHooks('${this.model.name}', 'create', hookCtx);`, ` return ${this.mutationJsonRow('hookCtx.result', constantPrefix, 201)};`);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
lines.push(` const row = await db.${clientKey}.create(body);`, ` return ${this.mutationJsonRow('row', constantPrefix, 201)};`);
|
|
194
|
+
}
|
|
195
|
+
lines.push('});');
|
|
196
|
+
return lines;
|
|
197
|
+
}
|
|
198
|
+
generateUpdateRoute(clientKey, pathParams, paramSchemaName, whereFromParams, modelHasPolicies, modelHasHooks, constantPrefix) {
|
|
199
|
+
const lines = [
|
|
175
200
|
`router.put('/${pathParams}', validateParam(${paramSchemaName}), validateJson(${this.model.name}UpdateSchema), async (c) => {`,
|
|
176
201
|
' const db = c.get(\'db\');',
|
|
177
|
-
' const auth = c.get(\'auth\');',
|
|
178
|
-
` const policy = assertPolicy('${this.model.name}', auth.role, 'update');`,
|
|
179
|
-
' const policyWhere = resolvePolicyWhere(policy, auth);',
|
|
180
|
-
' const params = c.req.valid(\'param\');',
|
|
181
|
-
' const body = c.req.valid(\'json\');',
|
|
182
|
-
` const row = await db.${clientKey}.update({ where: mergeWhere({ ${whereFromParams} }, policyWhere), data: body });`,
|
|
183
|
-
` return ${this.jsonRow('row', constantPrefix)};`,
|
|
184
|
-
'});',
|
|
185
202
|
];
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (!modelHasPolicies) {
|
|
189
|
-
return [
|
|
190
|
-
`router.delete('/${pathParams}', validateParam(${paramSchemaName}), async (c) => {`,
|
|
191
|
-
' const db = c.get(\'db\');',
|
|
192
|
-
' const params = c.req.valid(\'param\');',
|
|
193
|
-
` const row = await db.${clientKey}.delete({ ${whereFromParams} });`,
|
|
194
|
-
` return ${this.jsonRow('row', constantPrefix)};`,
|
|
195
|
-
'});',
|
|
196
|
-
];
|
|
203
|
+
if (modelHasPolicies || modelHasHooks) {
|
|
204
|
+
lines.push(' const auth = c.get(\'auth\');');
|
|
197
205
|
}
|
|
198
|
-
|
|
206
|
+
if (modelHasPolicies) {
|
|
207
|
+
lines.push(` const policy = assertPolicy('${this.model.name}', auth.role, 'update');`, ' const policyWhere = resolvePolicyWhere(policy, auth);');
|
|
208
|
+
}
|
|
209
|
+
lines.push(' const params = c.req.valid(\'param\');', ' const body = c.req.valid(\'json\');');
|
|
210
|
+
if (modelHasHooks) {
|
|
211
|
+
lines.push(` const hookCtx = createHookContext({ c, db, auth, model: '${this.model.name}', operation: 'update', data: body, params });`, ` const gate = await runBeforeHooks('${this.model.name}', 'update', hookCtx);`, ' if (!gate.proceed) return gate.response ?? cancelledResponse(c);');
|
|
212
|
+
if (modelHasPolicies) {
|
|
213
|
+
lines.push(` const row = await db.${clientKey}.update({ where: mergeWhere({ ${whereFromParams} }, policyWhere), data: hookCtx.data });`);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
lines.push(` const row = await db.${clientKey}.update({ where: { ${whereFromParams} }, data: hookCtx.data });`);
|
|
217
|
+
}
|
|
218
|
+
lines.push(' hookCtx.result = row;', ` await runAfterHooks('${this.model.name}', 'update', hookCtx);`, ` return ${this.mutationJsonRow('hookCtx.result', constantPrefix)};`);
|
|
219
|
+
}
|
|
220
|
+
else if (modelHasPolicies) {
|
|
221
|
+
lines.push(` const row = await db.${clientKey}.update({ where: mergeWhere({ ${whereFromParams} }, policyWhere), data: body });`, ` return ${this.mutationJsonRow('row', constantPrefix)};`);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
lines.push(` const row = await db.${clientKey}.update({ where: { ${whereFromParams} }, data: body });`, ` return ${this.mutationJsonRow('row', constantPrefix)};`);
|
|
225
|
+
}
|
|
226
|
+
lines.push('});');
|
|
227
|
+
return lines;
|
|
228
|
+
}
|
|
229
|
+
generateDeleteRoute(clientKey, pathParams, paramSchemaName, whereFromParams, modelHasPolicies, modelHasHooks, constantPrefix) {
|
|
230
|
+
const lines = [
|
|
199
231
|
`router.delete('/${pathParams}', validateParam(${paramSchemaName}), async (c) => {`,
|
|
200
232
|
' const db = c.get(\'db\');',
|
|
201
|
-
' const auth = c.get(\'auth\');',
|
|
202
|
-
` const policy = assertPolicy('${this.model.name}', auth.role, 'delete');`,
|
|
203
|
-
' const policyWhere = resolvePolicyWhere(policy, auth);',
|
|
204
|
-
' const params = c.req.valid(\'param\');',
|
|
205
|
-
` const row = await db.${clientKey}.delete(mergeWhere({ ${whereFromParams} }, policyWhere));`,
|
|
206
|
-
` return ${this.jsonRow('row', constantPrefix)};`,
|
|
207
|
-
'});',
|
|
208
233
|
];
|
|
234
|
+
if (modelHasPolicies || modelHasHooks) {
|
|
235
|
+
lines.push(' const auth = c.get(\'auth\');');
|
|
236
|
+
}
|
|
237
|
+
if (modelHasPolicies) {
|
|
238
|
+
lines.push(` const policy = assertPolicy('${this.model.name}', auth.role, 'delete');`, ' const policyWhere = resolvePolicyWhere(policy, auth);');
|
|
239
|
+
}
|
|
240
|
+
lines.push(' const params = c.req.valid(\'param\');');
|
|
241
|
+
if (modelHasHooks) {
|
|
242
|
+
lines.push(` const hookCtx = createHookContext({ c, db, auth, model: '${this.model.name}', operation: 'delete', params });`, ` const gate = await runBeforeHooks('${this.model.name}', 'delete', hookCtx);`, ' if (!gate.proceed) return gate.response ?? cancelledResponse(c);');
|
|
243
|
+
if (modelHasPolicies) {
|
|
244
|
+
lines.push(` const row = await db.${clientKey}.delete(mergeWhere({ ${whereFromParams} }, policyWhere));`);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
lines.push(` const row = await db.${clientKey}.delete({ ${whereFromParams} });`);
|
|
248
|
+
}
|
|
249
|
+
lines.push(' hookCtx.result = row;', ` await runAfterHooks('${this.model.name}', 'delete', hookCtx);`, ` return ${this.mutationJsonRow('hookCtx.result', constantPrefix)};`);
|
|
250
|
+
}
|
|
251
|
+
else if (modelHasPolicies) {
|
|
252
|
+
lines.push(` const row = await db.${clientKey}.delete(mergeWhere({ ${whereFromParams} }, policyWhere));`, ` return ${this.mutationJsonRow('row', constantPrefix)};`);
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
lines.push(` const row = await db.${clientKey}.delete({ ${whereFromParams} });`, ` return ${this.mutationJsonRow('row', constantPrefix)};`);
|
|
256
|
+
}
|
|
257
|
+
lines.push('});');
|
|
258
|
+
return lines;
|
|
209
259
|
}
|
|
210
260
|
getRouteFileName() {
|
|
211
261
|
return toRouteFileName(this.model.name);
|
|
@@ -214,10 +264,10 @@ export class RouteGenerator {
|
|
|
214
264
|
return toRouteBasePath(this.model.name);
|
|
215
265
|
}
|
|
216
266
|
}
|
|
217
|
-
export function generateRouteFiles(schema) {
|
|
267
|
+
export function generateRouteFiles(schema, modelsWithHooks = new Set()) {
|
|
218
268
|
const files = new Map();
|
|
219
269
|
for (const model of schema.models) {
|
|
220
|
-
const generator = new RouteGenerator(model, schema);
|
|
270
|
+
const generator = new RouteGenerator(model, schema, modelsWithHooks);
|
|
221
271
|
files.set(generator.getRouteFileName(), generator.generate());
|
|
222
272
|
}
|
|
223
273
|
return files;
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import type { Field, Model, Schema } from '../../schema-dsl/ast.js';
|
|
2
|
+
import type { IncludableRelationTree } from '../../api/utils/include-query.js';
|
|
2
3
|
export declare function isStoredScalarField(field: Field, schema: Schema): boolean;
|
|
4
|
+
export declare function isRelationField(field: Field, schema: Schema): boolean;
|
|
3
5
|
export declare function isUnfilterable(field: Field): boolean;
|
|
6
|
+
export declare function isUnincludeable(field: Field): boolean;
|
|
4
7
|
export declare function isOmitted(field: Field): boolean;
|
|
5
8
|
export declare function getFilterableFields(model: Model, schema: Schema): Field[];
|
|
6
9
|
export declare function getOmittedFields(model: Model, schema: Schema): Field[];
|
|
10
|
+
export declare function getIncludableRelationFields(model: Model, schema: Schema): Field[];
|
|
11
|
+
export declare function buildIncludableRelationTree(model: Model, schema: Schema, visited?: Set<string>): IncludableRelationTree;
|
|
12
|
+
export declare function buildRelationTargets(model: Model, schema: Schema): Record<string, string>;
|
|
7
13
|
export declare function getSortableFieldNames(model: Model, schema: Schema): string[];
|
|
8
14
|
export declare function toModelConstantPrefix(modelName: string): string;
|
|
@@ -3,9 +3,15 @@ export function isStoredScalarField(field, schema) {
|
|
|
3
3
|
const modelNames = getModelNames(schema);
|
|
4
4
|
return !modelNames.has(field.type.name);
|
|
5
5
|
}
|
|
6
|
+
export function isRelationField(field, schema) {
|
|
7
|
+
return getModelNames(schema).has(field.type.name);
|
|
8
|
+
}
|
|
6
9
|
export function isUnfilterable(field) {
|
|
7
10
|
return fieldHasAttribute(field, 'unfilterable') || fieldHasAttribute(field, 'omit');
|
|
8
11
|
}
|
|
12
|
+
export function isUnincludeable(field) {
|
|
13
|
+
return fieldHasAttribute(field, 'unincludeable');
|
|
14
|
+
}
|
|
9
15
|
export function isOmitted(field) {
|
|
10
16
|
return fieldHasAttribute(field, 'omit');
|
|
11
17
|
}
|
|
@@ -15,6 +21,32 @@ export function getFilterableFields(model, schema) {
|
|
|
15
21
|
export function getOmittedFields(model, schema) {
|
|
16
22
|
return getStoredFields(model, getModelNames(schema)).filter((field) => isStoredScalarField(field, schema) && isOmitted(field));
|
|
17
23
|
}
|
|
24
|
+
export function getIncludableRelationFields(model, schema) {
|
|
25
|
+
return model.fields.filter((field) => isRelationField(field, schema) && !isUnincludeable(field));
|
|
26
|
+
}
|
|
27
|
+
export function buildIncludableRelationTree(model, schema, visited = new Set()) {
|
|
28
|
+
if (visited.has(model.name)) {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
const nextVisited = new Set(visited);
|
|
32
|
+
nextVisited.add(model.name);
|
|
33
|
+
const tree = {};
|
|
34
|
+
for (const field of getIncludableRelationFields(model, schema)) {
|
|
35
|
+
const targetModel = schema.models.find((candidate) => candidate.name === field.type.name);
|
|
36
|
+
if (!targetModel) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
tree[field.name] = buildIncludableRelationTree(targetModel, schema, nextVisited);
|
|
40
|
+
}
|
|
41
|
+
return tree;
|
|
42
|
+
}
|
|
43
|
+
export function buildRelationTargets(model, schema) {
|
|
44
|
+
const targets = {};
|
|
45
|
+
for (const field of getIncludableRelationFields(model, schema)) {
|
|
46
|
+
targets[field.name] = field.type.name;
|
|
47
|
+
}
|
|
48
|
+
return targets;
|
|
49
|
+
}
|
|
18
50
|
export function getSortableFieldNames(model, schema) {
|
|
19
51
|
return getStoredFields(model, getModelNames(schema))
|
|
20
52
|
.filter((field) => isStoredScalarField(field, schema))
|
|
@@ -3,8 +3,11 @@ export declare class ZodSchemaGenerator {
|
|
|
3
3
|
private readonly schema;
|
|
4
4
|
constructor(schema: Schema);
|
|
5
5
|
generate(): string;
|
|
6
|
+
private generateGlobalMetadata;
|
|
6
7
|
private generateModelSchemas;
|
|
7
8
|
private generateListQuerySchemas;
|
|
9
|
+
private generateIncludeRefinement;
|
|
10
|
+
private generateReadQueryRefinement;
|
|
8
11
|
private generateListQueryFieldLines;
|
|
9
12
|
private generateObjectField;
|
|
10
13
|
private generateParamField;
|