@vertz/codegen 0.2.15 → 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 +240 -24
- 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
|
@@ -118,6 +118,62 @@ async function formatWithBiome(files) {
|
|
|
118
118
|
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
119
119
|
import { dirname as dirname4, join as join4, resolve as resolve4 } from "node:path";
|
|
120
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
|
+
}
|
|
176
|
+
|
|
121
177
|
// src/generators/client-generator.ts
|
|
122
178
|
import { posix } from "node:path";
|
|
123
179
|
|
|
@@ -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,10 +779,68 @@ ${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
|
+
|
|
669
840
|
// src/generators/router-augmentation-generator.ts
|
|
670
841
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
671
842
|
import { dirname as dirname2, extname, join as join2, relative, resolve as resolve2, sep } from "node:path";
|
|
672
|
-
var
|
|
843
|
+
var FILE_HEADER7 = `// Generated by @vertz/codegen — do not edit
|
|
673
844
|
|
|
674
845
|
`;
|
|
675
846
|
var CANDIDATE_ROUTE_FILES = [
|
|
@@ -753,7 +924,7 @@ function fileExportsRoutes(filePath) {
|
|
|
753
924
|
function renderRouterAugmentation(routeModulePath, outputDir) {
|
|
754
925
|
const importPath = toImportPath(relative(resolve2(outputDir), routeModulePath));
|
|
755
926
|
return [
|
|
756
|
-
|
|
927
|
+
FILE_HEADER7,
|
|
757
928
|
"import type { InferRouteMap, TypedRouter, UnwrapSignals } from '@vertz/ui';",
|
|
758
929
|
`import type { routes } from '${importPath}';`,
|
|
759
930
|
"",
|
|
@@ -944,20 +1115,57 @@ function adaptIR(appIR) {
|
|
|
944
1115
|
};
|
|
945
1116
|
});
|
|
946
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
|
+
}
|
|
947
1139
|
return {
|
|
948
1140
|
entityName: entity.name,
|
|
949
1141
|
operations,
|
|
950
1142
|
actions,
|
|
951
|
-
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
|
|
952
1151
|
};
|
|
953
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;
|
|
954
1161
|
return {
|
|
955
1162
|
basePath: appIR.app.basePath,
|
|
956
1163
|
version: appIR.app.version,
|
|
957
1164
|
modules: [],
|
|
958
1165
|
schemas: allSchemas,
|
|
959
1166
|
entities,
|
|
960
|
-
auth: { schemes: [] }
|
|
1167
|
+
auth: { schemes: [] },
|
|
1168
|
+
access
|
|
961
1169
|
};
|
|
962
1170
|
}
|
|
963
1171
|
|
|
@@ -975,6 +1183,14 @@ function runTypescriptGenerator(ir, _config) {
|
|
|
975
1183
|
files.push(...clientGen.generate(ir, generatorConfig));
|
|
976
1184
|
const routerAugmentationGen = new RouterAugmentationGenerator;
|
|
977
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
|
+
}
|
|
978
1194
|
return files;
|
|
979
1195
|
}
|
|
980
1196
|
function generateSync(ir, config) {
|
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",
|