@vertz/codegen 0.2.14 → 0.2.16
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/dist/index.d.ts +54 -1
- package/dist/index.js +369 -41
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ interface CodegenTypescriptConfig {
|
|
|
16
16
|
publishable?: CodegenPublishableConfig;
|
|
17
17
|
/** Augmentable types for customer-specific type narrowing */
|
|
18
18
|
augmentableTypes?: string[];
|
|
19
|
+
/** Generate PostgreSQL RLS policies from rules.where() conditions. Default: false */
|
|
20
|
+
rls?: boolean;
|
|
19
21
|
}
|
|
20
22
|
interface CodegenConfig {
|
|
21
23
|
/** Generators to run. Default: ['typescript'] */
|
|
@@ -48,7 +50,30 @@ interface CodegenIR {
|
|
|
48
50
|
schemas: CodegenSchema[];
|
|
49
51
|
entities: CodegenEntityModule[];
|
|
50
52
|
auth: CodegenAuth;
|
|
53
|
+
access?: CodegenAccess;
|
|
51
54
|
}
|
|
55
|
+
interface CodegenAccess {
|
|
56
|
+
entities: CodegenAccessEntity[];
|
|
57
|
+
entitlements: string[];
|
|
58
|
+
whereClauses: CodegenWhereClause[];
|
|
59
|
+
}
|
|
60
|
+
interface CodegenAccessEntity {
|
|
61
|
+
name: string;
|
|
62
|
+
roles: string[];
|
|
63
|
+
}
|
|
64
|
+
interface CodegenWhereClause {
|
|
65
|
+
entitlement: string;
|
|
66
|
+
conditions: CodegenWhereCondition[];
|
|
67
|
+
}
|
|
68
|
+
type CodegenWhereCondition = {
|
|
69
|
+
kind: "marker";
|
|
70
|
+
column: string;
|
|
71
|
+
marker: "user.id" | "user.tenantId";
|
|
72
|
+
} | {
|
|
73
|
+
kind: "literal";
|
|
74
|
+
column: string;
|
|
75
|
+
value: string | number | boolean;
|
|
76
|
+
};
|
|
52
77
|
interface CodegenModule {
|
|
53
78
|
name: string;
|
|
54
79
|
operations: CodegenOperation[];
|
|
@@ -148,6 +173,17 @@ interface CodegenEntityModule {
|
|
|
148
173
|
operations: CodegenEntityOperation[];
|
|
149
174
|
actions: CodegenEntityAction[];
|
|
150
175
|
relations?: CodegenRelation[];
|
|
176
|
+
tenantScoped?: boolean;
|
|
177
|
+
table?: string;
|
|
178
|
+
primaryKey?: string;
|
|
179
|
+
hiddenFields?: string[];
|
|
180
|
+
responseFields?: CodegenResolvedField[];
|
|
181
|
+
relationSelections?: Record<string, "all" | string[]>;
|
|
182
|
+
relationQueryConfig?: Record<string, {
|
|
183
|
+
allowWhere?: string[];
|
|
184
|
+
allowOrderBy?: string[];
|
|
185
|
+
maxLimit?: number;
|
|
186
|
+
}>;
|
|
151
187
|
}
|
|
152
188
|
interface CodegenEntityOperation {
|
|
153
189
|
kind: "list" | "get" | "create" | "update" | "delete";
|
|
@@ -265,6 +301,23 @@ declare class EntitySchemaGenerator implements Generator {
|
|
|
265
301
|
private generateEntitySchema;
|
|
266
302
|
private generateIndex;
|
|
267
303
|
}
|
|
304
|
+
interface EntitySchemaRelation {
|
|
305
|
+
type: "one" | "many";
|
|
306
|
+
entity: string;
|
|
307
|
+
selection: "all" | string[];
|
|
308
|
+
allowWhere: string[];
|
|
309
|
+
allowOrderBy: string[];
|
|
310
|
+
maxLimit?: number;
|
|
311
|
+
}
|
|
312
|
+
interface EntitySchemaManifestEntry {
|
|
313
|
+
table?: string;
|
|
314
|
+
primaryKey?: string;
|
|
315
|
+
tenantScoped: boolean;
|
|
316
|
+
hiddenFields: string[];
|
|
317
|
+
fields: string[];
|
|
318
|
+
relations: Record<string, EntitySchemaRelation>;
|
|
319
|
+
}
|
|
320
|
+
type EntitySchemaManifest = Record<string, EntitySchemaManifestEntry>;
|
|
268
321
|
declare class EntitySdkGenerator implements Generator {
|
|
269
322
|
readonly name = "entity-sdk";
|
|
270
323
|
generate(ir: CodegenIR, _config: GeneratorConfig): GeneratedFile[];
|
|
@@ -316,4 +369,4 @@ declare function toPascalCase(input: string): string;
|
|
|
316
369
|
declare function toCamelCase(input: string): string;
|
|
317
370
|
declare function toKebabCase(input: string): string;
|
|
318
371
|
declare function toSnakeCase(input: string): string;
|
|
319
|
-
export { writeIncremental, validateCodegenConfig, toSnakeCase, toPascalCase, toKebabCase, toCamelCase, resolveCodegenConfig, renderImports, mergeImportsToPackageJson, mergeImports, jsonSchemaToTS, hashContent, generateRelationManifest, generate, formatWithBiome, defineCodegenConfig, createCodegenPipeline, adaptIR, StreamingConfig, SchemaNamingParts, SchemaAnnotations, ResolvedCodegenConfig, RelationManifestEntry, OperationSchemaRefs, OperationAuth, OAuthFlows, JsonSchema, IncrementalResult, IncrementalOptions, Import, HttpMethod, GeneratorName, GeneratorConfig, Generator, GeneratedFile, GenerateResult, FileFragment, EntityTypesGenerator, EntitySdkGenerator, EntitySchemaGenerator, ConversionResult, ConversionContext, CodegenTypescriptConfig, CodegenSchema, CodegenResolvedField, CodegenRelation, CodegenPublishableConfig, CodegenPipeline, CodegenOperation, CodegenModule, CodegenIR, CodegenEntityOperation, CodegenEntityModule, CodegenEntityAction, CodegenConfig, CodegenAuthScheme, CodegenAuth, ClientGenerator };
|
|
372
|
+
export { writeIncremental, validateCodegenConfig, toSnakeCase, toPascalCase, toKebabCase, toCamelCase, resolveCodegenConfig, renderImports, mergeImportsToPackageJson, mergeImports, jsonSchemaToTS, hashContent, generateRelationManifest, generate, formatWithBiome, defineCodegenConfig, createCodegenPipeline, adaptIR, StreamingConfig, SchemaNamingParts, SchemaAnnotations, ResolvedCodegenConfig, RelationManifestEntry, OperationSchemaRefs, OperationAuth, OAuthFlows, JsonSchema, IncrementalResult, IncrementalOptions, Import, HttpMethod, GeneratorName, GeneratorConfig, Generator, GeneratedFile, GenerateResult, FileFragment, EntityTypesGenerator, EntitySdkGenerator, EntitySchemaRelation, EntitySchemaManifestEntry, EntitySchemaManifest, EntitySchemaGenerator, ConversionResult, ConversionContext, CodegenTypescriptConfig, CodegenSchema, CodegenResolvedField, CodegenRelation, CodegenPublishableConfig, CodegenPipeline, CodegenOperation, CodegenModule, CodegenIR, CodegenEntityOperation, CodegenEntityModule, CodegenEntityAction, CodegenConfig, CodegenAuthScheme, CodegenAuth, ClientGenerator };
|
package/dist/index.js
CHANGED
|
@@ -116,7 +116,63 @@ async function formatWithBiome(files) {
|
|
|
116
116
|
}
|
|
117
117
|
// src/generate.ts
|
|
118
118
|
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
119
|
-
import { dirname as
|
|
119
|
+
import { dirname as dirname4, join as join4, resolve as resolve4 } from "node:path";
|
|
120
|
+
|
|
121
|
+
// src/generators/access-types-generator.ts
|
|
122
|
+
var FILE_HEADER = `// Generated by @vertz/codegen — do not edit
|
|
123
|
+
|
|
124
|
+
`;
|
|
125
|
+
|
|
126
|
+
class AccessTypesGenerator {
|
|
127
|
+
name = "access-types";
|
|
128
|
+
generate(ir, _config) {
|
|
129
|
+
if (!ir.access || ir.access.entitlements.length === 0) {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
return [
|
|
133
|
+
{
|
|
134
|
+
path: "access.d.ts",
|
|
135
|
+
content: renderAccessTypes(ir.access)
|
|
136
|
+
}
|
|
137
|
+
];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function escapeStringLiteral(s) {
|
|
141
|
+
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
142
|
+
}
|
|
143
|
+
function renderAccessTypes(access) {
|
|
144
|
+
const lines = [FILE_HEADER];
|
|
145
|
+
const registryEntries = access.entitlements.map((e) => ` '${escapeStringLiteral(e)}': true;`).join(`
|
|
146
|
+
`);
|
|
147
|
+
lines.push("declare module '@vertz/server' {");
|
|
148
|
+
lines.push(" interface EntitlementRegistry {");
|
|
149
|
+
lines.push(registryEntries);
|
|
150
|
+
lines.push(" }");
|
|
151
|
+
lines.push("}");
|
|
152
|
+
lines.push("");
|
|
153
|
+
lines.push("declare module '@vertz/ui/auth' {");
|
|
154
|
+
lines.push(" interface EntitlementRegistry {");
|
|
155
|
+
lines.push(registryEntries);
|
|
156
|
+
lines.push(" }");
|
|
157
|
+
lines.push("}");
|
|
158
|
+
lines.push("");
|
|
159
|
+
const entitiesWithRoles = access.entities.filter((e) => e.roles.length > 0);
|
|
160
|
+
if (entitiesWithRoles.length > 0) {
|
|
161
|
+
const resourceUnion = entitiesWithRoles.map((e) => `'${escapeStringLiteral(e.name)}'`).join(" | ");
|
|
162
|
+
lines.push(`type ResourceType = ${resourceUnion};`);
|
|
163
|
+
lines.push("");
|
|
164
|
+
lines.push("type Role<T extends ResourceType> =");
|
|
165
|
+
entitiesWithRoles.forEach((entity, i) => {
|
|
166
|
+
const rolesUnion = entity.roles.map((r) => `'${escapeStringLiteral(r)}'`).join(" | ");
|
|
167
|
+
const prefix = i === 0 ? " " : " : ";
|
|
168
|
+
lines.push(`${prefix}T extends '${escapeStringLiteral(entity.name)}' ? ${rolesUnion}`);
|
|
169
|
+
});
|
|
170
|
+
lines.push(" : never;");
|
|
171
|
+
lines.push("");
|
|
172
|
+
}
|
|
173
|
+
return lines.join(`
|
|
174
|
+
`);
|
|
175
|
+
}
|
|
120
176
|
|
|
121
177
|
// src/generators/client-generator.ts
|
|
122
178
|
import { posix } from "node:path";
|
|
@@ -151,7 +207,7 @@ function generateRelationManifest(entities) {
|
|
|
151
207
|
}
|
|
152
208
|
|
|
153
209
|
// src/generators/client-generator.ts
|
|
154
|
-
var
|
|
210
|
+
var FILE_HEADER2 = `// Generated by @vertz/codegen — do not edit
|
|
155
211
|
|
|
156
212
|
`;
|
|
157
213
|
|
|
@@ -166,10 +222,10 @@ class ClientGenerator {
|
|
|
166
222
|
}
|
|
167
223
|
generateClient(ir) {
|
|
168
224
|
const entities = ir.entities ?? [];
|
|
169
|
-
const hasMutations = entities.some((e) => e.operations.some((op) => op.kind === "update" || op.kind === "delete"));
|
|
225
|
+
const hasMutations = entities.some((e) => e.operations.some((op) => op.kind === "create" || op.kind === "update" || op.kind === "delete"));
|
|
170
226
|
const manifest = generateRelationManifest(entities);
|
|
171
227
|
const hasRelations = manifest.some((entry) => Object.keys(entry.schema).length > 0);
|
|
172
|
-
const lines = [
|
|
228
|
+
const lines = [FILE_HEADER2];
|
|
173
229
|
if (entities.length > 0) {
|
|
174
230
|
if (hasMutations) {
|
|
175
231
|
lines.push("import { FetchClient, type OptimisticHandler } from '@vertz/fetch';");
|
|
@@ -224,7 +280,7 @@ class ClientGenerator {
|
|
|
224
280
|
for (const entity of entities) {
|
|
225
281
|
const pascal = toPascalCase(entity.entityName);
|
|
226
282
|
const camel = toCamelCase(entity.entityName);
|
|
227
|
-
const entityHasMutations = entity.operations.some((op) => op.kind === "update" || op.kind === "delete");
|
|
283
|
+
const entityHasMutations = entity.operations.some((op) => op.kind === "create" || op.kind === "update" || op.kind === "delete");
|
|
228
284
|
if (entityHasMutations) {
|
|
229
285
|
lines.push(` ${camel}: create${pascal}Sdk(client, optimistic),`);
|
|
230
286
|
} else {
|
|
@@ -299,7 +355,7 @@ class ClientGenerator {
|
|
|
299
355
|
}
|
|
300
356
|
|
|
301
357
|
// src/generators/entity-schema-generator.ts
|
|
302
|
-
var
|
|
358
|
+
var FILE_HEADER3 = `// Generated by @vertz/codegen — do not edit
|
|
303
359
|
|
|
304
360
|
`;
|
|
305
361
|
var TYPE_MAP = {
|
|
@@ -334,7 +390,7 @@ class EntitySchemaGenerator {
|
|
|
334
390
|
return files;
|
|
335
391
|
}
|
|
336
392
|
generateEntitySchema(entity, schemaOps, schemaActions) {
|
|
337
|
-
const lines = [
|
|
393
|
+
const lines = [FILE_HEADER3];
|
|
338
394
|
lines.push("import { s } from '@vertz/schema';");
|
|
339
395
|
lines.push("");
|
|
340
396
|
for (const op of schemaOps) {
|
|
@@ -374,7 +430,7 @@ class EntitySchemaGenerator {
|
|
|
374
430
|
};
|
|
375
431
|
}
|
|
376
432
|
generateIndex(entities) {
|
|
377
|
-
const lines = [
|
|
433
|
+
const lines = [FILE_HEADER3];
|
|
378
434
|
for (const entity of entities) {
|
|
379
435
|
const exports = [];
|
|
380
436
|
const schemaOps = entity.operations.filter((op) => (op.kind === "create" || op.kind === "update") && op.resolvedFields && op.resolvedFields.length > 0);
|
|
@@ -396,8 +452,50 @@ class EntitySchemaGenerator {
|
|
|
396
452
|
}
|
|
397
453
|
}
|
|
398
454
|
|
|
455
|
+
// src/generators/entity-schema-manifest-generator.ts
|
|
456
|
+
function buildManifestEntry(entity) {
|
|
457
|
+
const fields = (entity.responseFields ?? []).map((f) => f.name);
|
|
458
|
+
const relations = {};
|
|
459
|
+
for (const rel of entity.relations ?? []) {
|
|
460
|
+
const selection = entity.relationSelections?.[rel.name] ?? "all";
|
|
461
|
+
const qc = entity.relationQueryConfig?.[rel.name];
|
|
462
|
+
relations[rel.name] = {
|
|
463
|
+
type: rel.type,
|
|
464
|
+
entity: rel.entity,
|
|
465
|
+
selection,
|
|
466
|
+
allowWhere: qc?.allowWhere ?? [],
|
|
467
|
+
allowOrderBy: qc?.allowOrderBy ?? [],
|
|
468
|
+
...qc?.maxLimit !== undefined ? { maxLimit: qc.maxLimit } : {}
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
return {
|
|
472
|
+
...entity.table !== undefined ? { table: entity.table } : {},
|
|
473
|
+
...entity.primaryKey !== undefined ? { primaryKey: entity.primaryKey } : {},
|
|
474
|
+
tenantScoped: entity.tenantScoped ?? false,
|
|
475
|
+
hiddenFields: entity.hiddenFields ?? [],
|
|
476
|
+
fields,
|
|
477
|
+
relations
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
class EntitySchemaManifestGenerator {
|
|
482
|
+
name = "entity-schema-manifest";
|
|
483
|
+
generate(ir, _config) {
|
|
484
|
+
const manifest = {};
|
|
485
|
+
for (const entity of ir.entities) {
|
|
486
|
+
manifest[entity.entityName] = buildManifestEntry(entity);
|
|
487
|
+
}
|
|
488
|
+
return [
|
|
489
|
+
{
|
|
490
|
+
path: "entity-schema.json",
|
|
491
|
+
content: JSON.stringify(manifest, null, 2)
|
|
492
|
+
}
|
|
493
|
+
];
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
399
497
|
// src/generators/entity-sdk-generator.ts
|
|
400
|
-
var
|
|
498
|
+
var FILE_HEADER4 = `// Generated by @vertz/codegen — do not edit
|
|
401
499
|
|
|
402
500
|
`;
|
|
403
501
|
function toPascalCase2(s) {
|
|
@@ -418,7 +516,7 @@ class EntitySdkGenerator {
|
|
|
418
516
|
}
|
|
419
517
|
generateEntitySdk(entity, _basePath) {
|
|
420
518
|
const pascal = toPascalCase2(entity.entityName);
|
|
421
|
-
const lines = [
|
|
519
|
+
const lines = [FILE_HEADER4];
|
|
422
520
|
const createOpsWithMeta = entity.operations.filter((op) => op.kind === "create" && op.resolvedFields && op.resolvedFields.length > 0);
|
|
423
521
|
const hasSchemaImports = createOpsWithMeta.length > 0;
|
|
424
522
|
if (hasSchemaImports) {
|
|
@@ -431,7 +529,7 @@ class EntitySdkGenerator {
|
|
|
431
529
|
}
|
|
432
530
|
const hasTypes = entity.operations.some((op) => op.outputSchema || op.inputSchema);
|
|
433
531
|
const hasListOp = entity.operations.some((op) => op.kind === "list");
|
|
434
|
-
const hasMutationOp = entity.operations.some((op) => op.kind === "update" || op.kind === "delete");
|
|
532
|
+
const hasMutationOp = entity.operations.some((op) => op.kind === "create" || op.kind === "update" || op.kind === "delete");
|
|
435
533
|
if (hasTypes) {
|
|
436
534
|
const typeImports = new Set;
|
|
437
535
|
for (const op of entity.operations) {
|
|
@@ -447,6 +545,10 @@ class EntitySdkGenerator {
|
|
|
447
545
|
typeImports.add(action.outputSchema);
|
|
448
546
|
}
|
|
449
547
|
lines.push(`import type { ${[...typeImports].join(", ")} } from '../types/${entity.entityName}';`);
|
|
548
|
+
}
|
|
549
|
+
const hasReadOp = entity.operations.some((op) => op.kind === "list" || op.kind === "get");
|
|
550
|
+
const needsFetchImport = hasTypes || hasMutationOp || hasListOp;
|
|
551
|
+
if (needsFetchImport) {
|
|
450
552
|
const fetchImportParts = ["type FetchClient"];
|
|
451
553
|
if (hasListOp)
|
|
452
554
|
fetchImportParts.push("type ListResponse");
|
|
@@ -455,6 +557,8 @@ class EntitySdkGenerator {
|
|
|
455
557
|
fetchImportParts.push("createDescriptor");
|
|
456
558
|
if (hasMutationOp)
|
|
457
559
|
fetchImportParts.push("createMutationDescriptor");
|
|
560
|
+
if (hasReadOp)
|
|
561
|
+
fetchImportParts.push("resolveVertzQL");
|
|
458
562
|
lines.push(`import { ${fetchImportParts.join(", ")} } from '@vertz/fetch';`);
|
|
459
563
|
lines.push("");
|
|
460
564
|
}
|
|
@@ -469,23 +573,32 @@ class EntitySdkGenerator {
|
|
|
469
573
|
const outputType = op.outputSchema ?? "unknown";
|
|
470
574
|
const listOutput = op.kind === "list" ? `ListResponse<${outputType}>` : outputType;
|
|
471
575
|
switch (op.kind) {
|
|
472
|
-
case "list":
|
|
576
|
+
case "list": {
|
|
473
577
|
lines.push(` list: Object.assign(`);
|
|
474
|
-
lines.push(` (query?: Record<string, unknown>) =>
|
|
578
|
+
lines.push(` (query?: Record<string, unknown>) => {`);
|
|
579
|
+
lines.push(` const resolvedQuery = resolveVertzQL(query);`);
|
|
580
|
+
lines.push(` return createDescriptor('GET', '${op.path}', () => client.get<${listOutput}>('${op.path}', { query: resolvedQuery }), resolvedQuery, { entityType: '${entity.entityName}', kind: 'list' as const });`);
|
|
581
|
+
lines.push(` },`);
|
|
475
582
|
lines.push(` { url: '${op.path}', method: 'GET' as const },`);
|
|
476
583
|
lines.push(` ),`);
|
|
477
584
|
break;
|
|
478
|
-
|
|
585
|
+
}
|
|
586
|
+
case "get": {
|
|
587
|
+
const getPathExpr = `\`${op.path.replace(":id", "${id}")}\``;
|
|
479
588
|
lines.push(` get: Object.assign(`);
|
|
480
|
-
lines.push(` (id: string
|
|
589
|
+
lines.push(` (id: string, options?: { select?: Record<string, true> }) => {`);
|
|
590
|
+
lines.push(` const resolvedQuery = resolveVertzQL(options);`);
|
|
591
|
+
lines.push(` return createDescriptor('GET', ${getPathExpr}, () => client.get<${outputType}>(${getPathExpr}, { query: resolvedQuery }), resolvedQuery, { entityType: '${entity.entityName}', kind: 'get' as const, id });`);
|
|
592
|
+
lines.push(` },`);
|
|
481
593
|
lines.push(` { url: '${op.path}', method: 'GET' as const },`);
|
|
482
594
|
lines.push(` ),`);
|
|
483
595
|
break;
|
|
596
|
+
}
|
|
484
597
|
case "create":
|
|
485
598
|
if (op.resolvedFields && op.resolvedFields.length > 0) {
|
|
486
599
|
const schemaVarName = `${(op.inputSchema ?? "createInput").charAt(0).toLowerCase()}${(op.inputSchema ?? "createInput").slice(1)}Schema`;
|
|
487
600
|
lines.push(` create: Object.assign(`);
|
|
488
|
-
lines.push(` (body: ${inputType}) =>
|
|
601
|
+
lines.push(` (body: ${inputType}) => createMutationDescriptor('POST', '${op.path}', () => client.post<${outputType}>('${op.path}', body), { entityType: '${entity.entityName}', kind: 'create' as const, body }, optimistic),`);
|
|
489
602
|
lines.push(` {`);
|
|
490
603
|
lines.push(` url: '${op.path}',`);
|
|
491
604
|
lines.push(` method: 'POST' as const,`);
|
|
@@ -494,7 +607,7 @@ class EntitySdkGenerator {
|
|
|
494
607
|
lines.push(` ),`);
|
|
495
608
|
} else {
|
|
496
609
|
lines.push(` create: Object.assign(`);
|
|
497
|
-
lines.push(` (body: ${inputType}) =>
|
|
610
|
+
lines.push(` (body: ${inputType}) => createMutationDescriptor('POST', '${op.path}', () => client.post<${outputType}>('${op.path}', body), { entityType: '${entity.entityName}', kind: 'create' as const, body }, optimistic),`);
|
|
498
611
|
lines.push(` { url: '${op.path}', method: 'POST' as const },`);
|
|
499
612
|
lines.push(` ),`);
|
|
500
613
|
}
|
|
@@ -543,7 +656,7 @@ class EntitySdkGenerator {
|
|
|
543
656
|
};
|
|
544
657
|
}
|
|
545
658
|
generateIndex(entities) {
|
|
546
|
-
const lines = [
|
|
659
|
+
const lines = [FILE_HEADER4];
|
|
547
660
|
for (const entity of entities) {
|
|
548
661
|
const pascal = toPascalCase2(entity.entityName);
|
|
549
662
|
lines.push(`export { create${pascal}Sdk } from './${entity.entityName}';`);
|
|
@@ -554,7 +667,7 @@ class EntitySdkGenerator {
|
|
|
554
667
|
}
|
|
555
668
|
|
|
556
669
|
// src/generators/entity-types-generator.ts
|
|
557
|
-
var
|
|
670
|
+
var FILE_HEADER5 = `// Generated by @vertz/codegen — do not edit
|
|
558
671
|
|
|
559
672
|
`;
|
|
560
673
|
var TS_TYPE_MAP = {
|
|
@@ -586,7 +699,7 @@ class EntityTypesGenerator {
|
|
|
586
699
|
return files;
|
|
587
700
|
}
|
|
588
701
|
generateEntityTypes(entity) {
|
|
589
|
-
const lines = [
|
|
702
|
+
const lines = [FILE_HEADER5];
|
|
590
703
|
const emitted = new Set;
|
|
591
704
|
for (const op of entity.operations) {
|
|
592
705
|
if (op.inputSchema && !emitted.has(op.inputSchema)) {
|
|
@@ -657,7 +770,7 @@ ${props};
|
|
|
657
770
|
}`;
|
|
658
771
|
}
|
|
659
772
|
generateIndex(entities) {
|
|
660
|
-
const lines = [
|
|
773
|
+
const lines = [FILE_HEADER5];
|
|
661
774
|
for (const entity of entities) {
|
|
662
775
|
lines.push(`export * from './${entity.entityName}';`);
|
|
663
776
|
}
|
|
@@ -666,9 +779,177 @@ ${props};
|
|
|
666
779
|
}
|
|
667
780
|
}
|
|
668
781
|
|
|
782
|
+
// src/generators/rls-policy-generator.ts
|
|
783
|
+
var FILE_HEADER6 = `-- Generated by @vertz/codegen — do not edit
|
|
784
|
+
|
|
785
|
+
`;
|
|
786
|
+
var VALID_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
787
|
+
|
|
788
|
+
class RlsPolicyGenerator {
|
|
789
|
+
name = "rls-policies";
|
|
790
|
+
generate(ir, _config) {
|
|
791
|
+
if (!ir.access || ir.access.whereClauses.length === 0) {
|
|
792
|
+
return [];
|
|
793
|
+
}
|
|
794
|
+
return [
|
|
795
|
+
{
|
|
796
|
+
path: "rls-policies.sql",
|
|
797
|
+
content: renderPolicies(ir.access)
|
|
798
|
+
}
|
|
799
|
+
];
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
function quoteIdentifier(name) {
|
|
803
|
+
if (!VALID_IDENTIFIER.test(name)) {
|
|
804
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
805
|
+
}
|
|
806
|
+
return name;
|
|
807
|
+
}
|
|
808
|
+
function renderCondition(condition) {
|
|
809
|
+
const column = quoteIdentifier(toSnakeCase(condition.column));
|
|
810
|
+
if (condition.kind === "marker") {
|
|
811
|
+
if (condition.marker === "user.id") {
|
|
812
|
+
return `${column} = current_setting('app.user_id')::UUID`;
|
|
813
|
+
}
|
|
814
|
+
return `${column} = current_setting('app.tenant_id')::UUID`;
|
|
815
|
+
}
|
|
816
|
+
if (typeof condition.value === "string") {
|
|
817
|
+
const escaped = condition.value.replace(/'/g, "''");
|
|
818
|
+
return `${column} = '${escaped}'`;
|
|
819
|
+
}
|
|
820
|
+
return `${column} = ${condition.value}`;
|
|
821
|
+
}
|
|
822
|
+
function renderPolicies(access) {
|
|
823
|
+
const lines = [FILE_HEADER6];
|
|
824
|
+
for (const clause of access.whereClauses) {
|
|
825
|
+
const colonIdx = clause.entitlement.indexOf(":");
|
|
826
|
+
const entity = colonIdx >= 0 ? clause.entitlement.slice(0, colonIdx) : clause.entitlement;
|
|
827
|
+
const action = colonIdx >= 0 ? clause.entitlement.slice(colonIdx + 1) : "";
|
|
828
|
+
const tableName = quoteIdentifier(`${toSnakeCase(entity)}s`);
|
|
829
|
+
const policyName = quoteIdentifier(`${toSnakeCase(entity)}_${toSnakeCase(action)}`.replace(/^_+|_+$/g, ""));
|
|
830
|
+
lines.push(`-- Entitlement: ${clause.entitlement}`);
|
|
831
|
+
const using = clause.conditions.map(renderCondition).join(" AND ");
|
|
832
|
+
lines.push(`CREATE POLICY ${policyName} ON ${tableName} FOR ALL`);
|
|
833
|
+
lines.push(` USING (${using});`);
|
|
834
|
+
lines.push("");
|
|
835
|
+
}
|
|
836
|
+
return lines.join(`
|
|
837
|
+
`);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// src/generators/router-augmentation-generator.ts
|
|
841
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
842
|
+
import { dirname as dirname2, extname, join as join2, relative, resolve as resolve2, sep } from "node:path";
|
|
843
|
+
var FILE_HEADER7 = `// Generated by @vertz/codegen — do not edit
|
|
844
|
+
|
|
845
|
+
`;
|
|
846
|
+
var CANDIDATE_ROUTE_FILES = [
|
|
847
|
+
["src", "router.ts"],
|
|
848
|
+
["src", "router.tsx"],
|
|
849
|
+
["src", "ui", "router.ts"],
|
|
850
|
+
["src", "ui", "router.tsx"]
|
|
851
|
+
];
|
|
852
|
+
var SOURCE_EXTENSIONS = new Set([".ts", ".tsx"]);
|
|
853
|
+
var EXPORTED_ROUTES_PATTERN = /export\s+const\s+routes\b/m;
|
|
854
|
+
var REEXPORTED_ROUTES_PATTERN = /const\s+routes\b[\s\S]*?export\s*\{\s*routes\b/m;
|
|
855
|
+
|
|
856
|
+
class RouterAugmentationGenerator {
|
|
857
|
+
name = "router-augmentation";
|
|
858
|
+
generate(_ir, config) {
|
|
859
|
+
const projectRoot = findProjectRoot(config.outputDir);
|
|
860
|
+
if (!projectRoot)
|
|
861
|
+
return [];
|
|
862
|
+
const routeModulePath = findRouteModule(projectRoot);
|
|
863
|
+
if (!routeModulePath)
|
|
864
|
+
return [];
|
|
865
|
+
return [
|
|
866
|
+
{
|
|
867
|
+
path: "router.d.ts",
|
|
868
|
+
content: renderRouterAugmentation(routeModulePath, config.outputDir)
|
|
869
|
+
}
|
|
870
|
+
];
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
function findProjectRoot(outputDir) {
|
|
874
|
+
let current = resolve2(outputDir);
|
|
875
|
+
while (true) {
|
|
876
|
+
if (existsSync(join2(current, "package.json"))) {
|
|
877
|
+
return current;
|
|
878
|
+
}
|
|
879
|
+
const parent = dirname2(current);
|
|
880
|
+
if (parent === current) {
|
|
881
|
+
return null;
|
|
882
|
+
}
|
|
883
|
+
current = parent;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
function findRouteModule(projectRoot) {
|
|
887
|
+
for (const candidate of CANDIDATE_ROUTE_FILES) {
|
|
888
|
+
const candidatePath = join2(projectRoot, ...candidate);
|
|
889
|
+
if (existsSync(candidatePath) && fileExportsRoutes(candidatePath)) {
|
|
890
|
+
return candidatePath;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
return scanForRouteModule(join2(projectRoot, "src"));
|
|
894
|
+
}
|
|
895
|
+
function scanForRouteModule(dir) {
|
|
896
|
+
if (!existsSync(dir))
|
|
897
|
+
return null;
|
|
898
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
899
|
+
const fullPath = join2(dir, entry.name);
|
|
900
|
+
if (entry.isDirectory()) {
|
|
901
|
+
const nested = scanForRouteModule(fullPath);
|
|
902
|
+
if (nested) {
|
|
903
|
+
return nested;
|
|
904
|
+
}
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
if (!entry.isFile())
|
|
908
|
+
continue;
|
|
909
|
+
const extension = extname(entry.name);
|
|
910
|
+
if (extension === ".d.ts" || !SOURCE_EXTENSIONS.has(extension))
|
|
911
|
+
continue;
|
|
912
|
+
if (fileExportsRoutes(fullPath)) {
|
|
913
|
+
return fullPath;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return null;
|
|
917
|
+
}
|
|
918
|
+
function fileExportsRoutes(filePath) {
|
|
919
|
+
const source = readFileSync(filePath, "utf-8");
|
|
920
|
+
if (!source.includes("defineRoutes("))
|
|
921
|
+
return false;
|
|
922
|
+
return EXPORTED_ROUTES_PATTERN.test(source) || REEXPORTED_ROUTES_PATTERN.test(source);
|
|
923
|
+
}
|
|
924
|
+
function renderRouterAugmentation(routeModulePath, outputDir) {
|
|
925
|
+
const importPath = toImportPath(relative(resolve2(outputDir), routeModulePath));
|
|
926
|
+
return [
|
|
927
|
+
FILE_HEADER7,
|
|
928
|
+
"import type { InferRouteMap, TypedRouter, UnwrapSignals } from '@vertz/ui';",
|
|
929
|
+
`import type { routes } from '${importPath}';`,
|
|
930
|
+
"",
|
|
931
|
+
"type AppRouteMap = InferRouteMap<typeof routes>;",
|
|
932
|
+
"",
|
|
933
|
+
"declare module '@vertz/ui' {",
|
|
934
|
+
" export function useRouter(): UnwrapSignals<TypedRouter<AppRouteMap>>;",
|
|
935
|
+
"}",
|
|
936
|
+
"",
|
|
937
|
+
"declare module '@vertz/ui/router' {",
|
|
938
|
+
" export function useRouter(): UnwrapSignals<TypedRouter<AppRouteMap>>;",
|
|
939
|
+
"}",
|
|
940
|
+
""
|
|
941
|
+
].join(`
|
|
942
|
+
`);
|
|
943
|
+
}
|
|
944
|
+
function toImportPath(pathToModule) {
|
|
945
|
+
const normalized = pathToModule.split(sep).join("/");
|
|
946
|
+
const withoutExtension = normalized.replace(/\.[^.]+$/, "");
|
|
947
|
+
return withoutExtension.startsWith(".") ? withoutExtension : `./${withoutExtension}`;
|
|
948
|
+
}
|
|
949
|
+
|
|
669
950
|
// src/incremental.ts
|
|
670
951
|
import { mkdir as mkdir2, readdir, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
|
|
671
|
-
import { dirname as
|
|
952
|
+
import { dirname as dirname3, join as join3, relative as relative2, resolve as resolve3 } from "node:path";
|
|
672
953
|
|
|
673
954
|
// src/hasher.ts
|
|
674
955
|
import { createHash } from "node:crypto";
|
|
@@ -686,11 +967,11 @@ async function collectFiles(dir, baseDir) {
|
|
|
686
967
|
return results;
|
|
687
968
|
}
|
|
688
969
|
for (const entry of entries) {
|
|
689
|
-
const fullPath =
|
|
970
|
+
const fullPath = join3(dir, entry.name);
|
|
690
971
|
if (entry.isDirectory()) {
|
|
691
972
|
results.push(...await collectFiles(fullPath, baseDir));
|
|
692
973
|
} else if (entry.isFile()) {
|
|
693
|
-
results.push(
|
|
974
|
+
results.push(relative2(baseDir, fullPath));
|
|
694
975
|
}
|
|
695
976
|
}
|
|
696
977
|
return results;
|
|
@@ -702,12 +983,12 @@ async function writeIncremental(files, outputDir, options) {
|
|
|
702
983
|
await mkdir2(outputDir, { recursive: true });
|
|
703
984
|
const generatedPaths = new Set(files.map((f) => f.path));
|
|
704
985
|
for (const file of files) {
|
|
705
|
-
const filePath =
|
|
706
|
-
const resolvedPath =
|
|
707
|
-
if (!resolvedPath.startsWith(
|
|
986
|
+
const filePath = join3(outputDir, file.path);
|
|
987
|
+
const resolvedPath = resolve3(filePath);
|
|
988
|
+
if (!resolvedPath.startsWith(resolve3(outputDir))) {
|
|
708
989
|
throw new Error(`Generated file path "${file.path}" escapes output directory`);
|
|
709
990
|
}
|
|
710
|
-
const dir =
|
|
991
|
+
const dir = dirname3(filePath);
|
|
711
992
|
await mkdir2(dir, { recursive: true });
|
|
712
993
|
let existingContent;
|
|
713
994
|
try {
|
|
@@ -724,7 +1005,7 @@ async function writeIncremental(files, outputDir, options) {
|
|
|
724
1005
|
const existingFiles = await collectFiles(outputDir, outputDir);
|
|
725
1006
|
for (const existing of existingFiles) {
|
|
726
1007
|
if (!generatedPaths.has(existing)) {
|
|
727
|
-
await rm2(
|
|
1008
|
+
await rm2(join3(outputDir, existing), { force: true });
|
|
728
1009
|
removed.push(existing);
|
|
729
1010
|
}
|
|
730
1011
|
}
|
|
@@ -834,20 +1115,57 @@ function adaptIR(appIR) {
|
|
|
834
1115
|
};
|
|
835
1116
|
});
|
|
836
1117
|
const resolvedRelations = entity.relations.filter((r) => !!r.type && !!r.entity).map((r) => ({ name: r.name, type: r.type, entity: r.entity }));
|
|
1118
|
+
const relationSelections = {};
|
|
1119
|
+
const relationQueryConfig = {};
|
|
1120
|
+
for (const rel of entity.relations) {
|
|
1121
|
+
relationSelections[rel.name] = rel.selection;
|
|
1122
|
+
if (rel.allowWhere || rel.allowOrderBy || rel.maxLimit !== undefined) {
|
|
1123
|
+
relationQueryConfig[rel.name] = {
|
|
1124
|
+
...rel.allowWhere ? { allowWhere: rel.allowWhere } : {},
|
|
1125
|
+
...rel.allowOrderBy ? { allowOrderBy: rel.allowOrderBy } : {},
|
|
1126
|
+
...rel.maxLimit !== undefined ? { maxLimit: rel.maxLimit } : {}
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
let entityResponseFields;
|
|
1131
|
+
const respRef = entity.modelRef.schemaRefs.response;
|
|
1132
|
+
if (respRef?.kind === "inline") {
|
|
1133
|
+
entityResponseFields = respRef.resolvedFields?.map((f) => ({
|
|
1134
|
+
name: f.name,
|
|
1135
|
+
tsType: f.tsType,
|
|
1136
|
+
optional: f.optional
|
|
1137
|
+
}));
|
|
1138
|
+
}
|
|
837
1139
|
return {
|
|
838
1140
|
entityName: entity.name,
|
|
839
1141
|
operations,
|
|
840
1142
|
actions,
|
|
841
|
-
relations: resolvedRelations.length > 0 ? resolvedRelations : undefined
|
|
1143
|
+
relations: resolvedRelations.length > 0 ? resolvedRelations : undefined,
|
|
1144
|
+
tenantScoped: entity.tenantScoped,
|
|
1145
|
+
table: entity.table,
|
|
1146
|
+
primaryKey: entity.modelRef.primaryKey,
|
|
1147
|
+
hiddenFields: entity.modelRef.hiddenFields,
|
|
1148
|
+
responseFields: entityResponseFields,
|
|
1149
|
+
relationSelections: Object.keys(relationSelections).length > 0 ? relationSelections : undefined,
|
|
1150
|
+
relationQueryConfig: Object.keys(relationQueryConfig).length > 0 ? relationQueryConfig : undefined
|
|
842
1151
|
};
|
|
843
1152
|
});
|
|
1153
|
+
const access = appIR.access ? {
|
|
1154
|
+
entities: appIR.access.entities.map((e) => ({ name: e.name, roles: e.roles })),
|
|
1155
|
+
entitlements: appIR.access.entitlements,
|
|
1156
|
+
whereClauses: (appIR.access.whereClauses ?? []).map((wc) => ({
|
|
1157
|
+
entitlement: wc.entitlement,
|
|
1158
|
+
conditions: wc.conditions.map((c) => ({ ...c }))
|
|
1159
|
+
}))
|
|
1160
|
+
} : undefined;
|
|
844
1161
|
return {
|
|
845
1162
|
basePath: appIR.app.basePath,
|
|
846
1163
|
version: appIR.app.version,
|
|
847
1164
|
modules: [],
|
|
848
1165
|
schemas: allSchemas,
|
|
849
1166
|
entities,
|
|
850
|
-
auth: { schemes: [] }
|
|
1167
|
+
auth: { schemes: [] },
|
|
1168
|
+
access
|
|
851
1169
|
};
|
|
852
1170
|
}
|
|
853
1171
|
|
|
@@ -863,6 +1181,16 @@ function runTypescriptGenerator(ir, _config) {
|
|
|
863
1181
|
files.push(...entitySdkGen.generate(ir, generatorConfig));
|
|
864
1182
|
const clientGen = new ClientGenerator;
|
|
865
1183
|
files.push(...clientGen.generate(ir, generatorConfig));
|
|
1184
|
+
const routerAugmentationGen = new RouterAugmentationGenerator;
|
|
1185
|
+
files.push(...routerAugmentationGen.generate(ir, generatorConfig));
|
|
1186
|
+
const accessTypesGen = new AccessTypesGenerator;
|
|
1187
|
+
files.push(...accessTypesGen.generate(ir, generatorConfig));
|
|
1188
|
+
const entitySchemaManifestGen = new EntitySchemaManifestGenerator;
|
|
1189
|
+
files.push(...entitySchemaManifestGen.generate(ir, generatorConfig));
|
|
1190
|
+
if (_config.typescript?.rls) {
|
|
1191
|
+
const rlsPolicyGen = new RlsPolicyGenerator;
|
|
1192
|
+
files.push(...rlsPolicyGen.generate(ir, generatorConfig));
|
|
1193
|
+
}
|
|
866
1194
|
return files;
|
|
867
1195
|
}
|
|
868
1196
|
function generateSync(ir, config) {
|
|
@@ -889,10 +1217,10 @@ async function mergeImportsToPackageJson(files, outputDir) {
|
|
|
889
1217
|
const imports = generated.imports;
|
|
890
1218
|
if (!imports || Object.keys(imports).length === 0)
|
|
891
1219
|
return false;
|
|
892
|
-
const projectRoot = await
|
|
1220
|
+
const projectRoot = await findProjectRoot2(resolve4(outputDir));
|
|
893
1221
|
if (!projectRoot)
|
|
894
1222
|
return false;
|
|
895
|
-
const pkgPath =
|
|
1223
|
+
const pkgPath = join4(projectRoot, "package.json");
|
|
896
1224
|
const raw = await readFile3(pkgPath, "utf-8");
|
|
897
1225
|
const pkg = JSON.parse(raw);
|
|
898
1226
|
const existing = pkg.imports;
|
|
@@ -904,15 +1232,15 @@ async function mergeImportsToPackageJson(files, outputDir) {
|
|
|
904
1232
|
`, "utf-8");
|
|
905
1233
|
return true;
|
|
906
1234
|
}
|
|
907
|
-
async function
|
|
1235
|
+
async function findProjectRoot2(startDir) {
|
|
908
1236
|
let dir = startDir;
|
|
909
|
-
const root =
|
|
1237
|
+
const root = dirname4(dir);
|
|
910
1238
|
while (dir !== root) {
|
|
911
1239
|
try {
|
|
912
|
-
await readFile3(
|
|
1240
|
+
await readFile3(join4(dir, "package.json"), "utf-8");
|
|
913
1241
|
return dir;
|
|
914
1242
|
} catch {
|
|
915
|
-
dir =
|
|
1243
|
+
dir = dirname4(dir);
|
|
916
1244
|
}
|
|
917
1245
|
}
|
|
918
1246
|
return null;
|
|
@@ -932,12 +1260,12 @@ async function generate(appIR, config) {
|
|
|
932
1260
|
} else {
|
|
933
1261
|
await mkdir3(config.outputDir, { recursive: true });
|
|
934
1262
|
for (const file of files) {
|
|
935
|
-
const filePath =
|
|
936
|
-
const resolvedPath =
|
|
937
|
-
if (!resolvedPath.startsWith(
|
|
1263
|
+
const filePath = join4(config.outputDir, file.path);
|
|
1264
|
+
const resolvedPath = resolve4(filePath);
|
|
1265
|
+
if (!resolvedPath.startsWith(resolve4(config.outputDir))) {
|
|
938
1266
|
throw new Error(`Generated file path "${file.path}" escapes output directory`);
|
|
939
1267
|
}
|
|
940
|
-
const dir =
|
|
1268
|
+
const dir = dirname4(filePath);
|
|
941
1269
|
await mkdir3(dir, { recursive: true });
|
|
942
1270
|
await writeFile3(filePath, file.content, "utf-8");
|
|
943
1271
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vertz/codegen",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Vertz code generation — internal, no stability guarantee",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"typecheck": "tsc --noEmit -p tsconfig.typecheck.json"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@vertz/compiler": "^0.2.
|
|
30
|
+
"@vertz/compiler": "^0.2.15"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node": "^25.3.1",
|