node-type-registry 0.7.1 → 0.9.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,7 +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
79
+ cd graphql/node-type-registry && pnpm generate:seed --pgpm ../../constructive-db/packages/metaschema
75
80
  ```
76
81
 
77
82
  ---
@@ -363,7 +363,7 @@ export interface RelationHasManyParams {
363
363
  delete_action: "c" | "r" | "n" | "d" | "a";
364
364
  is_required?: boolean;
365
365
  }
366
- /** Creates a junction table between source and target tables with auto-derived naming and FK fields. The trigger creates a bare table (no implicit DataId or any node_type), adds FK fields to both tables, optionally creates a composite PK (use_composite_key), then forwards all security config to secure_table_provision as-is. The trigger never injects values the caller did not provide. Junction table FKs always CASCADE on delete. */
366
+ /** Creates a junction table between source and target tables with auto-derived naming and FK fields. The trigger creates a bare table (no implicit DataId), adds FK fields to both tables, optionally creates a composite PK (use_composite_key), then forwards all security config to secure_table_provision as-is. The trigger never injects values the caller did not provide. Junction table FKs always CASCADE on delete. */
367
367
  export interface RelationManyToManyParams {
368
368
  source_table_id: string;
369
369
  target_table_id: string;
@@ -372,10 +372,9 @@ export interface RelationManyToManyParams {
372
372
  source_field_name?: string;
373
373
  target_field_name?: string;
374
374
  use_composite_key?: boolean;
375
- node_type?: string;
376
- node_data?: {
375
+ nodes?: {
377
376
  [key: string]: unknown;
378
- };
377
+ }[];
379
378
  grant_roles?: string[];
380
379
  grant_privileges?: string[][];
381
380
  policy_type?: string;
@@ -444,18 +443,18 @@ export interface BlueprintField {
444
443
  /** Comment/description for this field. */
445
444
  description?: string;
446
445
  }
447
- /** An RLS policy entry for a blueprint table. */
446
+ /** An RLS policy entry for a blueprint table. Uses $type to match the blueprint JSON convention. */
448
447
  export interface BlueprintPolicy {
449
448
  /** 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";
449
+ $type: "AuthzDirectOwner" | "AuthzDirectOwnerAny" | "AuthzMembership" | "AuthzEntityMembership" | "AuthzRelatedEntityMembership" | "AuthzOrgHierarchy" | "AuthzTemporal" | "AuthzPublishable" | "AuthzMemberList" | "AuthzRelatedMemberList" | "AuthzAllowAll" | "AuthzDenyAll" | "AuthzComposite" | "AuthzPeerOwnership" | "AuthzRelatedPeerOwnership";
450
+ /** Privileges this policy applies to (e.g., ["select"], ["insert", "update", "delete"]). */
451
+ privileges?: string[];
452
+ /** Whether this policy is permissive (true) or restrictive (false). Defaults to true. */
453
+ permissive?: boolean;
451
454
  /** Role for this policy. Defaults to "authenticated". */
452
455
  policy_role?: string;
453
- /** Whether this policy is permissive (true) or restrictive (false). */
454
- permissive?: boolean;
455
456
  /** Optional custom name for this policy. */
456
457
  policy_name?: string;
457
- /** Privileges this policy applies to. */
458
- privileges?: string[];
459
458
  /** Policy-specific data (structure varies by policy type). */
460
459
  data?: Record<string, unknown>;
461
460
  }
@@ -468,19 +467,49 @@ export interface BlueprintFtsSource {
468
467
  /** Language for text search. Defaults to "english". */
469
468
  lang?: string;
470
469
  }
471
- /** A full-text search configuration for a blueprint table. */
470
+ /** A full-text search configuration for a blueprint table (top-level, requires table_name). */
472
471
  export interface BlueprintFullTextSearch {
473
- /** Reference key of the table this full-text search belongs to. */
474
- table_ref: string;
472
+ /** Table name this full-text search belongs to. */
473
+ table_name: string;
474
+ /** Optional schema name for disambiguation (falls back to top-level default). */
475
+ schema_name?: string;
475
476
  /** Name of the tsvector field on the table. */
476
477
  field: string;
477
478
  /** Source fields that feed into this tsvector. */
478
479
  sources: BlueprintFtsSource[];
479
480
  }
480
- /** An index definition within a blueprint. */
481
+ /** A full-text search configuration nested inside a table definition (table_name not required). */
482
+ export interface BlueprintTableFullTextSearch {
483
+ /** Name of the tsvector field on the table. */
484
+ field: string;
485
+ /** Source fields that feed into this tsvector. */
486
+ sources: BlueprintFtsSource[];
487
+ /** Optional schema name override. */
488
+ schema_name?: string;
489
+ }
490
+ /** An index definition within a blueprint (top-level, requires table_name). */
481
491
  export interface BlueprintIndex {
482
- /** Reference key of the table this index belongs to. */
483
- table_ref: string;
492
+ /** Table name this index belongs to. */
493
+ table_name: string;
494
+ /** Optional schema name for disambiguation (falls back to top-level default). */
495
+ schema_name?: string;
496
+ /** Single column name for the index. */
497
+ column?: string;
498
+ /** Array of column names for a multi-column index. */
499
+ columns?: string[];
500
+ /** Index access method (e.g., "BTREE", "GIN", "GIST", "HNSW", "BM25"). */
501
+ access_method: string;
502
+ /** Whether this is a unique index. */
503
+ is_unique?: boolean;
504
+ /** Optional custom name for the index. */
505
+ name?: string;
506
+ /** Operator classes for the index columns. */
507
+ op_classes?: string[];
508
+ /** Additional index-specific options. */
509
+ options?: Record<string, unknown>;
510
+ }
511
+ /** An index definition nested inside a table definition (table_name not required). */
512
+ export interface BlueprintTableIndex {
484
513
  /** Single column name for the index. */
485
514
  column?: string;
486
515
  /** Array of column names for a multi-column index. */
@@ -495,6 +524,24 @@ export interface BlueprintIndex {
495
524
  op_classes?: string[];
496
525
  /** Additional index-specific options. */
497
526
  options?: Record<string, unknown>;
527
+ /** Optional schema name override. */
528
+ schema_name?: string;
529
+ }
530
+ /** A unique constraint definition within a blueprint (top-level, requires table_name). */
531
+ export interface BlueprintUniqueConstraint {
532
+ /** Table name this unique constraint belongs to. */
533
+ table_name: string;
534
+ /** Optional schema name for disambiguation (falls back to top-level default). */
535
+ schema_name?: string;
536
+ /** Column names that form the unique constraint. */
537
+ columns: string[];
538
+ }
539
+ /** A unique constraint nested inside a table definition (table_name not required). */
540
+ export interface BlueprintTableUniqueConstraint {
541
+ /** Column names that form the unique constraint. */
542
+ columns: string[];
543
+ /** Optional schema name override. */
544
+ schema_name?: string;
498
545
  }
499
546
  /** String shorthand -- just the node type name. */
500
547
  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 +681,35 @@ export type BlueprintNode = BlueprintNodeShorthand | BlueprintNodeObject;
634
681
  /** A relation entry in a blueprint definition. */
635
682
  export type BlueprintRelation = {
636
683
  $type: "RelationBelongsTo";
637
- source_ref: string;
638
- target_ref: string;
684
+ source_table: string;
685
+ target_table: string;
686
+ source_schema_name?: string;
687
+ target_schema_name?: string;
639
688
  } & Partial<RelationBelongsToParams> | {
640
689
  $type: "RelationHasOne";
641
- source_ref: string;
642
- target_ref: string;
690
+ source_table: string;
691
+ target_table: string;
692
+ source_schema_name?: string;
693
+ target_schema_name?: string;
643
694
  } & Partial<RelationHasOneParams> | {
644
695
  $type: "RelationHasMany";
645
- source_ref: string;
646
- target_ref: string;
696
+ source_table: string;
697
+ target_table: string;
698
+ source_schema_name?: string;
699
+ target_schema_name?: string;
647
700
  } & Partial<RelationHasManyParams> | {
648
701
  $type: "RelationManyToMany";
649
- source_ref: string;
650
- target_ref: string;
702
+ source_table: string;
703
+ target_table: string;
704
+ source_schema_name?: string;
705
+ target_schema_name?: string;
651
706
  } & Partial<RelationManyToManyParams>;
652
707
  /** A table definition within a blueprint. */
653
708
  export interface BlueprintTable {
654
- /** Local reference key for this table (used by relations, indexes, fts). */
655
- ref: string;
656
709
  /** The PostgreSQL table name to create. */
657
710
  table_name: string;
711
+ /** Optional schema name (falls back to top-level default). */
712
+ schema_name?: string;
658
713
  /** Array of node type entries that define the table's behavior. */
659
714
  nodes: BlueprintNode[];
660
715
  /** Custom fields (columns) to add to the table. */
@@ -667,6 +722,12 @@ export interface BlueprintTable {
667
722
  grants?: unknown[];
668
723
  /** Whether to enable RLS on this table. Defaults to true. */
669
724
  use_rls?: boolean;
725
+ /** Table-level indexes (table_name inherited from parent). */
726
+ indexes?: BlueprintTableIndex[];
727
+ /** Table-level full-text search configurations (table_name inherited from parent). */
728
+ full_text_searches?: BlueprintTableFullTextSearch[];
729
+ /** Table-level unique constraints (table_name inherited from parent). */
730
+ unique_constraints?: BlueprintTableUniqueConstraint[];
670
731
  }
671
732
  /** The complete blueprint definition -- the JSONB shape accepted by construct_blueprint(). */
672
733
  export interface BlueprintDefinition {
@@ -678,4 +739,6 @@ export interface BlueprintDefinition {
678
739
  indexes?: BlueprintIndex[];
679
740
  /** Full-text search configurations. */
680
741
  full_text_searches?: BlueprintFullTextSearch[];
742
+ /** Unique constraints on table columns. */
743
+ unique_constraints?: BlueprintUniqueConstraint[];
681
744
  }
@@ -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.',
@@ -363,7 +363,7 @@ export interface RelationHasManyParams {
363
363
  delete_action: "c" | "r" | "n" | "d" | "a";
364
364
  is_required?: boolean;
365
365
  }
366
- /** Creates a junction table between source and target tables with auto-derived naming and FK fields. The trigger creates a bare table (no implicit DataId or any node_type), adds FK fields to both tables, optionally creates a composite PK (use_composite_key), then forwards all security config to secure_table_provision as-is. The trigger never injects values the caller did not provide. Junction table FKs always CASCADE on delete. */
366
+ /** Creates a junction table between source and target tables with auto-derived naming and FK fields. The trigger creates a bare table (no implicit DataId), adds FK fields to both tables, optionally creates a composite PK (use_composite_key), then forwards all security config to secure_table_provision as-is. The trigger never injects values the caller did not provide. Junction table FKs always CASCADE on delete. */
367
367
  export interface RelationManyToManyParams {
368
368
  source_table_id: string;
369
369
  target_table_id: string;
@@ -372,10 +372,9 @@ export interface RelationManyToManyParams {
372
372
  source_field_name?: string;
373
373
  target_field_name?: string;
374
374
  use_composite_key?: boolean;
375
- node_type?: string;
376
- node_data?: {
375
+ nodes?: {
377
376
  [key: string]: unknown;
378
- };
377
+ }[];
379
378
  grant_roles?: string[];
380
379
  grant_privileges?: string[][];
381
380
  policy_type?: string;
@@ -444,18 +443,18 @@ export interface BlueprintField {
444
443
  /** Comment/description for this field. */
445
444
  description?: string;
446
445
  }
447
- /** An RLS policy entry for a blueprint table. */
446
+ /** An RLS policy entry for a blueprint table. Uses $type to match the blueprint JSON convention. */
448
447
  export interface BlueprintPolicy {
449
448
  /** 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";
449
+ $type: "AuthzDirectOwner" | "AuthzDirectOwnerAny" | "AuthzMembership" | "AuthzEntityMembership" | "AuthzRelatedEntityMembership" | "AuthzOrgHierarchy" | "AuthzTemporal" | "AuthzPublishable" | "AuthzMemberList" | "AuthzRelatedMemberList" | "AuthzAllowAll" | "AuthzDenyAll" | "AuthzComposite" | "AuthzPeerOwnership" | "AuthzRelatedPeerOwnership";
450
+ /** Privileges this policy applies to (e.g., ["select"], ["insert", "update", "delete"]). */
451
+ privileges?: string[];
452
+ /** Whether this policy is permissive (true) or restrictive (false). Defaults to true. */
453
+ permissive?: boolean;
451
454
  /** Role for this policy. Defaults to "authenticated". */
452
455
  policy_role?: string;
453
- /** Whether this policy is permissive (true) or restrictive (false). */
454
- permissive?: boolean;
455
456
  /** Optional custom name for this policy. */
456
457
  policy_name?: string;
457
- /** Privileges this policy applies to. */
458
- privileges?: string[];
459
458
  /** Policy-specific data (structure varies by policy type). */
460
459
  data?: Record<string, unknown>;
461
460
  }
@@ -468,19 +467,49 @@ export interface BlueprintFtsSource {
468
467
  /** Language for text search. Defaults to "english". */
469
468
  lang?: string;
470
469
  }
471
- /** A full-text search configuration for a blueprint table. */
470
+ /** A full-text search configuration for a blueprint table (top-level, requires table_name). */
472
471
  export interface BlueprintFullTextSearch {
473
- /** Reference key of the table this full-text search belongs to. */
474
- table_ref: string;
472
+ /** Table name this full-text search belongs to. */
473
+ table_name: string;
474
+ /** Optional schema name for disambiguation (falls back to top-level default). */
475
+ schema_name?: string;
475
476
  /** Name of the tsvector field on the table. */
476
477
  field: string;
477
478
  /** Source fields that feed into this tsvector. */
478
479
  sources: BlueprintFtsSource[];
479
480
  }
480
- /** An index definition within a blueprint. */
481
+ /** A full-text search configuration nested inside a table definition (table_name not required). */
482
+ export interface BlueprintTableFullTextSearch {
483
+ /** Name of the tsvector field on the table. */
484
+ field: string;
485
+ /** Source fields that feed into this tsvector. */
486
+ sources: BlueprintFtsSource[];
487
+ /** Optional schema name override. */
488
+ schema_name?: string;
489
+ }
490
+ /** An index definition within a blueprint (top-level, requires table_name). */
481
491
  export interface BlueprintIndex {
482
- /** Reference key of the table this index belongs to. */
483
- table_ref: string;
492
+ /** Table name this index belongs to. */
493
+ table_name: string;
494
+ /** Optional schema name for disambiguation (falls back to top-level default). */
495
+ schema_name?: string;
496
+ /** Single column name for the index. */
497
+ column?: string;
498
+ /** Array of column names for a multi-column index. */
499
+ columns?: string[];
500
+ /** Index access method (e.g., "BTREE", "GIN", "GIST", "HNSW", "BM25"). */
501
+ access_method: string;
502
+ /** Whether this is a unique index. */
503
+ is_unique?: boolean;
504
+ /** Optional custom name for the index. */
505
+ name?: string;
506
+ /** Operator classes for the index columns. */
507
+ op_classes?: string[];
508
+ /** Additional index-specific options. */
509
+ options?: Record<string, unknown>;
510
+ }
511
+ /** An index definition nested inside a table definition (table_name not required). */
512
+ export interface BlueprintTableIndex {
484
513
  /** Single column name for the index. */
485
514
  column?: string;
486
515
  /** Array of column names for a multi-column index. */
@@ -495,6 +524,24 @@ export interface BlueprintIndex {
495
524
  op_classes?: string[];
496
525
  /** Additional index-specific options. */
497
526
  options?: Record<string, unknown>;
527
+ /** Optional schema name override. */
528
+ schema_name?: string;
529
+ }
530
+ /** A unique constraint definition within a blueprint (top-level, requires table_name). */
531
+ export interface BlueprintUniqueConstraint {
532
+ /** Table name this unique constraint belongs to. */
533
+ table_name: string;
534
+ /** Optional schema name for disambiguation (falls back to top-level default). */
535
+ schema_name?: string;
536
+ /** Column names that form the unique constraint. */
537
+ columns: string[];
538
+ }
539
+ /** A unique constraint nested inside a table definition (table_name not required). */
540
+ export interface BlueprintTableUniqueConstraint {
541
+ /** Column names that form the unique constraint. */
542
+ columns: string[];
543
+ /** Optional schema name override. */
544
+ schema_name?: string;
498
545
  }
499
546
  /** String shorthand -- just the node type name. */
500
547
  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 +681,35 @@ export type BlueprintNode = BlueprintNodeShorthand | BlueprintNodeObject;
634
681
  /** A relation entry in a blueprint definition. */
635
682
  export type BlueprintRelation = {
636
683
  $type: "RelationBelongsTo";
637
- source_ref: string;
638
- target_ref: string;
684
+ source_table: string;
685
+ target_table: string;
686
+ source_schema_name?: string;
687
+ target_schema_name?: string;
639
688
  } & Partial<RelationBelongsToParams> | {
640
689
  $type: "RelationHasOne";
641
- source_ref: string;
642
- target_ref: string;
690
+ source_table: string;
691
+ target_table: string;
692
+ source_schema_name?: string;
693
+ target_schema_name?: string;
643
694
  } & Partial<RelationHasOneParams> | {
644
695
  $type: "RelationHasMany";
645
- source_ref: string;
646
- target_ref: string;
696
+ source_table: string;
697
+ target_table: string;
698
+ source_schema_name?: string;
699
+ target_schema_name?: string;
647
700
  } & Partial<RelationHasManyParams> | {
648
701
  $type: "RelationManyToMany";
649
- source_ref: string;
650
- target_ref: string;
702
+ source_table: string;
703
+ target_table: string;
704
+ source_schema_name?: string;
705
+ target_schema_name?: string;
651
706
  } & Partial<RelationManyToManyParams>;
652
707
  /** A table definition within a blueprint. */
653
708
  export interface BlueprintTable {
654
- /** Local reference key for this table (used by relations, indexes, fts). */
655
- ref: string;
656
709
  /** The PostgreSQL table name to create. */
657
710
  table_name: string;
711
+ /** Optional schema name (falls back to top-level default). */
712
+ schema_name?: string;
658
713
  /** Array of node type entries that define the table's behavior. */
659
714
  nodes: BlueprintNode[];
660
715
  /** Custom fields (columns) to add to the table. */
@@ -667,6 +722,12 @@ export interface BlueprintTable {
667
722
  grants?: unknown[];
668
723
  /** Whether to enable RLS on this table. Defaults to true. */
669
724
  use_rls?: boolean;
725
+ /** Table-level indexes (table_name inherited from parent). */
726
+ indexes?: BlueprintTableIndex[];
727
+ /** Table-level full-text search configurations (table_name inherited from parent). */
728
+ full_text_searches?: BlueprintTableFullTextSearch[];
729
+ /** Table-level unique constraints (table_name inherited from parent). */
730
+ unique_constraints?: BlueprintTableUniqueConstraint[];
670
731
  }
671
732
  /** The complete blueprint definition -- the JSONB shape accepted by construct_blueprint(). */
672
733
  export interface BlueprintDefinition {
@@ -678,4 +739,6 @@ export interface BlueprintDefinition {
678
739
  indexes?: BlueprintIndex[];
679
740
  /** Full-text search configurations. */
680
741
  full_text_searches?: BlueprintFullTextSearch[];
742
+ /** Unique constraints on table columns. */
743
+ unique_constraints?: BlueprintUniqueConstraint[];
681
744
  }
@@ -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.',
@@ -3,7 +3,7 @@ export const RelationManyToMany = {
3
3
  "slug": "relation_many_to_many",
4
4
  "category": "relation",
5
5
  "display_name": "Many to Many",
6
- "description": "Creates a junction table between source and target tables with auto-derived naming and FK fields. The trigger creates a bare table (no implicit DataId or any node_type), adds FK fields to both tables, optionally creates a composite PK (use_composite_key), then forwards all security config to secure_table_provision as-is. The trigger never injects values the caller did not provide. Junction table FKs always CASCADE on delete.",
6
+ "description": "Creates a junction table between source and target tables with auto-derived naming and FK fields. The trigger creates a bare table (no implicit DataId), adds FK fields to both tables, optionally creates a composite PK (use_composite_key), then forwards all security config to secure_table_provision as-is. The trigger never injects values the caller did not provide. Junction table FKs always CASCADE on delete.",
7
7
  "parameter_schema": {
8
8
  "type": "object",
9
9
  "properties": {
@@ -36,16 +36,15 @@ export const RelationManyToMany = {
36
36
  },
37
37
  "use_composite_key": {
38
38
  "type": "boolean",
39
- "description": "When true, creates a composite PK from the two FK fields. When false, no PK is created by the trigger (use node_type=DataId for UUID PK). Mutually exclusive with node_type=DataId.",
39
+ "description": "When true, creates a composite PK from the two FK fields. When false, no PK is created by the trigger (use nodes with DataId for UUID PK). Mutually exclusive with nodes containing DataId.",
40
40
  "default": false
41
41
  },
42
- "node_type": {
43
- "type": "string",
44
- "description": "Generator for field creation on junction table. Forwarded to secure_table_provision as-is. Examples: DataId, DataEntityMembership, DataDirectOwner. NULL means no additional fields."
45
- },
46
- "node_data": {
47
- "type": "object",
48
- "description": "Configuration for the generator. Forwarded to secure_table_provision as-is. Only used when node_type is set."
42
+ "nodes": {
43
+ "type": "array",
44
+ "items": {
45
+ "type": "object"
46
+ },
47
+ "description": "Array of node objects for field creation on junction table. Each object has a $type key (e.g. DataId, DataEntityMembership) and optional data keys. Forwarded to secure_table_provision as-is. Empty array means no additional fields."
49
48
  },
50
49
  "grant_roles": {
51
50
  "type": "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-type-registry",
3
- "version": "0.7.1",
3
+ "version": "0.9.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": "d0d8f5ca5828ad3efba5f607bc699a8d520e4603"
51
+ "gitHead": "fc23b83307d007a14e54b1d0fc36614b9650a5dc"
52
52
  }
@@ -6,7 +6,7 @@ exports.RelationManyToMany = {
6
6
  "slug": "relation_many_to_many",
7
7
  "category": "relation",
8
8
  "display_name": "Many to Many",
9
- "description": "Creates a junction table between source and target tables with auto-derived naming and FK fields. The trigger creates a bare table (no implicit DataId or any node_type), adds FK fields to both tables, optionally creates a composite PK (use_composite_key), then forwards all security config to secure_table_provision as-is. The trigger never injects values the caller did not provide. Junction table FKs always CASCADE on delete.",
9
+ "description": "Creates a junction table between source and target tables with auto-derived naming and FK fields. The trigger creates a bare table (no implicit DataId), adds FK fields to both tables, optionally creates a composite PK (use_composite_key), then forwards all security config to secure_table_provision as-is. The trigger never injects values the caller did not provide. Junction table FKs always CASCADE on delete.",
10
10
  "parameter_schema": {
11
11
  "type": "object",
12
12
  "properties": {
@@ -39,16 +39,15 @@ exports.RelationManyToMany = {
39
39
  },
40
40
  "use_composite_key": {
41
41
  "type": "boolean",
42
- "description": "When true, creates a composite PK from the two FK fields. When false, no PK is created by the trigger (use node_type=DataId for UUID PK). Mutually exclusive with node_type=DataId.",
42
+ "description": "When true, creates a composite PK from the two FK fields. When false, no PK is created by the trigger (use nodes with DataId for UUID PK). Mutually exclusive with nodes containing DataId.",
43
43
  "default": false
44
44
  },
45
- "node_type": {
46
- "type": "string",
47
- "description": "Generator for field creation on junction table. Forwarded to secure_table_provision as-is. Examples: DataId, DataEntityMembership, DataDirectOwner. NULL means no additional fields."
48
- },
49
- "node_data": {
50
- "type": "object",
51
- "description": "Configuration for the generator. Forwarded to secure_table_provision as-is. Only used when node_type is set."
45
+ "nodes": {
46
+ "type": "array",
47
+ "items": {
48
+ "type": "object"
49
+ },
50
+ "description": "Array of node objects for field creation on junction table. Each object has a $type key (e.g. DataId, DataEntityMembership) and optional data keys. Forwarded to secure_table_provision as-is. Empty array means no additional fields."
52
51
  },
53
52
  "grant_roles": {
54
53
  "type": "array",