@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 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 dirname3, join as join3, resolve as resolve3 } from "node:path";
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 FILE_HEADER = `// Generated by @vertz/codegen — do not edit
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 = [FILE_HEADER];
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 FILE_HEADER2 = `// Generated by @vertz/codegen — do not edit
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 = [FILE_HEADER2];
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 = [FILE_HEADER2];
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 FILE_HEADER3 = `// Generated by @vertz/codegen — do not edit
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 = [FILE_HEADER3];
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>) => createDescriptor('GET', '${op.path}', () => client.get<${listOutput}>('${op.path}', { query }), query, { entityType: '${entity.entityName}', kind: 'list' as const }),`);
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
- case "get":
585
+ }
586
+ case "get": {
587
+ const getPathExpr = `\`${op.path.replace(":id", "${id}")}\``;
479
588
  lines.push(` get: Object.assign(`);
480
- lines.push(` (id: string) => createDescriptor('GET', \`${op.path.replace(":id", "${id}")}\`, () => client.get<${outputType}>(\`${op.path.replace(":id", "${id}")}\`), undefined, { entityType: '${entity.entityName}', kind: 'get' as const, id }),`);
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}) => createDescriptor('POST', '${op.path}', () => client.post<${outputType}>('${op.path}', body)),`);
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}) => createDescriptor('POST', '${op.path}', () => client.post<${outputType}>('${op.path}', body)),`);
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 = [FILE_HEADER3];
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 FILE_HEADER4 = `// Generated by @vertz/codegen — do not edit
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 = [FILE_HEADER4];
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 = [FILE_HEADER4];
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 dirname2, join as join2, relative, resolve as resolve2 } from "node:path";
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 = join2(dir, entry.name);
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(relative(baseDir, fullPath));
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 = join2(outputDir, file.path);
706
- const resolvedPath = resolve2(filePath);
707
- if (!resolvedPath.startsWith(resolve2(outputDir))) {
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 = dirname2(filePath);
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(join2(outputDir, existing), { force: true });
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 findProjectRoot(resolve3(outputDir));
1220
+ const projectRoot = await findProjectRoot2(resolve4(outputDir));
893
1221
  if (!projectRoot)
894
1222
  return false;
895
- const pkgPath = join3(projectRoot, "package.json");
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 findProjectRoot(startDir) {
1235
+ async function findProjectRoot2(startDir) {
908
1236
  let dir = startDir;
909
- const root = dirname3(dir);
1237
+ const root = dirname4(dir);
910
1238
  while (dir !== root) {
911
1239
  try {
912
- await readFile3(join3(dir, "package.json"), "utf-8");
1240
+ await readFile3(join4(dir, "package.json"), "utf-8");
913
1241
  return dir;
914
1242
  } catch {
915
- dir = dirname3(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 = join3(config.outputDir, file.path);
936
- const resolvedPath = resolve3(filePath);
937
- if (!resolvedPath.startsWith(resolve3(config.outputDir))) {
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 = dirname3(filePath);
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.14",
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.13"
30
+ "@vertz/compiler": "^0.2.15"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^25.3.1",