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 +10 -5
- package/blueprint-types.generated.d.ts +89 -26
- package/blueprint-types.generated.js +1 -1
- package/codegen/generate-seed.js +1 -1
- package/codegen/generate-types.d.ts +1 -1
- package/codegen/generate-types.js +62 -24
- package/esm/blueprint-types.generated.d.ts +89 -26
- package/esm/blueprint-types.generated.js +1 -1
- package/esm/codegen/generate-seed.js +1 -1
- package/esm/codegen/generate-types.d.ts +1 -1
- package/esm/codegen/generate-types.js +62 -24
- package/esm/relation/relation-many-to-many.js +8 -9
- package/package.json +2 -2
- package/relation/relation-many-to-many.js +8 -9
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
|
-
|
|
52
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
474
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
483
|
-
|
|
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
|
-
|
|
638
|
-
|
|
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
|
-
|
|
642
|
-
|
|
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
|
-
|
|
646
|
-
|
|
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
|
-
|
|
650
|
-
|
|
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
|
|
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.
|
package/codegen/generate-seed.js
CHANGED
|
@@ -128,7 +128,7 @@ const GENERATED_HEADER = [
|
|
|
128
128
|
'-- GENERATED FILE — DO NOT EDIT',
|
|
129
129
|
'--',
|
|
130
130
|
'-- Regenerate with:',
|
|
131
|
-
'-- cd
|
|
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,
|
|
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,
|
|
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,
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
343
|
-
requiredProp('
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
474
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
483
|
-
|
|
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
|
-
|
|
638
|
-
|
|
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
|
-
|
|
642
|
-
|
|
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
|
-
|
|
646
|
-
|
|
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
|
-
|
|
650
|
-
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
308
|
-
requiredProp('
|
|
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
|
|
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
|
|
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
|
|
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
|
-
"
|
|
43
|
-
"type": "
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"
|
|
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.
|
|
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": "
|
|
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
|
|
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
|
|
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
|
-
"
|
|
46
|
-
"type": "
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"
|
|
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",
|