node-type-registry 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -31,7 +31,6 @@ import type {
31
31
  const definition: BlueprintDefinition = {
32
32
  tables: [
33
33
  {
34
- ref: 'tasks',
35
34
  table_name: 'tasks',
36
35
  nodes: [
37
36
  'DataId',
@@ -48,11 +47,17 @@ const definition: BlueprintDefinition = {
48
47
  relations: [
49
48
  {
50
49
  $type: 'RelationBelongsTo',
51
- source_ref: 'tasks',
52
- target_ref: 'projects',
50
+ source_table: 'tasks',
51
+ target_table: 'projects',
53
52
  delete_action: 'c',
54
53
  },
55
54
  ],
55
+ unique_constraints: [
56
+ {
57
+ table_name: 'tasks',
58
+ columns: ['title', 'owner_id'],
59
+ },
60
+ ],
56
61
  };
57
62
  ```
58
63
 
@@ -61,7 +66,7 @@ const definition: BlueprintDefinition = {
61
66
  When node type definitions are added or modified, regenerate with:
62
67
 
63
68
  ```bash
64
- cd graphile/node-type-registry && pnpm generate:types
69
+ cd graphql/node-type-registry && pnpm generate:types
65
70
  ```
66
71
 
67
72
  This produces `src/blueprint-types.generated.ts` from the TS node type source of truth.
@@ -71,17 +76,7 @@ This produces `src/blueprint-types.generated.ts` from the TS node type source of
71
76
  Generate SQL seed scripts for `node_type_registry` table:
72
77
 
73
78
  ```bash
74
- cd graphile/node-type-registry && pnpm generate:seed --pgpm ../../constructive-db/packages/metaschema
75
- ```
76
-
77
- ## Preset (deprecated)
78
-
79
- > **Note:** The `NodeTypeRegistryPreset` is no longer the recommended approach.
80
- > Use the generated TypeScript types instead (see above). The preset remains
81
- > available for backward compatibility but will be removed in a future version.
82
-
83
- ```typescript
84
- import { NodeTypeRegistryPreset } from 'node-type-registry/preset';
79
+ cd graphql/node-type-registry && pnpm generate:seed --pgpm ../../constructive-db/packages/metaschema
85
80
  ```
86
81
 
87
82
  ---
@@ -444,18 +444,18 @@ export interface BlueprintField {
444
444
  /** Comment/description for this field. */
445
445
  description?: string;
446
446
  }
447
- /** An RLS policy entry for a blueprint table. */
447
+ /** An RLS policy entry for a blueprint table. Uses $type to match the blueprint JSON convention. */
448
448
  export interface BlueprintPolicy {
449
449
  /** Authz* policy type name (e.g., "AuthzDirectOwner", "AuthzAllowAll"). */
450
- policy_type: "AuthzDirectOwner" | "AuthzDirectOwnerAny" | "AuthzMembership" | "AuthzEntityMembership" | "AuthzRelatedEntityMembership" | "AuthzOrgHierarchy" | "AuthzTemporal" | "AuthzPublishable" | "AuthzMemberList" | "AuthzRelatedMemberList" | "AuthzAllowAll" | "AuthzDenyAll" | "AuthzComposite" | "AuthzPeerOwnership" | "AuthzRelatedPeerOwnership";
450
+ $type: "AuthzDirectOwner" | "AuthzDirectOwnerAny" | "AuthzMembership" | "AuthzEntityMembership" | "AuthzRelatedEntityMembership" | "AuthzOrgHierarchy" | "AuthzTemporal" | "AuthzPublishable" | "AuthzMemberList" | "AuthzRelatedMemberList" | "AuthzAllowAll" | "AuthzDenyAll" | "AuthzComposite" | "AuthzPeerOwnership" | "AuthzRelatedPeerOwnership";
451
+ /** Privileges this policy applies to (e.g., ["select"], ["insert", "update", "delete"]). */
452
+ privileges?: string[];
453
+ /** Whether this policy is permissive (true) or restrictive (false). Defaults to true. */
454
+ permissive?: boolean;
451
455
  /** Role for this policy. Defaults to "authenticated". */
452
456
  policy_role?: string;
453
- /** Whether this policy is permissive (true) or restrictive (false). */
454
- permissive?: boolean;
455
457
  /** Optional custom name for this policy. */
456
458
  policy_name?: string;
457
- /** Privileges this policy applies to. */
458
- privileges?: string[];
459
459
  /** Policy-specific data (structure varies by policy type). */
460
460
  data?: Record<string, unknown>;
461
461
  }
@@ -468,19 +468,49 @@ export interface BlueprintFtsSource {
468
468
  /** Language for text search. Defaults to "english". */
469
469
  lang?: string;
470
470
  }
471
- /** A full-text search configuration for a blueprint table. */
471
+ /** A full-text search configuration for a blueprint table (top-level, requires table_name). */
472
472
  export interface BlueprintFullTextSearch {
473
- /** Reference key of the table this full-text search belongs to. */
474
- table_ref: string;
473
+ /** Table name this full-text search belongs to. */
474
+ table_name: string;
475
+ /** Optional schema name for disambiguation (falls back to top-level default). */
476
+ schema_name?: string;
477
+ /** Name of the tsvector field on the table. */
478
+ field: string;
479
+ /** Source fields that feed into this tsvector. */
480
+ sources: BlueprintFtsSource[];
481
+ }
482
+ /** A full-text search configuration nested inside a table definition (table_name not required). */
483
+ export interface BlueprintTableFullTextSearch {
475
484
  /** Name of the tsvector field on the table. */
476
485
  field: string;
477
486
  /** Source fields that feed into this tsvector. */
478
487
  sources: BlueprintFtsSource[];
488
+ /** Optional schema name override. */
489
+ schema_name?: string;
479
490
  }
480
- /** An index definition within a blueprint. */
491
+ /** An index definition within a blueprint (top-level, requires table_name). */
481
492
  export interface BlueprintIndex {
482
- /** Reference key of the table this index belongs to. */
483
- table_ref: string;
493
+ /** Table name this index belongs to. */
494
+ table_name: string;
495
+ /** Optional schema name for disambiguation (falls back to top-level default). */
496
+ schema_name?: string;
497
+ /** Single column name for the index. */
498
+ column?: string;
499
+ /** Array of column names for a multi-column index. */
500
+ columns?: string[];
501
+ /** Index access method (e.g., "BTREE", "GIN", "GIST", "HNSW", "BM25"). */
502
+ access_method: string;
503
+ /** Whether this is a unique index. */
504
+ is_unique?: boolean;
505
+ /** Optional custom name for the index. */
506
+ name?: string;
507
+ /** Operator classes for the index columns. */
508
+ op_classes?: string[];
509
+ /** Additional index-specific options. */
510
+ options?: Record<string, unknown>;
511
+ }
512
+ /** An index definition nested inside a table definition (table_name not required). */
513
+ export interface BlueprintTableIndex {
484
514
  /** Single column name for the index. */
485
515
  column?: string;
486
516
  /** Array of column names for a multi-column index. */
@@ -495,6 +525,24 @@ export interface BlueprintIndex {
495
525
  op_classes?: string[];
496
526
  /** Additional index-specific options. */
497
527
  options?: Record<string, unknown>;
528
+ /** Optional schema name override. */
529
+ schema_name?: string;
530
+ }
531
+ /** A unique constraint definition within a blueprint (top-level, requires table_name). */
532
+ export interface BlueprintUniqueConstraint {
533
+ /** Table name this unique constraint belongs to. */
534
+ table_name: string;
535
+ /** Optional schema name for disambiguation (falls back to top-level default). */
536
+ schema_name?: string;
537
+ /** Column names that form the unique constraint. */
538
+ columns: string[];
539
+ }
540
+ /** A unique constraint nested inside a table definition (table_name not required). */
541
+ export interface BlueprintTableUniqueConstraint {
542
+ /** Column names that form the unique constraint. */
543
+ columns: string[];
544
+ /** Optional schema name override. */
545
+ schema_name?: string;
498
546
  }
499
547
  /** String shorthand -- just the node type name. */
500
548
  export type BlueprintNodeShorthand = "AuthzDirectOwner" | "AuthzDirectOwnerAny" | "AuthzMembership" | "AuthzEntityMembership" | "AuthzRelatedEntityMembership" | "AuthzOrgHierarchy" | "AuthzTemporal" | "AuthzPublishable" | "AuthzMemberList" | "AuthzRelatedMemberList" | "AuthzAllowAll" | "AuthzDenyAll" | "AuthzComposite" | "AuthzPeerOwnership" | "AuthzRelatedPeerOwnership" | "DataId" | "DataDirectOwner" | "DataEntityMembership" | "DataOwnershipInEntity" | "DataTimestamps" | "DataPeoplestamps" | "DataPublishable" | "DataSoftDelete" | "DataEmbedding" | "DataFullTextSearch" | "DataBm25" | "DataSearch" | "DataPostGIS" | "DataPostGISAggregate" | "DataJobTrigger" | "DataTags" | "DataStatusField" | "DataJsonb" | "DataTrgm" | "DataSlug" | "DataInflection" | "DataOwnedFields" | "DataInheritFromParent" | "DataForceCurrentUser" | "DataImmutableFields" | "TableUserProfiles" | "TableOrganizationSettings" | "TableUserSettings";
@@ -634,27 +682,35 @@ export type BlueprintNode = BlueprintNodeShorthand | BlueprintNodeObject;
634
682
  /** A relation entry in a blueprint definition. */
635
683
  export type BlueprintRelation = {
636
684
  $type: "RelationBelongsTo";
637
- source_ref: string;
638
- target_ref: string;
685
+ source_table: string;
686
+ target_table: string;
687
+ source_schema_name?: string;
688
+ target_schema_name?: string;
639
689
  } & Partial<RelationBelongsToParams> | {
640
690
  $type: "RelationHasOne";
641
- source_ref: string;
642
- target_ref: string;
691
+ source_table: string;
692
+ target_table: string;
693
+ source_schema_name?: string;
694
+ target_schema_name?: string;
643
695
  } & Partial<RelationHasOneParams> | {
644
696
  $type: "RelationHasMany";
645
- source_ref: string;
646
- target_ref: string;
697
+ source_table: string;
698
+ target_table: string;
699
+ source_schema_name?: string;
700
+ target_schema_name?: string;
647
701
  } & Partial<RelationHasManyParams> | {
648
702
  $type: "RelationManyToMany";
649
- source_ref: string;
650
- target_ref: string;
703
+ source_table: string;
704
+ target_table: string;
705
+ source_schema_name?: string;
706
+ target_schema_name?: string;
651
707
  } & Partial<RelationManyToManyParams>;
652
708
  /** A table definition within a blueprint. */
653
709
  export interface BlueprintTable {
654
- /** Local reference key for this table (used by relations, indexes, fts). */
655
- ref: string;
656
710
  /** The PostgreSQL table name to create. */
657
711
  table_name: string;
712
+ /** Optional schema name (falls back to top-level default). */
713
+ schema_name?: string;
658
714
  /** Array of node type entries that define the table's behavior. */
659
715
  nodes: BlueprintNode[];
660
716
  /** Custom fields (columns) to add to the table. */
@@ -667,6 +723,12 @@ export interface BlueprintTable {
667
723
  grants?: unknown[];
668
724
  /** Whether to enable RLS on this table. Defaults to true. */
669
725
  use_rls?: boolean;
726
+ /** Table-level indexes (table_name inherited from parent). */
727
+ indexes?: BlueprintTableIndex[];
728
+ /** Table-level full-text search configurations (table_name inherited from parent). */
729
+ full_text_searches?: BlueprintTableFullTextSearch[];
730
+ /** Table-level unique constraints (table_name inherited from parent). */
731
+ unique_constraints?: BlueprintTableUniqueConstraint[];
670
732
  }
671
733
  /** The complete blueprint definition -- the JSONB shape accepted by construct_blueprint(). */
672
734
  export interface BlueprintDefinition {
@@ -678,4 +740,6 @@ export interface BlueprintDefinition {
678
740
  indexes?: BlueprintIndex[];
679
741
  /** Full-text search configurations. */
680
742
  full_text_searches?: BlueprintFullTextSearch[];
743
+ /** Unique constraints on table columns. */
744
+ unique_constraints?: BlueprintUniqueConstraint[];
681
745
  }
@@ -2,7 +2,7 @@
2
2
  // GENERATED FILE — DO NOT EDIT
3
3
  //
4
4
  // Regenerate with:
5
- // cd graphile/node-type-registry && pnpm generate:types
5
+ // cd graphql/node-type-registry && pnpm generate:types
6
6
  //
7
7
  // These types match the JSONB shape expected by construct_blueprint().
8
8
  // All field names are snake_case to match the SQL convention.
@@ -128,7 +128,7 @@ const GENERATED_HEADER = [
128
128
  '-- GENERATED FILE — DO NOT EDIT',
129
129
  '--',
130
130
  '-- Regenerate with:',
131
- '-- cd graphile/node-type-registry && pnpm generate:seed --pgpm ../../constructive-db/packages/metaschema',
131
+ '-- cd graphql/node-type-registry && pnpm generate:seed --pgpm ../../constructive-db/packages/metaschema',
132
132
  '--',
133
133
  '',
134
134
  ].join('\n');
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * - Per-node-type parameter interfaces (via schema-typescript)
8
8
  * - BlueprintNode -- discriminated union of all non-relation node types
9
- * - BlueprintRelation -- typed relation entries with $type, source_ref, target_ref
9
+ * - BlueprintRelation -- typed relation entries with $type, source_table, target_table
10
10
  * - BlueprintTable, BlueprintField, BlueprintPolicy, BlueprintIndex, etc.
11
11
  * - BlueprintDefinition -- the top-level type matching the JSONB shape
12
12
  *
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * - Per-node-type parameter interfaces (via schema-typescript)
9
9
  * - BlueprintNode -- discriminated union of all non-relation node types
10
- * - BlueprintRelation -- typed relation entries with $type, source_ref, target_ref
10
+ * - BlueprintRelation -- typed relation entries with $type, source_table, target_table
11
11
  * - BlueprintTable, BlueprintField, BlueprintPolicy, BlueprintIndex, etc.
12
12
  * - BlueprintDefinition -- the top-level type matching the JSONB shape
13
13
  *
@@ -246,28 +246,22 @@ function buildBlueprintField(meta) {
246
246
  addJSDoc(optionalProp('description', t.tsStringKeyword()), 'Comment/description for this field.'),
247
247
  ]), 'A custom field (column) to add to a blueprint table.');
248
248
  }
249
- function buildBlueprintPolicy(authzNodes, meta) {
249
+ function buildBlueprintPolicy(authzNodes, _meta) {
250
+ // BlueprintPolicy represents the blueprint JSON shape (not the DB table).
251
+ // The SQL procedure reads $type (not policy_type) from the JSONB:
252
+ // v_policy_type := v_policy_entry->>'$type';
253
+ // So we always use the static definition with $type.
250
254
  const policyTypeAnnotation = authzNodes.length > 0
251
255
  ? strUnion(authzNodes.map((nt) => nt.name))
252
256
  : t.tsStringKeyword();
253
- const table = meta && findTable(meta, 'metaschema_public', 'policy');
254
- if (table) {
255
- return deriveInterfaceFromTable(table, 'BlueprintPolicy', 'An RLS policy entry for a blueprint table. Derived from _meta.', {
256
- // policy_type gets a typed union of known Authz* node names
257
- policy_type: policyTypeAnnotation,
258
- // data is untyped JSONB — use Record<string, unknown>
259
- data: recordType(t.tsStringKeyword(), t.tsUnknownKeyword()),
260
- });
261
- }
262
- // Static fallback
263
257
  return addJSDoc(exportInterface('BlueprintPolicy', [
264
- addJSDoc(requiredProp('policy_type', policyTypeAnnotation), 'Authz* policy type name (e.g., "AuthzDirectOwner", "AuthzAllowAll").'),
258
+ addJSDoc(requiredProp('$type', policyTypeAnnotation), 'Authz* policy type name (e.g., "AuthzDirectOwner", "AuthzAllowAll").'),
259
+ addJSDoc(optionalProp('privileges', t.tsArrayType(t.tsStringKeyword())), 'Privileges this policy applies to (e.g., ["select"], ["insert", "update", "delete"]).'),
260
+ addJSDoc(optionalProp('permissive', t.tsBooleanKeyword()), 'Whether this policy is permissive (true) or restrictive (false). Defaults to true.'),
265
261
  addJSDoc(optionalProp('policy_role', t.tsStringKeyword()), 'Role for this policy. Defaults to "authenticated".'),
266
- addJSDoc(optionalProp('permissive', t.tsBooleanKeyword()), 'Whether this policy is permissive (true) or restrictive (false).'),
267
262
  addJSDoc(optionalProp('policy_name', t.tsStringKeyword()), 'Optional custom name for this policy.'),
268
- addJSDoc(optionalProp('privileges', t.tsArrayType(t.tsStringKeyword())), 'Privileges this policy applies to.'),
269
263
  addJSDoc(optionalProp('data', recordType(t.tsStringKeyword(), t.tsUnknownKeyword())), 'Policy-specific data (structure varies by policy type).'),
270
- ]), 'An RLS policy entry for a blueprint table.');
264
+ ]), 'An RLS policy entry for a blueprint table. Uses $type to match the blueprint JSON convention.');
271
265
  }
272
266
  function buildBlueprintFtsSource() {
273
267
  return addJSDoc(exportInterface('BlueprintFtsSource', [
@@ -278,10 +272,18 @@ function buildBlueprintFtsSource() {
278
272
  }
279
273
  function buildBlueprintFullTextSearch() {
280
274
  return addJSDoc(exportInterface('BlueprintFullTextSearch', [
281
- addJSDoc(requiredProp('table_ref', t.tsStringKeyword()), 'Reference key of the table this full-text search belongs to.'),
275
+ addJSDoc(requiredProp('table_name', t.tsStringKeyword()), 'Table name this full-text search belongs to.'),
276
+ addJSDoc(optionalProp('schema_name', t.tsStringKeyword()), 'Optional schema name for disambiguation (falls back to top-level default).'),
282
277
  addJSDoc(requiredProp('field', t.tsStringKeyword()), 'Name of the tsvector field on the table.'),
283
278
  addJSDoc(requiredProp('sources', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintFtsSource')))), 'Source fields that feed into this tsvector.'),
284
- ]), 'A full-text search configuration for a blueprint table.');
279
+ ]), 'A full-text search configuration for a blueprint table (top-level, requires table_name).');
280
+ }
281
+ function buildBlueprintTableFullTextSearch() {
282
+ return addJSDoc(exportInterface('BlueprintTableFullTextSearch', [
283
+ addJSDoc(requiredProp('field', t.tsStringKeyword()), 'Name of the tsvector field on the table.'),
284
+ addJSDoc(requiredProp('sources', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintFtsSource')))), 'Source fields that feed into this tsvector.'),
285
+ addJSDoc(optionalProp('schema_name', t.tsStringKeyword()), 'Optional schema name override.'),
286
+ ]), 'A full-text search configuration nested inside a table definition (table_name not required).');
285
287
  }
286
288
  function buildBlueprintIndex(meta) {
287
289
  const table = meta && findTable(meta, 'metaschema_public', 'index');
@@ -295,7 +297,8 @@ function buildBlueprintIndex(meta) {
295
297
  }
296
298
  // Static fallback
297
299
  return addJSDoc(exportInterface('BlueprintIndex', [
298
- addJSDoc(requiredProp('table_ref', t.tsStringKeyword()), 'Reference key of the table this index belongs to.'),
300
+ addJSDoc(requiredProp('table_name', t.tsStringKeyword()), 'Table name this index belongs to.'),
301
+ addJSDoc(optionalProp('schema_name', t.tsStringKeyword()), 'Optional schema name for disambiguation (falls back to top-level default).'),
299
302
  addJSDoc(optionalProp('column', t.tsStringKeyword()), 'Single column name for the index.'),
300
303
  addJSDoc(optionalProp('columns', t.tsArrayType(t.tsStringKeyword())), 'Array of column names for a multi-column index.'),
301
304
  addJSDoc(requiredProp('access_method', t.tsStringKeyword()), 'Index access method (e.g., "BTREE", "GIN", "GIST", "HNSW", "BM25").'),
@@ -303,7 +306,19 @@ function buildBlueprintIndex(meta) {
303
306
  addJSDoc(optionalProp('name', t.tsStringKeyword()), 'Optional custom name for the index.'),
304
307
  addJSDoc(optionalProp('op_classes', t.tsArrayType(t.tsStringKeyword())), 'Operator classes for the index columns.'),
305
308
  addJSDoc(optionalProp('options', recordType(t.tsStringKeyword(), t.tsUnknownKeyword())), 'Additional index-specific options.'),
306
- ]), 'An index definition within a blueprint.');
309
+ ]), 'An index definition within a blueprint (top-level, requires table_name).');
310
+ }
311
+ function buildBlueprintTableIndex() {
312
+ return addJSDoc(exportInterface('BlueprintTableIndex', [
313
+ addJSDoc(optionalProp('column', t.tsStringKeyword()), 'Single column name for the index.'),
314
+ addJSDoc(optionalProp('columns', t.tsArrayType(t.tsStringKeyword())), 'Array of column names for a multi-column index.'),
315
+ addJSDoc(requiredProp('access_method', t.tsStringKeyword()), 'Index access method (e.g., "BTREE", "GIN", "GIST", "HNSW", "BM25").'),
316
+ addJSDoc(optionalProp('is_unique', t.tsBooleanKeyword()), 'Whether this is a unique index.'),
317
+ addJSDoc(optionalProp('name', t.tsStringKeyword()), 'Optional custom name for the index.'),
318
+ addJSDoc(optionalProp('op_classes', t.tsArrayType(t.tsStringKeyword())), 'Operator classes for the index columns.'),
319
+ addJSDoc(optionalProp('options', recordType(t.tsStringKeyword(), t.tsUnknownKeyword())), 'Additional index-specific options.'),
320
+ addJSDoc(optionalProp('schema_name', t.tsStringKeyword()), 'Optional schema name override.'),
321
+ ]), 'An index definition nested inside a table definition (table_name not required).');
307
322
  }
308
323
  // ---------------------------------------------------------------------------
309
324
  // Node type discriminated unions
@@ -339,8 +354,10 @@ function buildRelationTypes(relationNodes) {
339
354
  const relationMembers = relationNodes.map((nt) => {
340
355
  const baseType = t.tsTypeLiteral([
341
356
  requiredProp('$type', strLit(nt.name)),
342
- requiredProp('source_ref', t.tsStringKeyword()),
343
- requiredProp('target_ref', t.tsStringKeyword()),
357
+ requiredProp('source_table', t.tsStringKeyword()),
358
+ requiredProp('target_table', t.tsStringKeyword()),
359
+ optionalProp('source_schema_name', t.tsStringKeyword()),
360
+ optionalProp('target_schema_name', t.tsStringKeyword()),
344
361
  ]);
345
362
  return t.tsIntersectionType([
346
363
  baseType,
@@ -354,16 +371,32 @@ function buildRelationTypes(relationNodes) {
354
371
  // ---------------------------------------------------------------------------
355
372
  // BlueprintTable and BlueprintDefinition
356
373
  // ---------------------------------------------------------------------------
374
+ function buildBlueprintUniqueConstraint() {
375
+ return addJSDoc(exportInterface('BlueprintUniqueConstraint', [
376
+ addJSDoc(requiredProp('table_name', t.tsStringKeyword()), 'Table name this unique constraint belongs to.'),
377
+ addJSDoc(optionalProp('schema_name', t.tsStringKeyword()), 'Optional schema name for disambiguation (falls back to top-level default).'),
378
+ addJSDoc(requiredProp('columns', t.tsArrayType(t.tsStringKeyword())), 'Column names that form the unique constraint.'),
379
+ ]), 'A unique constraint definition within a blueprint (top-level, requires table_name).');
380
+ }
381
+ function buildBlueprintTableUniqueConstraint() {
382
+ return addJSDoc(exportInterface('BlueprintTableUniqueConstraint', [
383
+ addJSDoc(requiredProp('columns', t.tsArrayType(t.tsStringKeyword())), 'Column names that form the unique constraint.'),
384
+ addJSDoc(optionalProp('schema_name', t.tsStringKeyword()), 'Optional schema name override.'),
385
+ ]), 'A unique constraint nested inside a table definition (table_name not required).');
386
+ }
357
387
  function buildBlueprintTable() {
358
388
  return addJSDoc(exportInterface('BlueprintTable', [
359
- addJSDoc(requiredProp('ref', t.tsStringKeyword()), 'Local reference key for this table (used by relations, indexes, fts).'),
360
389
  addJSDoc(requiredProp('table_name', t.tsStringKeyword()), 'The PostgreSQL table name to create.'),
390
+ addJSDoc(optionalProp('schema_name', t.tsStringKeyword()), 'Optional schema name (falls back to top-level default).'),
361
391
  addJSDoc(requiredProp('nodes', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintNode')))), "Array of node type entries that define the table's behavior."),
362
392
  addJSDoc(optionalProp('fields', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintField')))), 'Custom fields (columns) to add to the table.'),
363
393
  addJSDoc(optionalProp('policies', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintPolicy')))), 'RLS policies for this table.'),
364
394
  addJSDoc(optionalProp('grant_roles', t.tsArrayType(t.tsStringKeyword())), 'Database roles to grant privileges to. Defaults to ["authenticated"].'),
365
395
  addJSDoc(optionalProp('grants', t.tsArrayType(t.tsUnknownKeyword())), 'Privilege grants as [verb, column] tuples or objects. Defaults to empty (no grants — callers must explicitly specify).'),
366
396
  addJSDoc(optionalProp('use_rls', t.tsBooleanKeyword()), 'Whether to enable RLS on this table. Defaults to true.'),
397
+ addJSDoc(optionalProp('indexes', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintTableIndex')))), 'Table-level indexes (table_name inherited from parent).'),
398
+ addJSDoc(optionalProp('full_text_searches', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintTableFullTextSearch')))), 'Table-level full-text search configurations (table_name inherited from parent).'),
399
+ addJSDoc(optionalProp('unique_constraints', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintTableUniqueConstraint')))), 'Table-level unique constraints (table_name inherited from parent).'),
367
400
  ]), 'A table definition within a blueprint.');
368
401
  }
369
402
  function buildBlueprintDefinition() {
@@ -372,6 +405,7 @@ function buildBlueprintDefinition() {
372
405
  addJSDoc(optionalProp('relations', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintRelation')))), 'Relations between tables.'),
373
406
  addJSDoc(optionalProp('indexes', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintIndex')))), 'Indexes on table columns.'),
374
407
  addJSDoc(optionalProp('full_text_searches', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintFullTextSearch')))), 'Full-text search configurations.'),
408
+ addJSDoc(optionalProp('unique_constraints', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintUniqueConstraint')))), 'Unique constraints on table columns.'),
375
409
  ]), 'The complete blueprint definition -- the JSONB shape accepted by construct_blueprint().');
376
410
  }
377
411
  // ---------------------------------------------------------------------------
@@ -420,7 +454,11 @@ function buildProgram(meta) {
420
454
  statements.push(buildBlueprintPolicy(authzNodes, meta));
421
455
  statements.push(buildBlueprintFtsSource());
422
456
  statements.push(buildBlueprintFullTextSearch());
457
+ statements.push(buildBlueprintTableFullTextSearch());
423
458
  statements.push(buildBlueprintIndex(meta));
459
+ statements.push(buildBlueprintTableIndex());
460
+ statements.push(buildBlueprintUniqueConstraint());
461
+ statements.push(buildBlueprintTableUniqueConstraint());
424
462
  // -- Node types discriminated union --
425
463
  statements.push(sectionComment('Node types -- discriminated union for nodes[] entries'));
426
464
  statements.push(...buildNodeTypes(dataNodes));
@@ -438,7 +476,7 @@ function buildProgram(meta) {
438
476
  '// GENERATED FILE \u2014 DO NOT EDIT',
439
477
  '//',
440
478
  '// Regenerate with:',
441
- '// cd graphile/node-type-registry && pnpm generate:types',
479
+ '// cd graphql/node-type-registry && pnpm generate:types',
442
480
  '//',
443
481
  '// These types match the JSONB shape expected by construct_blueprint().',
444
482
  '// All field names are snake_case to match the SQL convention.',
@@ -444,18 +444,18 @@ export interface BlueprintField {
444
444
  /** Comment/description for this field. */
445
445
  description?: string;
446
446
  }
447
- /** An RLS policy entry for a blueprint table. */
447
+ /** An RLS policy entry for a blueprint table. Uses $type to match the blueprint JSON convention. */
448
448
  export interface BlueprintPolicy {
449
449
  /** Authz* policy type name (e.g., "AuthzDirectOwner", "AuthzAllowAll"). */
450
- policy_type: "AuthzDirectOwner" | "AuthzDirectOwnerAny" | "AuthzMembership" | "AuthzEntityMembership" | "AuthzRelatedEntityMembership" | "AuthzOrgHierarchy" | "AuthzTemporal" | "AuthzPublishable" | "AuthzMemberList" | "AuthzRelatedMemberList" | "AuthzAllowAll" | "AuthzDenyAll" | "AuthzComposite" | "AuthzPeerOwnership" | "AuthzRelatedPeerOwnership";
450
+ $type: "AuthzDirectOwner" | "AuthzDirectOwnerAny" | "AuthzMembership" | "AuthzEntityMembership" | "AuthzRelatedEntityMembership" | "AuthzOrgHierarchy" | "AuthzTemporal" | "AuthzPublishable" | "AuthzMemberList" | "AuthzRelatedMemberList" | "AuthzAllowAll" | "AuthzDenyAll" | "AuthzComposite" | "AuthzPeerOwnership" | "AuthzRelatedPeerOwnership";
451
+ /** Privileges this policy applies to (e.g., ["select"], ["insert", "update", "delete"]). */
452
+ privileges?: string[];
453
+ /** Whether this policy is permissive (true) or restrictive (false). Defaults to true. */
454
+ permissive?: boolean;
451
455
  /** Role for this policy. Defaults to "authenticated". */
452
456
  policy_role?: string;
453
- /** Whether this policy is permissive (true) or restrictive (false). */
454
- permissive?: boolean;
455
457
  /** Optional custom name for this policy. */
456
458
  policy_name?: string;
457
- /** Privileges this policy applies to. */
458
- privileges?: string[];
459
459
  /** Policy-specific data (structure varies by policy type). */
460
460
  data?: Record<string, unknown>;
461
461
  }
@@ -468,19 +468,49 @@ export interface BlueprintFtsSource {
468
468
  /** Language for text search. Defaults to "english". */
469
469
  lang?: string;
470
470
  }
471
- /** A full-text search configuration for a blueprint table. */
471
+ /** A full-text search configuration for a blueprint table (top-level, requires table_name). */
472
472
  export interface BlueprintFullTextSearch {
473
- /** Reference key of the table this full-text search belongs to. */
474
- table_ref: string;
473
+ /** Table name this full-text search belongs to. */
474
+ table_name: string;
475
+ /** Optional schema name for disambiguation (falls back to top-level default). */
476
+ schema_name?: string;
477
+ /** Name of the tsvector field on the table. */
478
+ field: string;
479
+ /** Source fields that feed into this tsvector. */
480
+ sources: BlueprintFtsSource[];
481
+ }
482
+ /** A full-text search configuration nested inside a table definition (table_name not required). */
483
+ export interface BlueprintTableFullTextSearch {
475
484
  /** Name of the tsvector field on the table. */
476
485
  field: string;
477
486
  /** Source fields that feed into this tsvector. */
478
487
  sources: BlueprintFtsSource[];
488
+ /** Optional schema name override. */
489
+ schema_name?: string;
479
490
  }
480
- /** An index definition within a blueprint. */
491
+ /** An index definition within a blueprint (top-level, requires table_name). */
481
492
  export interface BlueprintIndex {
482
- /** Reference key of the table this index belongs to. */
483
- table_ref: string;
493
+ /** Table name this index belongs to. */
494
+ table_name: string;
495
+ /** Optional schema name for disambiguation (falls back to top-level default). */
496
+ schema_name?: string;
497
+ /** Single column name for the index. */
498
+ column?: string;
499
+ /** Array of column names for a multi-column index. */
500
+ columns?: string[];
501
+ /** Index access method (e.g., "BTREE", "GIN", "GIST", "HNSW", "BM25"). */
502
+ access_method: string;
503
+ /** Whether this is a unique index. */
504
+ is_unique?: boolean;
505
+ /** Optional custom name for the index. */
506
+ name?: string;
507
+ /** Operator classes for the index columns. */
508
+ op_classes?: string[];
509
+ /** Additional index-specific options. */
510
+ options?: Record<string, unknown>;
511
+ }
512
+ /** An index definition nested inside a table definition (table_name not required). */
513
+ export interface BlueprintTableIndex {
484
514
  /** Single column name for the index. */
485
515
  column?: string;
486
516
  /** Array of column names for a multi-column index. */
@@ -495,6 +525,24 @@ export interface BlueprintIndex {
495
525
  op_classes?: string[];
496
526
  /** Additional index-specific options. */
497
527
  options?: Record<string, unknown>;
528
+ /** Optional schema name override. */
529
+ schema_name?: string;
530
+ }
531
+ /** A unique constraint definition within a blueprint (top-level, requires table_name). */
532
+ export interface BlueprintUniqueConstraint {
533
+ /** Table name this unique constraint belongs to. */
534
+ table_name: string;
535
+ /** Optional schema name for disambiguation (falls back to top-level default). */
536
+ schema_name?: string;
537
+ /** Column names that form the unique constraint. */
538
+ columns: string[];
539
+ }
540
+ /** A unique constraint nested inside a table definition (table_name not required). */
541
+ export interface BlueprintTableUniqueConstraint {
542
+ /** Column names that form the unique constraint. */
543
+ columns: string[];
544
+ /** Optional schema name override. */
545
+ schema_name?: string;
498
546
  }
499
547
  /** String shorthand -- just the node type name. */
500
548
  export type BlueprintNodeShorthand = "AuthzDirectOwner" | "AuthzDirectOwnerAny" | "AuthzMembership" | "AuthzEntityMembership" | "AuthzRelatedEntityMembership" | "AuthzOrgHierarchy" | "AuthzTemporal" | "AuthzPublishable" | "AuthzMemberList" | "AuthzRelatedMemberList" | "AuthzAllowAll" | "AuthzDenyAll" | "AuthzComposite" | "AuthzPeerOwnership" | "AuthzRelatedPeerOwnership" | "DataId" | "DataDirectOwner" | "DataEntityMembership" | "DataOwnershipInEntity" | "DataTimestamps" | "DataPeoplestamps" | "DataPublishable" | "DataSoftDelete" | "DataEmbedding" | "DataFullTextSearch" | "DataBm25" | "DataSearch" | "DataPostGIS" | "DataPostGISAggregate" | "DataJobTrigger" | "DataTags" | "DataStatusField" | "DataJsonb" | "DataTrgm" | "DataSlug" | "DataInflection" | "DataOwnedFields" | "DataInheritFromParent" | "DataForceCurrentUser" | "DataImmutableFields" | "TableUserProfiles" | "TableOrganizationSettings" | "TableUserSettings";
@@ -634,27 +682,35 @@ export type BlueprintNode = BlueprintNodeShorthand | BlueprintNodeObject;
634
682
  /** A relation entry in a blueprint definition. */
635
683
  export type BlueprintRelation = {
636
684
  $type: "RelationBelongsTo";
637
- source_ref: string;
638
- target_ref: string;
685
+ source_table: string;
686
+ target_table: string;
687
+ source_schema_name?: string;
688
+ target_schema_name?: string;
639
689
  } & Partial<RelationBelongsToParams> | {
640
690
  $type: "RelationHasOne";
641
- source_ref: string;
642
- target_ref: string;
691
+ source_table: string;
692
+ target_table: string;
693
+ source_schema_name?: string;
694
+ target_schema_name?: string;
643
695
  } & Partial<RelationHasOneParams> | {
644
696
  $type: "RelationHasMany";
645
- source_ref: string;
646
- target_ref: string;
697
+ source_table: string;
698
+ target_table: string;
699
+ source_schema_name?: string;
700
+ target_schema_name?: string;
647
701
  } & Partial<RelationHasManyParams> | {
648
702
  $type: "RelationManyToMany";
649
- source_ref: string;
650
- target_ref: string;
703
+ source_table: string;
704
+ target_table: string;
705
+ source_schema_name?: string;
706
+ target_schema_name?: string;
651
707
  } & Partial<RelationManyToManyParams>;
652
708
  /** A table definition within a blueprint. */
653
709
  export interface BlueprintTable {
654
- /** Local reference key for this table (used by relations, indexes, fts). */
655
- ref: string;
656
710
  /** The PostgreSQL table name to create. */
657
711
  table_name: string;
712
+ /** Optional schema name (falls back to top-level default). */
713
+ schema_name?: string;
658
714
  /** Array of node type entries that define the table's behavior. */
659
715
  nodes: BlueprintNode[];
660
716
  /** Custom fields (columns) to add to the table. */
@@ -667,6 +723,12 @@ export interface BlueprintTable {
667
723
  grants?: unknown[];
668
724
  /** Whether to enable RLS on this table. Defaults to true. */
669
725
  use_rls?: boolean;
726
+ /** Table-level indexes (table_name inherited from parent). */
727
+ indexes?: BlueprintTableIndex[];
728
+ /** Table-level full-text search configurations (table_name inherited from parent). */
729
+ full_text_searches?: BlueprintTableFullTextSearch[];
730
+ /** Table-level unique constraints (table_name inherited from parent). */
731
+ unique_constraints?: BlueprintTableUniqueConstraint[];
670
732
  }
671
733
  /** The complete blueprint definition -- the JSONB shape accepted by construct_blueprint(). */
672
734
  export interface BlueprintDefinition {
@@ -678,4 +740,6 @@ export interface BlueprintDefinition {
678
740
  indexes?: BlueprintIndex[];
679
741
  /** Full-text search configurations. */
680
742
  full_text_searches?: BlueprintFullTextSearch[];
743
+ /** Unique constraints on table columns. */
744
+ unique_constraints?: BlueprintUniqueConstraint[];
681
745
  }
@@ -1,7 +1,7 @@
1
1
  // GENERATED FILE — DO NOT EDIT
2
2
  //
3
3
  // Regenerate with:
4
- // cd graphile/node-type-registry && pnpm generate:types
4
+ // cd graphql/node-type-registry && pnpm generate:types
5
5
  //
6
6
  // These types match the JSONB shape expected by construct_blueprint().
7
7
  // All field names are snake_case to match the SQL convention.
@@ -126,7 +126,7 @@ const GENERATED_HEADER = [
126
126
  '-- GENERATED FILE — DO NOT EDIT',
127
127
  '--',
128
128
  '-- Regenerate with:',
129
- '-- cd graphile/node-type-registry && pnpm generate:seed --pgpm ../../constructive-db/packages/metaschema',
129
+ '-- cd graphql/node-type-registry && pnpm generate:seed --pgpm ../../constructive-db/packages/metaschema',
130
130
  '--',
131
131
  '',
132
132
  ].join('\n');
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * - Per-node-type parameter interfaces (via schema-typescript)
8
8
  * - BlueprintNode -- discriminated union of all non-relation node types
9
- * - BlueprintRelation -- typed relation entries with $type, source_ref, target_ref
9
+ * - BlueprintRelation -- typed relation entries with $type, source_table, target_table
10
10
  * - BlueprintTable, BlueprintField, BlueprintPolicy, BlueprintIndex, etc.
11
11
  * - BlueprintDefinition -- the top-level type matching the JSONB shape
12
12
  *
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * - Per-node-type parameter interfaces (via schema-typescript)
8
8
  * - BlueprintNode -- discriminated union of all non-relation node types
9
- * - BlueprintRelation -- typed relation entries with $type, source_ref, target_ref
9
+ * - BlueprintRelation -- typed relation entries with $type, source_table, target_table
10
10
  * - BlueprintTable, BlueprintField, BlueprintPolicy, BlueprintIndex, etc.
11
11
  * - BlueprintDefinition -- the top-level type matching the JSONB shape
12
12
  *
@@ -211,28 +211,22 @@ function buildBlueprintField(meta) {
211
211
  addJSDoc(optionalProp('description', t.tsStringKeyword()), 'Comment/description for this field.'),
212
212
  ]), 'A custom field (column) to add to a blueprint table.');
213
213
  }
214
- function buildBlueprintPolicy(authzNodes, meta) {
214
+ function buildBlueprintPolicy(authzNodes, _meta) {
215
+ // BlueprintPolicy represents the blueprint JSON shape (not the DB table).
216
+ // The SQL procedure reads $type (not policy_type) from the JSONB:
217
+ // v_policy_type := v_policy_entry->>'$type';
218
+ // So we always use the static definition with $type.
215
219
  const policyTypeAnnotation = authzNodes.length > 0
216
220
  ? strUnion(authzNodes.map((nt) => nt.name))
217
221
  : t.tsStringKeyword();
218
- const table = meta && findTable(meta, 'metaschema_public', 'policy');
219
- if (table) {
220
- return deriveInterfaceFromTable(table, 'BlueprintPolicy', 'An RLS policy entry for a blueprint table. Derived from _meta.', {
221
- // policy_type gets a typed union of known Authz* node names
222
- policy_type: policyTypeAnnotation,
223
- // data is untyped JSONB — use Record<string, unknown>
224
- data: recordType(t.tsStringKeyword(), t.tsUnknownKeyword()),
225
- });
226
- }
227
- // Static fallback
228
222
  return addJSDoc(exportInterface('BlueprintPolicy', [
229
- addJSDoc(requiredProp('policy_type', policyTypeAnnotation), 'Authz* policy type name (e.g., "AuthzDirectOwner", "AuthzAllowAll").'),
223
+ addJSDoc(requiredProp('$type', policyTypeAnnotation), 'Authz* policy type name (e.g., "AuthzDirectOwner", "AuthzAllowAll").'),
224
+ addJSDoc(optionalProp('privileges', t.tsArrayType(t.tsStringKeyword())), 'Privileges this policy applies to (e.g., ["select"], ["insert", "update", "delete"]).'),
225
+ addJSDoc(optionalProp('permissive', t.tsBooleanKeyword()), 'Whether this policy is permissive (true) or restrictive (false). Defaults to true.'),
230
226
  addJSDoc(optionalProp('policy_role', t.tsStringKeyword()), 'Role for this policy. Defaults to "authenticated".'),
231
- addJSDoc(optionalProp('permissive', t.tsBooleanKeyword()), 'Whether this policy is permissive (true) or restrictive (false).'),
232
227
  addJSDoc(optionalProp('policy_name', t.tsStringKeyword()), 'Optional custom name for this policy.'),
233
- addJSDoc(optionalProp('privileges', t.tsArrayType(t.tsStringKeyword())), 'Privileges this policy applies to.'),
234
228
  addJSDoc(optionalProp('data', recordType(t.tsStringKeyword(), t.tsUnknownKeyword())), 'Policy-specific data (structure varies by policy type).'),
235
- ]), 'An RLS policy entry for a blueprint table.');
229
+ ]), 'An RLS policy entry for a blueprint table. Uses $type to match the blueprint JSON convention.');
236
230
  }
237
231
  function buildBlueprintFtsSource() {
238
232
  return addJSDoc(exportInterface('BlueprintFtsSource', [
@@ -243,10 +237,18 @@ function buildBlueprintFtsSource() {
243
237
  }
244
238
  function buildBlueprintFullTextSearch() {
245
239
  return addJSDoc(exportInterface('BlueprintFullTextSearch', [
246
- addJSDoc(requiredProp('table_ref', t.tsStringKeyword()), 'Reference key of the table this full-text search belongs to.'),
240
+ addJSDoc(requiredProp('table_name', t.tsStringKeyword()), 'Table name this full-text search belongs to.'),
241
+ addJSDoc(optionalProp('schema_name', t.tsStringKeyword()), 'Optional schema name for disambiguation (falls back to top-level default).'),
247
242
  addJSDoc(requiredProp('field', t.tsStringKeyword()), 'Name of the tsvector field on the table.'),
248
243
  addJSDoc(requiredProp('sources', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintFtsSource')))), 'Source fields that feed into this tsvector.'),
249
- ]), 'A full-text search configuration for a blueprint table.');
244
+ ]), 'A full-text search configuration for a blueprint table (top-level, requires table_name).');
245
+ }
246
+ function buildBlueprintTableFullTextSearch() {
247
+ return addJSDoc(exportInterface('BlueprintTableFullTextSearch', [
248
+ addJSDoc(requiredProp('field', t.tsStringKeyword()), 'Name of the tsvector field on the table.'),
249
+ addJSDoc(requiredProp('sources', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintFtsSource')))), 'Source fields that feed into this tsvector.'),
250
+ addJSDoc(optionalProp('schema_name', t.tsStringKeyword()), 'Optional schema name override.'),
251
+ ]), 'A full-text search configuration nested inside a table definition (table_name not required).');
250
252
  }
251
253
  function buildBlueprintIndex(meta) {
252
254
  const table = meta && findTable(meta, 'metaschema_public', 'index');
@@ -260,7 +262,8 @@ function buildBlueprintIndex(meta) {
260
262
  }
261
263
  // Static fallback
262
264
  return addJSDoc(exportInterface('BlueprintIndex', [
263
- addJSDoc(requiredProp('table_ref', t.tsStringKeyword()), 'Reference key of the table this index belongs to.'),
265
+ addJSDoc(requiredProp('table_name', t.tsStringKeyword()), 'Table name this index belongs to.'),
266
+ addJSDoc(optionalProp('schema_name', t.tsStringKeyword()), 'Optional schema name for disambiguation (falls back to top-level default).'),
264
267
  addJSDoc(optionalProp('column', t.tsStringKeyword()), 'Single column name for the index.'),
265
268
  addJSDoc(optionalProp('columns', t.tsArrayType(t.tsStringKeyword())), 'Array of column names for a multi-column index.'),
266
269
  addJSDoc(requiredProp('access_method', t.tsStringKeyword()), 'Index access method (e.g., "BTREE", "GIN", "GIST", "HNSW", "BM25").'),
@@ -268,7 +271,19 @@ function buildBlueprintIndex(meta) {
268
271
  addJSDoc(optionalProp('name', t.tsStringKeyword()), 'Optional custom name for the index.'),
269
272
  addJSDoc(optionalProp('op_classes', t.tsArrayType(t.tsStringKeyword())), 'Operator classes for the index columns.'),
270
273
  addJSDoc(optionalProp('options', recordType(t.tsStringKeyword(), t.tsUnknownKeyword())), 'Additional index-specific options.'),
271
- ]), 'An index definition within a blueprint.');
274
+ ]), 'An index definition within a blueprint (top-level, requires table_name).');
275
+ }
276
+ function buildBlueprintTableIndex() {
277
+ return addJSDoc(exportInterface('BlueprintTableIndex', [
278
+ addJSDoc(optionalProp('column', t.tsStringKeyword()), 'Single column name for the index.'),
279
+ addJSDoc(optionalProp('columns', t.tsArrayType(t.tsStringKeyword())), 'Array of column names for a multi-column index.'),
280
+ addJSDoc(requiredProp('access_method', t.tsStringKeyword()), 'Index access method (e.g., "BTREE", "GIN", "GIST", "HNSW", "BM25").'),
281
+ addJSDoc(optionalProp('is_unique', t.tsBooleanKeyword()), 'Whether this is a unique index.'),
282
+ addJSDoc(optionalProp('name', t.tsStringKeyword()), 'Optional custom name for the index.'),
283
+ addJSDoc(optionalProp('op_classes', t.tsArrayType(t.tsStringKeyword())), 'Operator classes for the index columns.'),
284
+ addJSDoc(optionalProp('options', recordType(t.tsStringKeyword(), t.tsUnknownKeyword())), 'Additional index-specific options.'),
285
+ addJSDoc(optionalProp('schema_name', t.tsStringKeyword()), 'Optional schema name override.'),
286
+ ]), 'An index definition nested inside a table definition (table_name not required).');
272
287
  }
273
288
  // ---------------------------------------------------------------------------
274
289
  // Node type discriminated unions
@@ -304,8 +319,10 @@ function buildRelationTypes(relationNodes) {
304
319
  const relationMembers = relationNodes.map((nt) => {
305
320
  const baseType = t.tsTypeLiteral([
306
321
  requiredProp('$type', strLit(nt.name)),
307
- requiredProp('source_ref', t.tsStringKeyword()),
308
- requiredProp('target_ref', t.tsStringKeyword()),
322
+ requiredProp('source_table', t.tsStringKeyword()),
323
+ requiredProp('target_table', t.tsStringKeyword()),
324
+ optionalProp('source_schema_name', t.tsStringKeyword()),
325
+ optionalProp('target_schema_name', t.tsStringKeyword()),
309
326
  ]);
310
327
  return t.tsIntersectionType([
311
328
  baseType,
@@ -319,16 +336,32 @@ function buildRelationTypes(relationNodes) {
319
336
  // ---------------------------------------------------------------------------
320
337
  // BlueprintTable and BlueprintDefinition
321
338
  // ---------------------------------------------------------------------------
339
+ function buildBlueprintUniqueConstraint() {
340
+ return addJSDoc(exportInterface('BlueprintUniqueConstraint', [
341
+ addJSDoc(requiredProp('table_name', t.tsStringKeyword()), 'Table name this unique constraint belongs to.'),
342
+ addJSDoc(optionalProp('schema_name', t.tsStringKeyword()), 'Optional schema name for disambiguation (falls back to top-level default).'),
343
+ addJSDoc(requiredProp('columns', t.tsArrayType(t.tsStringKeyword())), 'Column names that form the unique constraint.'),
344
+ ]), 'A unique constraint definition within a blueprint (top-level, requires table_name).');
345
+ }
346
+ function buildBlueprintTableUniqueConstraint() {
347
+ return addJSDoc(exportInterface('BlueprintTableUniqueConstraint', [
348
+ addJSDoc(requiredProp('columns', t.tsArrayType(t.tsStringKeyword())), 'Column names that form the unique constraint.'),
349
+ addJSDoc(optionalProp('schema_name', t.tsStringKeyword()), 'Optional schema name override.'),
350
+ ]), 'A unique constraint nested inside a table definition (table_name not required).');
351
+ }
322
352
  function buildBlueprintTable() {
323
353
  return addJSDoc(exportInterface('BlueprintTable', [
324
- addJSDoc(requiredProp('ref', t.tsStringKeyword()), 'Local reference key for this table (used by relations, indexes, fts).'),
325
354
  addJSDoc(requiredProp('table_name', t.tsStringKeyword()), 'The PostgreSQL table name to create.'),
355
+ addJSDoc(optionalProp('schema_name', t.tsStringKeyword()), 'Optional schema name (falls back to top-level default).'),
326
356
  addJSDoc(requiredProp('nodes', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintNode')))), "Array of node type entries that define the table's behavior."),
327
357
  addJSDoc(optionalProp('fields', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintField')))), 'Custom fields (columns) to add to the table.'),
328
358
  addJSDoc(optionalProp('policies', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintPolicy')))), 'RLS policies for this table.'),
329
359
  addJSDoc(optionalProp('grant_roles', t.tsArrayType(t.tsStringKeyword())), 'Database roles to grant privileges to. Defaults to ["authenticated"].'),
330
360
  addJSDoc(optionalProp('grants', t.tsArrayType(t.tsUnknownKeyword())), 'Privilege grants as [verb, column] tuples or objects. Defaults to empty (no grants — callers must explicitly specify).'),
331
361
  addJSDoc(optionalProp('use_rls', t.tsBooleanKeyword()), 'Whether to enable RLS on this table. Defaults to true.'),
362
+ addJSDoc(optionalProp('indexes', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintTableIndex')))), 'Table-level indexes (table_name inherited from parent).'),
363
+ addJSDoc(optionalProp('full_text_searches', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintTableFullTextSearch')))), 'Table-level full-text search configurations (table_name inherited from parent).'),
364
+ addJSDoc(optionalProp('unique_constraints', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintTableUniqueConstraint')))), 'Table-level unique constraints (table_name inherited from parent).'),
332
365
  ]), 'A table definition within a blueprint.');
333
366
  }
334
367
  function buildBlueprintDefinition() {
@@ -337,6 +370,7 @@ function buildBlueprintDefinition() {
337
370
  addJSDoc(optionalProp('relations', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintRelation')))), 'Relations between tables.'),
338
371
  addJSDoc(optionalProp('indexes', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintIndex')))), 'Indexes on table columns.'),
339
372
  addJSDoc(optionalProp('full_text_searches', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintFullTextSearch')))), 'Full-text search configurations.'),
373
+ addJSDoc(optionalProp('unique_constraints', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintUniqueConstraint')))), 'Unique constraints on table columns.'),
340
374
  ]), 'The complete blueprint definition -- the JSONB shape accepted by construct_blueprint().');
341
375
  }
342
376
  // ---------------------------------------------------------------------------
@@ -385,7 +419,11 @@ function buildProgram(meta) {
385
419
  statements.push(buildBlueprintPolicy(authzNodes, meta));
386
420
  statements.push(buildBlueprintFtsSource());
387
421
  statements.push(buildBlueprintFullTextSearch());
422
+ statements.push(buildBlueprintTableFullTextSearch());
388
423
  statements.push(buildBlueprintIndex(meta));
424
+ statements.push(buildBlueprintTableIndex());
425
+ statements.push(buildBlueprintUniqueConstraint());
426
+ statements.push(buildBlueprintTableUniqueConstraint());
389
427
  // -- Node types discriminated union --
390
428
  statements.push(sectionComment('Node types -- discriminated union for nodes[] entries'));
391
429
  statements.push(...buildNodeTypes(dataNodes));
@@ -403,7 +441,7 @@ function buildProgram(meta) {
403
441
  '// GENERATED FILE \u2014 DO NOT EDIT',
404
442
  '//',
405
443
  '// Regenerate with:',
406
- '// cd graphile/node-type-registry && pnpm generate:types',
444
+ '// cd graphql/node-type-registry && pnpm generate:types',
407
445
  '//',
408
446
  '// These types match the JSONB shape expected by construct_blueprint().',
409
447
  '// All field names are snake_case to match the SQL convention.',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-type-registry",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Node type definitions for the Constructive blueprint system. Single source of truth for all Authz*, Data*, Relation*, and View* node types.",
5
5
  "author": "Constructive <developers@constructive.io>",
6
6
  "main": "index.js",
@@ -48,5 +48,5 @@
48
48
  "registry",
49
49
  "graphile"
50
50
  ],
51
- "gitHead": "ac3d503b309d7981fe715116502062afb13500e0"
51
+ "gitHead": "091694a30420500790004997a61770972e158d82"
52
52
  }