node-type-registry 0.10.1 → 0.12.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 +0 -8
- package/authz/authz-entity-membership.js +5 -1
- package/authz/authz-membership-check.js +6 -4
- package/authz/authz-not-read-only.d.ts +2 -0
- package/authz/authz-not-read-only.js +34 -0
- package/authz/authz-peer-ownership.js +5 -1
- package/authz/authz-related-entity-membership.js +5 -1
- package/authz/authz-related-peer-ownership.js +5 -1
- package/authz/index.d.ts +1 -0
- package/authz/index.js +3 -1
- package/blueprint-types.generated.d.ts +41 -3
- package/codegen/generate-types.js +16 -0
- package/esm/authz/authz-entity-membership.js +5 -1
- package/esm/authz/authz-membership-check.js +6 -4
- package/esm/authz/authz-not-read-only.d.ts +2 -0
- package/esm/authz/authz-not-read-only.js +31 -0
- package/esm/authz/authz-peer-ownership.js +5 -1
- package/esm/authz/authz-related-entity-membership.js +5 -1
- package/esm/authz/authz-related-peer-ownership.js +5 -1
- package/esm/authz/index.d.ts +1 -0
- package/esm/authz/index.js +1 -0
- package/esm/blueprint-types.generated.d.ts +41 -3
- package/esm/codegen/generate-types.js +16 -0
- package/package.json +2 -3
- package/codegen/generate-seed.d.ts +0 -32
- package/codegen/generate-seed.js +0 -252
- package/esm/codegen/generate-seed.d.ts +0 -32
- package/esm/codegen/generate-seed.js +0 -250
package/README.md
CHANGED
|
@@ -83,14 +83,6 @@ cd graphql/node-type-registry && pnpm generate:types
|
|
|
83
83
|
|
|
84
84
|
This produces `src/blueprint-types.generated.ts` from the TS node type source of truth.
|
|
85
85
|
|
|
86
|
-
## Codegen: SQL seed
|
|
87
|
-
|
|
88
|
-
Generate SQL seed scripts for `node_type_registry` table:
|
|
89
|
-
|
|
90
|
-
```bash
|
|
91
|
-
cd graphql/node-type-registry && pnpm generate:seed --pgpm ../../constructive-db/packages/metaschema
|
|
92
|
-
```
|
|
93
|
-
|
|
94
86
|
---
|
|
95
87
|
|
|
96
88
|
## Education and Tutorials
|
|
@@ -19,7 +19,11 @@ exports.AuthzEntityMembership = {
|
|
|
19
19
|
"integer",
|
|
20
20
|
"string"
|
|
21
21
|
],
|
|
22
|
-
"description": "Scope: 1=app, 2=org, 3
|
|
22
|
+
"description": "Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)"
|
|
23
|
+
},
|
|
24
|
+
"entity_type": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "Entity type prefix (e.g. 'channel', 'department'). Resolved to membership_type integer via memberships_module lookup. Use instead of membership_type for readability."
|
|
23
27
|
},
|
|
24
28
|
"permission": {
|
|
25
29
|
"type": "string",
|
|
@@ -15,7 +15,11 @@ exports.AuthzMembership = {
|
|
|
15
15
|
"integer",
|
|
16
16
|
"string"
|
|
17
17
|
],
|
|
18
|
-
"description": "Scope: 1=app, 2=org, 3
|
|
18
|
+
"description": "Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)"
|
|
19
|
+
},
|
|
20
|
+
"entity_type": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "Entity type prefix (e.g. 'channel', 'department'). Resolved to membership_type integer via memberships_module lookup. Use instead of membership_type for readability."
|
|
19
23
|
},
|
|
20
24
|
"permission": {
|
|
21
25
|
"type": "string",
|
|
@@ -37,9 +41,7 @@ exports.AuthzMembership = {
|
|
|
37
41
|
"description": "If true, require is_owner flag"
|
|
38
42
|
}
|
|
39
43
|
},
|
|
40
|
-
"required": [
|
|
41
|
-
"membership_type"
|
|
42
|
-
]
|
|
44
|
+
"required": []
|
|
43
45
|
},
|
|
44
46
|
"tags": [
|
|
45
47
|
"membership",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AuthzNotReadOnly = void 0;
|
|
4
|
+
exports.AuthzNotReadOnly = {
|
|
5
|
+
"name": "AuthzNotReadOnly",
|
|
6
|
+
"slug": "authz_not_read_only",
|
|
7
|
+
"category": "authz",
|
|
8
|
+
"display_name": "Not Read-Only",
|
|
9
|
+
"description": "Restrictive policy that blocks read-only members from mutations. Checks actor_id + is_read_only IS NOT TRUE on the SPRT. Designed to run as a restrictive counterpart after a permissive AuthzEntityMembership policy has already verified membership.",
|
|
10
|
+
"parameter_schema": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"properties": {
|
|
13
|
+
"entity_field": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"description": "Column name referencing the entity (e.g., entity_id, org_id)"
|
|
16
|
+
},
|
|
17
|
+
"membership_type": {
|
|
18
|
+
"type": [
|
|
19
|
+
"integer",
|
|
20
|
+
"string"
|
|
21
|
+
],
|
|
22
|
+
"description": "Scope: 2=org, 3+=dynamic entity types. Must be >= 2 (entity-scoped)."
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"required": [
|
|
26
|
+
"entity_field"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
"tags": [
|
|
30
|
+
"membership",
|
|
31
|
+
"authz",
|
|
32
|
+
"restrictive"
|
|
33
|
+
]
|
|
34
|
+
};
|
|
@@ -19,7 +19,11 @@ exports.AuthzPeerOwnership = {
|
|
|
19
19
|
"integer",
|
|
20
20
|
"string"
|
|
21
21
|
],
|
|
22
|
-
"description": "Scope: 1=app, 2=org, 3
|
|
22
|
+
"description": "Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)"
|
|
23
|
+
},
|
|
24
|
+
"entity_type": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "Entity type prefix (e.g. 'channel', 'department'). Resolved to membership_type integer via memberships_module lookup. Use instead of membership_type for readability."
|
|
23
27
|
},
|
|
24
28
|
"permission": {
|
|
25
29
|
"type": "string",
|
|
@@ -19,7 +19,11 @@ exports.AuthzRelatedEntityMembership = {
|
|
|
19
19
|
"integer",
|
|
20
20
|
"string"
|
|
21
21
|
],
|
|
22
|
-
"description": "Scope: 1=app, 2=org, 3
|
|
22
|
+
"description": "Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)"
|
|
23
|
+
},
|
|
24
|
+
"entity_type": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "Entity type prefix (e.g. 'channel', 'department'). Resolved to membership_type integer via memberships_module lookup. Use instead of membership_type for readability."
|
|
23
27
|
},
|
|
24
28
|
"obj_table_id": {
|
|
25
29
|
"type": "string",
|
|
@@ -19,7 +19,11 @@ exports.AuthzRelatedPeerOwnership = {
|
|
|
19
19
|
"integer",
|
|
20
20
|
"string"
|
|
21
21
|
],
|
|
22
|
-
"description": "Scope: 1=app, 2=org, 3
|
|
22
|
+
"description": "Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)"
|
|
23
|
+
},
|
|
24
|
+
"entity_type": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "Entity type prefix (e.g. 'channel', 'department'). Resolved to membership_type integer via memberships_module lookup. Use instead of membership_type for readability."
|
|
23
27
|
},
|
|
24
28
|
"obj_table_id": {
|
|
25
29
|
"type": "string",
|
package/authz/index.d.ts
CHANGED
|
@@ -11,5 +11,6 @@ export { AuthzRelatedMemberList } from './authz-related-member-list';
|
|
|
11
11
|
export { AuthzAllowAll } from './authz-allow-all';
|
|
12
12
|
export { AuthzDenyAll } from './authz-deny-all';
|
|
13
13
|
export { AuthzComposite } from './authz-composite';
|
|
14
|
+
export { AuthzNotReadOnly } from './authz-not-read-only';
|
|
14
15
|
export { AuthzPeerOwnership } from './authz-peer-ownership';
|
|
15
16
|
export { AuthzRelatedPeerOwnership } from './authz-related-peer-ownership';
|
package/authz/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.AuthzRelatedPeerOwnership = exports.AuthzPeerOwnership = exports.AuthzComposite = exports.AuthzDenyAll = exports.AuthzAllowAll = exports.AuthzRelatedMemberList = exports.AuthzMemberList = exports.AuthzPublishable = exports.AuthzTemporal = exports.AuthzOrgHierarchy = exports.AuthzRelatedEntityMembership = exports.AuthzEntityMembership = exports.AuthzMembership = exports.AuthzDirectOwnerAny = exports.AuthzDirectOwner = void 0;
|
|
3
|
+
exports.AuthzRelatedPeerOwnership = exports.AuthzPeerOwnership = exports.AuthzNotReadOnly = exports.AuthzComposite = exports.AuthzDenyAll = exports.AuthzAllowAll = exports.AuthzRelatedMemberList = exports.AuthzMemberList = exports.AuthzPublishable = exports.AuthzTemporal = exports.AuthzOrgHierarchy = exports.AuthzRelatedEntityMembership = exports.AuthzEntityMembership = exports.AuthzMembership = exports.AuthzDirectOwnerAny = exports.AuthzDirectOwner = void 0;
|
|
4
4
|
var authz_direct_owner_1 = require("./authz-direct-owner");
|
|
5
5
|
Object.defineProperty(exports, "AuthzDirectOwner", { enumerable: true, get: function () { return authz_direct_owner_1.AuthzDirectOwner; } });
|
|
6
6
|
var authz_direct_owner_any_1 = require("./authz-direct-owner-any");
|
|
@@ -27,6 +27,8 @@ var authz_deny_all_1 = require("./authz-deny-all");
|
|
|
27
27
|
Object.defineProperty(exports, "AuthzDenyAll", { enumerable: true, get: function () { return authz_deny_all_1.AuthzDenyAll; } });
|
|
28
28
|
var authz_composite_1 = require("./authz-composite");
|
|
29
29
|
Object.defineProperty(exports, "AuthzComposite", { enumerable: true, get: function () { return authz_composite_1.AuthzComposite; } });
|
|
30
|
+
var authz_not_read_only_1 = require("./authz-not-read-only");
|
|
31
|
+
Object.defineProperty(exports, "AuthzNotReadOnly", { enumerable: true, get: function () { return authz_not_read_only_1.AuthzNotReadOnly; } });
|
|
30
32
|
var authz_peer_ownership_1 = require("./authz-peer-ownership");
|
|
31
33
|
Object.defineProperty(exports, "AuthzPeerOwnership", { enumerable: true, get: function () { return authz_peer_ownership_1.AuthzPeerOwnership; } });
|
|
32
34
|
var authz_related_peer_ownership_1 = require("./authz-related-peer-ownership");
|
|
@@ -247,7 +247,8 @@ export interface AuthzDirectOwnerAnyParams {
|
|
|
247
247
|
}
|
|
248
248
|
/** Membership check that verifies the user has membership (optionally with specific permission) without binding to any entity from the row. Uses EXISTS subquery against SPRT table. */
|
|
249
249
|
export interface AuthzMembershipParams {
|
|
250
|
-
membership_type
|
|
250
|
+
membership_type?: number | string;
|
|
251
|
+
entity_type?: string;
|
|
251
252
|
permission?: string;
|
|
252
253
|
permissions?: string[];
|
|
253
254
|
is_admin?: boolean;
|
|
@@ -257,6 +258,7 @@ export interface AuthzMembershipParams {
|
|
|
257
258
|
export interface AuthzEntityMembershipParams {
|
|
258
259
|
entity_field: string;
|
|
259
260
|
membership_type?: number | string;
|
|
261
|
+
entity_type?: string;
|
|
260
262
|
permission?: string;
|
|
261
263
|
permissions?: string[];
|
|
262
264
|
is_admin?: boolean;
|
|
@@ -266,6 +268,7 @@ export interface AuthzEntityMembershipParams {
|
|
|
266
268
|
export interface AuthzRelatedEntityMembershipParams {
|
|
267
269
|
entity_field: string;
|
|
268
270
|
membership_type?: number | string;
|
|
271
|
+
entity_type?: string;
|
|
269
272
|
obj_table_id?: string;
|
|
270
273
|
obj_schema?: string;
|
|
271
274
|
obj_table?: string;
|
|
@@ -321,10 +324,16 @@ export interface AuthzCompositeParams {
|
|
|
321
324
|
}[];
|
|
322
325
|
};
|
|
323
326
|
}
|
|
327
|
+
/** Restrictive policy that blocks read-only members from mutations. Checks actor_id + is_read_only IS NOT TRUE on the SPRT. Designed to run as a restrictive counterpart after a permissive AuthzEntityMembership policy has already verified membership. */
|
|
328
|
+
export interface AuthzNotReadOnlyParams {
|
|
329
|
+
entity_field: string;
|
|
330
|
+
membership_type?: number | string;
|
|
331
|
+
}
|
|
324
332
|
/** Peer visibility through shared entity membership. Authorizes access to user-owned rows when the owner and current user are both members of the same entity. Self-joins the SPRT table to find peers. */
|
|
325
333
|
export interface AuthzPeerOwnershipParams {
|
|
326
334
|
owner_field: string;
|
|
327
335
|
membership_type?: number | string;
|
|
336
|
+
entity_type?: string;
|
|
328
337
|
permission?: string;
|
|
329
338
|
permissions?: string[];
|
|
330
339
|
is_admin?: boolean;
|
|
@@ -334,6 +343,7 @@ export interface AuthzPeerOwnershipParams {
|
|
|
334
343
|
export interface AuthzRelatedPeerOwnershipParams {
|
|
335
344
|
entity_field: string;
|
|
336
345
|
membership_type?: number | string;
|
|
346
|
+
entity_type?: string;
|
|
337
347
|
obj_table_id?: string;
|
|
338
348
|
obj_schema?: string;
|
|
339
349
|
obj_table?: string;
|
|
@@ -452,7 +462,7 @@ export interface BlueprintField {
|
|
|
452
462
|
/** An RLS policy entry for a blueprint table. Uses $type to match the blueprint JSON convention. */
|
|
453
463
|
export interface BlueprintPolicy {
|
|
454
464
|
/** Authz* policy type name (e.g., "AuthzDirectOwner", "AuthzAllowAll"). */
|
|
455
|
-
$type: "AuthzDirectOwner" | "AuthzDirectOwnerAny" | "AuthzMembership" | "AuthzEntityMembership" | "AuthzRelatedEntityMembership" | "AuthzOrgHierarchy" | "AuthzTemporal" | "AuthzPublishable" | "AuthzMemberList" | "AuthzRelatedMemberList" | "AuthzAllowAll" | "AuthzDenyAll" | "AuthzComposite" | "AuthzPeerOwnership" | "AuthzRelatedPeerOwnership";
|
|
465
|
+
$type: "AuthzDirectOwner" | "AuthzDirectOwnerAny" | "AuthzMembership" | "AuthzEntityMembership" | "AuthzRelatedEntityMembership" | "AuthzOrgHierarchy" | "AuthzTemporal" | "AuthzPublishable" | "AuthzMemberList" | "AuthzRelatedMemberList" | "AuthzAllowAll" | "AuthzDenyAll" | "AuthzComposite" | "AuthzNotReadOnly" | "AuthzPeerOwnership" | "AuthzRelatedPeerOwnership";
|
|
456
466
|
/** Privileges this policy applies to (e.g., ["select"], ["insert", "update", "delete"]). */
|
|
457
467
|
privileges?: string[];
|
|
458
468
|
/** Whether this policy is permissive (true) or restrictive (false). Defaults to true. */
|
|
@@ -549,8 +559,31 @@ export interface BlueprintTableUniqueConstraint {
|
|
|
549
559
|
/** Optional schema name override. */
|
|
550
560
|
schema_name?: string;
|
|
551
561
|
}
|
|
562
|
+
/** A membership type entry for Phase 0 of construct_blueprint(). Provisions a full entity type with its own entity table, membership modules, and security policies via entity_type_provision. */
|
|
563
|
+
export interface BlueprintMembershipType {
|
|
564
|
+
/** Entity type name (e.g., "data_room", "channel", "department"). Must be unique per database. */
|
|
565
|
+
name: string;
|
|
566
|
+
/** Short prefix for generated objects (e.g., "dr", "ch", "dept"). Used in table/trigger naming. */
|
|
567
|
+
prefix: string;
|
|
568
|
+
/** Human-readable description of this entity type. */
|
|
569
|
+
description?: string;
|
|
570
|
+
/** Parent entity type name. Defaults to "org". */
|
|
571
|
+
parent_entity?: string;
|
|
572
|
+
/** Custom table name for the entity table. Defaults to name-derived convention. */
|
|
573
|
+
table_name?: string;
|
|
574
|
+
/** Whether this entity type is visible in the API. Defaults to true. */
|
|
575
|
+
is_visible?: boolean;
|
|
576
|
+
/** Whether to provision a limits module for this entity type. Defaults to false. */
|
|
577
|
+
has_limits?: boolean;
|
|
578
|
+
/** Whether to provision a profiles module for this entity type. Defaults to false. */
|
|
579
|
+
has_profiles?: boolean;
|
|
580
|
+
/** Whether to provision a levels module for this entity type. Defaults to false. */
|
|
581
|
+
has_levels?: boolean;
|
|
582
|
+
/** Whether to skip creating default RLS policies on the entity table. Defaults to false. */
|
|
583
|
+
skip_entity_policies?: boolean;
|
|
584
|
+
}
|
|
552
585
|
/** String shorthand -- just the node type name. */
|
|
553
|
-
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" | "SearchVector" | "SearchFullText" | "SearchBm25" | "SearchUnified" | "SearchSpatial" | "SearchSpatialAggregate" | "DataJobTrigger" | "DataTags" | "DataStatusField" | "DataJsonb" | "SearchTrgm" | "DataSlug" | "DataInflection" | "DataOwnedFields" | "DataInheritFromParent" | "DataForceCurrentUser" | "DataImmutableFields" | "DataCompositeField" | "TableUserProfiles" | "TableOrganizationSettings" | "TableUserSettings";
|
|
586
|
+
export type BlueprintNodeShorthand = "AuthzDirectOwner" | "AuthzDirectOwnerAny" | "AuthzMembership" | "AuthzEntityMembership" | "AuthzRelatedEntityMembership" | "AuthzOrgHierarchy" | "AuthzTemporal" | "AuthzPublishable" | "AuthzMemberList" | "AuthzRelatedMemberList" | "AuthzAllowAll" | "AuthzDenyAll" | "AuthzComposite" | "AuthzNotReadOnly" | "AuthzPeerOwnership" | "AuthzRelatedPeerOwnership" | "DataId" | "DataDirectOwner" | "DataEntityMembership" | "DataOwnershipInEntity" | "DataTimestamps" | "DataPeoplestamps" | "DataPublishable" | "DataSoftDelete" | "SearchVector" | "SearchFullText" | "SearchBm25" | "SearchUnified" | "SearchSpatial" | "SearchSpatialAggregate" | "DataJobTrigger" | "DataTags" | "DataStatusField" | "DataJsonb" | "SearchTrgm" | "DataSlug" | "DataInflection" | "DataOwnedFields" | "DataInheritFromParent" | "DataForceCurrentUser" | "DataImmutableFields" | "DataCompositeField" | "TableUserProfiles" | "TableOrganizationSettings" | "TableUserSettings";
|
|
554
587
|
/** Object form -- { $type, data } with typed parameters. */
|
|
555
588
|
export type BlueprintNodeObject = {
|
|
556
589
|
$type: "AuthzDirectOwner";
|
|
@@ -591,6 +624,9 @@ export type BlueprintNodeObject = {
|
|
|
591
624
|
} | {
|
|
592
625
|
$type: "AuthzComposite";
|
|
593
626
|
data: AuthzCompositeParams;
|
|
627
|
+
} | {
|
|
628
|
+
$type: "AuthzNotReadOnly";
|
|
629
|
+
data: AuthzNotReadOnlyParams;
|
|
594
630
|
} | {
|
|
595
631
|
$type: "AuthzPeerOwnership";
|
|
596
632
|
data: AuthzPeerOwnershipParams;
|
|
@@ -750,4 +786,6 @@ export interface BlueprintDefinition {
|
|
|
750
786
|
full_text_searches?: BlueprintFullTextSearch[];
|
|
751
787
|
/** Unique constraints on table columns. */
|
|
752
788
|
unique_constraints?: BlueprintUniqueConstraint[];
|
|
789
|
+
/** Entity types to provision in Phase 0 (before tables). Each entry creates an entity table with membership modules and security. */
|
|
790
|
+
membership_types?: BlueprintMembershipType[];
|
|
753
791
|
}
|
|
@@ -384,6 +384,20 @@ function buildBlueprintTableUniqueConstraint() {
|
|
|
384
384
|
addJSDoc(optionalProp('schema_name', t.tsStringKeyword()), 'Optional schema name override.'),
|
|
385
385
|
]), 'A unique constraint nested inside a table definition (table_name not required).');
|
|
386
386
|
}
|
|
387
|
+
function buildBlueprintMembershipType() {
|
|
388
|
+
return addJSDoc(exportInterface('BlueprintMembershipType', [
|
|
389
|
+
addJSDoc(requiredProp('name', t.tsStringKeyword()), 'Entity type name (e.g., "data_room", "channel", "department"). Must be unique per database.'),
|
|
390
|
+
addJSDoc(requiredProp('prefix', t.tsStringKeyword()), 'Short prefix for generated objects (e.g., "dr", "ch", "dept"). Used in table/trigger naming.'),
|
|
391
|
+
addJSDoc(optionalProp('description', t.tsStringKeyword()), 'Human-readable description of this entity type.'),
|
|
392
|
+
addJSDoc(optionalProp('parent_entity', t.tsStringKeyword()), 'Parent entity type name. Defaults to "org".'),
|
|
393
|
+
addJSDoc(optionalProp('table_name', t.tsStringKeyword()), 'Custom table name for the entity table. Defaults to name-derived convention.'),
|
|
394
|
+
addJSDoc(optionalProp('is_visible', t.tsBooleanKeyword()), 'Whether this entity type is visible in the API. Defaults to true.'),
|
|
395
|
+
addJSDoc(optionalProp('has_limits', t.tsBooleanKeyword()), 'Whether to provision a limits module for this entity type. Defaults to false.'),
|
|
396
|
+
addJSDoc(optionalProp('has_profiles', t.tsBooleanKeyword()), 'Whether to provision a profiles module for this entity type. Defaults to false.'),
|
|
397
|
+
addJSDoc(optionalProp('has_levels', t.tsBooleanKeyword()), 'Whether to provision a levels module for this entity type. Defaults to false.'),
|
|
398
|
+
addJSDoc(optionalProp('skip_entity_policies', t.tsBooleanKeyword()), 'Whether to skip creating default RLS policies on the entity table. Defaults to false.'),
|
|
399
|
+
]), 'A membership type entry for Phase 0 of construct_blueprint(). Provisions a full entity type with its own entity table, membership modules, and security policies via entity_type_provision.');
|
|
400
|
+
}
|
|
387
401
|
function buildBlueprintTable() {
|
|
388
402
|
return addJSDoc(exportInterface('BlueprintTable', [
|
|
389
403
|
addJSDoc(requiredProp('table_name', t.tsStringKeyword()), 'The PostgreSQL table name to create.'),
|
|
@@ -406,6 +420,7 @@ function buildBlueprintDefinition() {
|
|
|
406
420
|
addJSDoc(optionalProp('indexes', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintIndex')))), 'Indexes on table columns.'),
|
|
407
421
|
addJSDoc(optionalProp('full_text_searches', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintFullTextSearch')))), 'Full-text search configurations.'),
|
|
408
422
|
addJSDoc(optionalProp('unique_constraints', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintUniqueConstraint')))), 'Unique constraints on table columns.'),
|
|
423
|
+
addJSDoc(optionalProp('membership_types', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintMembershipType')))), 'Entity types to provision in Phase 0 (before tables). Each entry creates an entity table with membership modules and security.'),
|
|
409
424
|
]), 'The complete blueprint definition -- the JSONB shape accepted by construct_blueprint().');
|
|
410
425
|
}
|
|
411
426
|
// ---------------------------------------------------------------------------
|
|
@@ -459,6 +474,7 @@ function buildProgram(meta) {
|
|
|
459
474
|
statements.push(buildBlueprintTableIndex());
|
|
460
475
|
statements.push(buildBlueprintUniqueConstraint());
|
|
461
476
|
statements.push(buildBlueprintTableUniqueConstraint());
|
|
477
|
+
statements.push(buildBlueprintMembershipType());
|
|
462
478
|
// -- Node types discriminated union --
|
|
463
479
|
statements.push(sectionComment('Node types -- discriminated union for nodes[] entries'));
|
|
464
480
|
statements.push(...buildNodeTypes(dataNodes));
|
|
@@ -16,7 +16,11 @@ export const AuthzEntityMembership = {
|
|
|
16
16
|
"integer",
|
|
17
17
|
"string"
|
|
18
18
|
],
|
|
19
|
-
"description": "Scope: 1=app, 2=org, 3
|
|
19
|
+
"description": "Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)"
|
|
20
|
+
},
|
|
21
|
+
"entity_type": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"description": "Entity type prefix (e.g. 'channel', 'department'). Resolved to membership_type integer via memberships_module lookup. Use instead of membership_type for readability."
|
|
20
24
|
},
|
|
21
25
|
"permission": {
|
|
22
26
|
"type": "string",
|
|
@@ -12,7 +12,11 @@ export const AuthzMembership = {
|
|
|
12
12
|
"integer",
|
|
13
13
|
"string"
|
|
14
14
|
],
|
|
15
|
-
"description": "Scope: 1=app, 2=org, 3
|
|
15
|
+
"description": "Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)"
|
|
16
|
+
},
|
|
17
|
+
"entity_type": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"description": "Entity type prefix (e.g. 'channel', 'department'). Resolved to membership_type integer via memberships_module lookup. Use instead of membership_type for readability."
|
|
16
20
|
},
|
|
17
21
|
"permission": {
|
|
18
22
|
"type": "string",
|
|
@@ -34,9 +38,7 @@ export const AuthzMembership = {
|
|
|
34
38
|
"description": "If true, require is_owner flag"
|
|
35
39
|
}
|
|
36
40
|
},
|
|
37
|
-
"required": [
|
|
38
|
-
"membership_type"
|
|
39
|
-
]
|
|
41
|
+
"required": []
|
|
40
42
|
},
|
|
41
43
|
"tags": [
|
|
42
44
|
"membership",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const AuthzNotReadOnly = {
|
|
2
|
+
"name": "AuthzNotReadOnly",
|
|
3
|
+
"slug": "authz_not_read_only",
|
|
4
|
+
"category": "authz",
|
|
5
|
+
"display_name": "Not Read-Only",
|
|
6
|
+
"description": "Restrictive policy that blocks read-only members from mutations. Checks actor_id + is_read_only IS NOT TRUE on the SPRT. Designed to run as a restrictive counterpart after a permissive AuthzEntityMembership policy has already verified membership.",
|
|
7
|
+
"parameter_schema": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"properties": {
|
|
10
|
+
"entity_field": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"description": "Column name referencing the entity (e.g., entity_id, org_id)"
|
|
13
|
+
},
|
|
14
|
+
"membership_type": {
|
|
15
|
+
"type": [
|
|
16
|
+
"integer",
|
|
17
|
+
"string"
|
|
18
|
+
],
|
|
19
|
+
"description": "Scope: 2=org, 3+=dynamic entity types. Must be >= 2 (entity-scoped)."
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"required": [
|
|
23
|
+
"entity_field"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"tags": [
|
|
27
|
+
"membership",
|
|
28
|
+
"authz",
|
|
29
|
+
"restrictive"
|
|
30
|
+
]
|
|
31
|
+
};
|
|
@@ -16,7 +16,11 @@ export const AuthzPeerOwnership = {
|
|
|
16
16
|
"integer",
|
|
17
17
|
"string"
|
|
18
18
|
],
|
|
19
|
-
"description": "Scope: 1=app, 2=org, 3
|
|
19
|
+
"description": "Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)"
|
|
20
|
+
},
|
|
21
|
+
"entity_type": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"description": "Entity type prefix (e.g. 'channel', 'department'). Resolved to membership_type integer via memberships_module lookup. Use instead of membership_type for readability."
|
|
20
24
|
},
|
|
21
25
|
"permission": {
|
|
22
26
|
"type": "string",
|
|
@@ -16,7 +16,11 @@ export const AuthzRelatedEntityMembership = {
|
|
|
16
16
|
"integer",
|
|
17
17
|
"string"
|
|
18
18
|
],
|
|
19
|
-
"description": "Scope: 1=app, 2=org, 3
|
|
19
|
+
"description": "Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)"
|
|
20
|
+
},
|
|
21
|
+
"entity_type": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"description": "Entity type prefix (e.g. 'channel', 'department'). Resolved to membership_type integer via memberships_module lookup. Use instead of membership_type for readability."
|
|
20
24
|
},
|
|
21
25
|
"obj_table_id": {
|
|
22
26
|
"type": "string",
|
|
@@ -16,7 +16,11 @@ export const AuthzRelatedPeerOwnership = {
|
|
|
16
16
|
"integer",
|
|
17
17
|
"string"
|
|
18
18
|
],
|
|
19
|
-
"description": "Scope: 1=app, 2=org, 3
|
|
19
|
+
"description": "Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)"
|
|
20
|
+
},
|
|
21
|
+
"entity_type": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"description": "Entity type prefix (e.g. 'channel', 'department'). Resolved to membership_type integer via memberships_module lookup. Use instead of membership_type for readability."
|
|
20
24
|
},
|
|
21
25
|
"obj_table_id": {
|
|
22
26
|
"type": "string",
|
package/esm/authz/index.d.ts
CHANGED
|
@@ -11,5 +11,6 @@ export { AuthzRelatedMemberList } from './authz-related-member-list';
|
|
|
11
11
|
export { AuthzAllowAll } from './authz-allow-all';
|
|
12
12
|
export { AuthzDenyAll } from './authz-deny-all';
|
|
13
13
|
export { AuthzComposite } from './authz-composite';
|
|
14
|
+
export { AuthzNotReadOnly } from './authz-not-read-only';
|
|
14
15
|
export { AuthzPeerOwnership } from './authz-peer-ownership';
|
|
15
16
|
export { AuthzRelatedPeerOwnership } from './authz-related-peer-ownership';
|
package/esm/authz/index.js
CHANGED
|
@@ -11,5 +11,6 @@ export { AuthzRelatedMemberList } from './authz-related-member-list';
|
|
|
11
11
|
export { AuthzAllowAll } from './authz-allow-all';
|
|
12
12
|
export { AuthzDenyAll } from './authz-deny-all';
|
|
13
13
|
export { AuthzComposite } from './authz-composite';
|
|
14
|
+
export { AuthzNotReadOnly } from './authz-not-read-only';
|
|
14
15
|
export { AuthzPeerOwnership } from './authz-peer-ownership';
|
|
15
16
|
export { AuthzRelatedPeerOwnership } from './authz-related-peer-ownership';
|
|
@@ -247,7 +247,8 @@ export interface AuthzDirectOwnerAnyParams {
|
|
|
247
247
|
}
|
|
248
248
|
/** Membership check that verifies the user has membership (optionally with specific permission) without binding to any entity from the row. Uses EXISTS subquery against SPRT table. */
|
|
249
249
|
export interface AuthzMembershipParams {
|
|
250
|
-
membership_type
|
|
250
|
+
membership_type?: number | string;
|
|
251
|
+
entity_type?: string;
|
|
251
252
|
permission?: string;
|
|
252
253
|
permissions?: string[];
|
|
253
254
|
is_admin?: boolean;
|
|
@@ -257,6 +258,7 @@ export interface AuthzMembershipParams {
|
|
|
257
258
|
export interface AuthzEntityMembershipParams {
|
|
258
259
|
entity_field: string;
|
|
259
260
|
membership_type?: number | string;
|
|
261
|
+
entity_type?: string;
|
|
260
262
|
permission?: string;
|
|
261
263
|
permissions?: string[];
|
|
262
264
|
is_admin?: boolean;
|
|
@@ -266,6 +268,7 @@ export interface AuthzEntityMembershipParams {
|
|
|
266
268
|
export interface AuthzRelatedEntityMembershipParams {
|
|
267
269
|
entity_field: string;
|
|
268
270
|
membership_type?: number | string;
|
|
271
|
+
entity_type?: string;
|
|
269
272
|
obj_table_id?: string;
|
|
270
273
|
obj_schema?: string;
|
|
271
274
|
obj_table?: string;
|
|
@@ -321,10 +324,16 @@ export interface AuthzCompositeParams {
|
|
|
321
324
|
}[];
|
|
322
325
|
};
|
|
323
326
|
}
|
|
327
|
+
/** Restrictive policy that blocks read-only members from mutations. Checks actor_id + is_read_only IS NOT TRUE on the SPRT. Designed to run as a restrictive counterpart after a permissive AuthzEntityMembership policy has already verified membership. */
|
|
328
|
+
export interface AuthzNotReadOnlyParams {
|
|
329
|
+
entity_field: string;
|
|
330
|
+
membership_type?: number | string;
|
|
331
|
+
}
|
|
324
332
|
/** Peer visibility through shared entity membership. Authorizes access to user-owned rows when the owner and current user are both members of the same entity. Self-joins the SPRT table to find peers. */
|
|
325
333
|
export interface AuthzPeerOwnershipParams {
|
|
326
334
|
owner_field: string;
|
|
327
335
|
membership_type?: number | string;
|
|
336
|
+
entity_type?: string;
|
|
328
337
|
permission?: string;
|
|
329
338
|
permissions?: string[];
|
|
330
339
|
is_admin?: boolean;
|
|
@@ -334,6 +343,7 @@ export interface AuthzPeerOwnershipParams {
|
|
|
334
343
|
export interface AuthzRelatedPeerOwnershipParams {
|
|
335
344
|
entity_field: string;
|
|
336
345
|
membership_type?: number | string;
|
|
346
|
+
entity_type?: string;
|
|
337
347
|
obj_table_id?: string;
|
|
338
348
|
obj_schema?: string;
|
|
339
349
|
obj_table?: string;
|
|
@@ -452,7 +462,7 @@ export interface BlueprintField {
|
|
|
452
462
|
/** An RLS policy entry for a blueprint table. Uses $type to match the blueprint JSON convention. */
|
|
453
463
|
export interface BlueprintPolicy {
|
|
454
464
|
/** Authz* policy type name (e.g., "AuthzDirectOwner", "AuthzAllowAll"). */
|
|
455
|
-
$type: "AuthzDirectOwner" | "AuthzDirectOwnerAny" | "AuthzMembership" | "AuthzEntityMembership" | "AuthzRelatedEntityMembership" | "AuthzOrgHierarchy" | "AuthzTemporal" | "AuthzPublishable" | "AuthzMemberList" | "AuthzRelatedMemberList" | "AuthzAllowAll" | "AuthzDenyAll" | "AuthzComposite" | "AuthzPeerOwnership" | "AuthzRelatedPeerOwnership";
|
|
465
|
+
$type: "AuthzDirectOwner" | "AuthzDirectOwnerAny" | "AuthzMembership" | "AuthzEntityMembership" | "AuthzRelatedEntityMembership" | "AuthzOrgHierarchy" | "AuthzTemporal" | "AuthzPublishable" | "AuthzMemberList" | "AuthzRelatedMemberList" | "AuthzAllowAll" | "AuthzDenyAll" | "AuthzComposite" | "AuthzNotReadOnly" | "AuthzPeerOwnership" | "AuthzRelatedPeerOwnership";
|
|
456
466
|
/** Privileges this policy applies to (e.g., ["select"], ["insert", "update", "delete"]). */
|
|
457
467
|
privileges?: string[];
|
|
458
468
|
/** Whether this policy is permissive (true) or restrictive (false). Defaults to true. */
|
|
@@ -549,8 +559,31 @@ export interface BlueprintTableUniqueConstraint {
|
|
|
549
559
|
/** Optional schema name override. */
|
|
550
560
|
schema_name?: string;
|
|
551
561
|
}
|
|
562
|
+
/** A membership type entry for Phase 0 of construct_blueprint(). Provisions a full entity type with its own entity table, membership modules, and security policies via entity_type_provision. */
|
|
563
|
+
export interface BlueprintMembershipType {
|
|
564
|
+
/** Entity type name (e.g., "data_room", "channel", "department"). Must be unique per database. */
|
|
565
|
+
name: string;
|
|
566
|
+
/** Short prefix for generated objects (e.g., "dr", "ch", "dept"). Used in table/trigger naming. */
|
|
567
|
+
prefix: string;
|
|
568
|
+
/** Human-readable description of this entity type. */
|
|
569
|
+
description?: string;
|
|
570
|
+
/** Parent entity type name. Defaults to "org". */
|
|
571
|
+
parent_entity?: string;
|
|
572
|
+
/** Custom table name for the entity table. Defaults to name-derived convention. */
|
|
573
|
+
table_name?: string;
|
|
574
|
+
/** Whether this entity type is visible in the API. Defaults to true. */
|
|
575
|
+
is_visible?: boolean;
|
|
576
|
+
/** Whether to provision a limits module for this entity type. Defaults to false. */
|
|
577
|
+
has_limits?: boolean;
|
|
578
|
+
/** Whether to provision a profiles module for this entity type. Defaults to false. */
|
|
579
|
+
has_profiles?: boolean;
|
|
580
|
+
/** Whether to provision a levels module for this entity type. Defaults to false. */
|
|
581
|
+
has_levels?: boolean;
|
|
582
|
+
/** Whether to skip creating default RLS policies on the entity table. Defaults to false. */
|
|
583
|
+
skip_entity_policies?: boolean;
|
|
584
|
+
}
|
|
552
585
|
/** String shorthand -- just the node type name. */
|
|
553
|
-
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" | "SearchVector" | "SearchFullText" | "SearchBm25" | "SearchUnified" | "SearchSpatial" | "SearchSpatialAggregate" | "DataJobTrigger" | "DataTags" | "DataStatusField" | "DataJsonb" | "SearchTrgm" | "DataSlug" | "DataInflection" | "DataOwnedFields" | "DataInheritFromParent" | "DataForceCurrentUser" | "DataImmutableFields" | "DataCompositeField" | "TableUserProfiles" | "TableOrganizationSettings" | "TableUserSettings";
|
|
586
|
+
export type BlueprintNodeShorthand = "AuthzDirectOwner" | "AuthzDirectOwnerAny" | "AuthzMembership" | "AuthzEntityMembership" | "AuthzRelatedEntityMembership" | "AuthzOrgHierarchy" | "AuthzTemporal" | "AuthzPublishable" | "AuthzMemberList" | "AuthzRelatedMemberList" | "AuthzAllowAll" | "AuthzDenyAll" | "AuthzComposite" | "AuthzNotReadOnly" | "AuthzPeerOwnership" | "AuthzRelatedPeerOwnership" | "DataId" | "DataDirectOwner" | "DataEntityMembership" | "DataOwnershipInEntity" | "DataTimestamps" | "DataPeoplestamps" | "DataPublishable" | "DataSoftDelete" | "SearchVector" | "SearchFullText" | "SearchBm25" | "SearchUnified" | "SearchSpatial" | "SearchSpatialAggregate" | "DataJobTrigger" | "DataTags" | "DataStatusField" | "DataJsonb" | "SearchTrgm" | "DataSlug" | "DataInflection" | "DataOwnedFields" | "DataInheritFromParent" | "DataForceCurrentUser" | "DataImmutableFields" | "DataCompositeField" | "TableUserProfiles" | "TableOrganizationSettings" | "TableUserSettings";
|
|
554
587
|
/** Object form -- { $type, data } with typed parameters. */
|
|
555
588
|
export type BlueprintNodeObject = {
|
|
556
589
|
$type: "AuthzDirectOwner";
|
|
@@ -591,6 +624,9 @@ export type BlueprintNodeObject = {
|
|
|
591
624
|
} | {
|
|
592
625
|
$type: "AuthzComposite";
|
|
593
626
|
data: AuthzCompositeParams;
|
|
627
|
+
} | {
|
|
628
|
+
$type: "AuthzNotReadOnly";
|
|
629
|
+
data: AuthzNotReadOnlyParams;
|
|
594
630
|
} | {
|
|
595
631
|
$type: "AuthzPeerOwnership";
|
|
596
632
|
data: AuthzPeerOwnershipParams;
|
|
@@ -750,4 +786,6 @@ export interface BlueprintDefinition {
|
|
|
750
786
|
full_text_searches?: BlueprintFullTextSearch[];
|
|
751
787
|
/** Unique constraints on table columns. */
|
|
752
788
|
unique_constraints?: BlueprintUniqueConstraint[];
|
|
789
|
+
/** Entity types to provision in Phase 0 (before tables). Each entry creates an entity table with membership modules and security. */
|
|
790
|
+
membership_types?: BlueprintMembershipType[];
|
|
753
791
|
}
|
|
@@ -349,6 +349,20 @@ function buildBlueprintTableUniqueConstraint() {
|
|
|
349
349
|
addJSDoc(optionalProp('schema_name', t.tsStringKeyword()), 'Optional schema name override.'),
|
|
350
350
|
]), 'A unique constraint nested inside a table definition (table_name not required).');
|
|
351
351
|
}
|
|
352
|
+
function buildBlueprintMembershipType() {
|
|
353
|
+
return addJSDoc(exportInterface('BlueprintMembershipType', [
|
|
354
|
+
addJSDoc(requiredProp('name', t.tsStringKeyword()), 'Entity type name (e.g., "data_room", "channel", "department"). Must be unique per database.'),
|
|
355
|
+
addJSDoc(requiredProp('prefix', t.tsStringKeyword()), 'Short prefix for generated objects (e.g., "dr", "ch", "dept"). Used in table/trigger naming.'),
|
|
356
|
+
addJSDoc(optionalProp('description', t.tsStringKeyword()), 'Human-readable description of this entity type.'),
|
|
357
|
+
addJSDoc(optionalProp('parent_entity', t.tsStringKeyword()), 'Parent entity type name. Defaults to "org".'),
|
|
358
|
+
addJSDoc(optionalProp('table_name', t.tsStringKeyword()), 'Custom table name for the entity table. Defaults to name-derived convention.'),
|
|
359
|
+
addJSDoc(optionalProp('is_visible', t.tsBooleanKeyword()), 'Whether this entity type is visible in the API. Defaults to true.'),
|
|
360
|
+
addJSDoc(optionalProp('has_limits', t.tsBooleanKeyword()), 'Whether to provision a limits module for this entity type. Defaults to false.'),
|
|
361
|
+
addJSDoc(optionalProp('has_profiles', t.tsBooleanKeyword()), 'Whether to provision a profiles module for this entity type. Defaults to false.'),
|
|
362
|
+
addJSDoc(optionalProp('has_levels', t.tsBooleanKeyword()), 'Whether to provision a levels module for this entity type. Defaults to false.'),
|
|
363
|
+
addJSDoc(optionalProp('skip_entity_policies', t.tsBooleanKeyword()), 'Whether to skip creating default RLS policies on the entity table. Defaults to false.'),
|
|
364
|
+
]), 'A membership type entry for Phase 0 of construct_blueprint(). Provisions a full entity type with its own entity table, membership modules, and security policies via entity_type_provision.');
|
|
365
|
+
}
|
|
352
366
|
function buildBlueprintTable() {
|
|
353
367
|
return addJSDoc(exportInterface('BlueprintTable', [
|
|
354
368
|
addJSDoc(requiredProp('table_name', t.tsStringKeyword()), 'The PostgreSQL table name to create.'),
|
|
@@ -371,6 +385,7 @@ function buildBlueprintDefinition() {
|
|
|
371
385
|
addJSDoc(optionalProp('indexes', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintIndex')))), 'Indexes on table columns.'),
|
|
372
386
|
addJSDoc(optionalProp('full_text_searches', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintFullTextSearch')))), 'Full-text search configurations.'),
|
|
373
387
|
addJSDoc(optionalProp('unique_constraints', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintUniqueConstraint')))), 'Unique constraints on table columns.'),
|
|
388
|
+
addJSDoc(optionalProp('membership_types', t.tsArrayType(t.tsTypeReference(t.identifier('BlueprintMembershipType')))), 'Entity types to provision in Phase 0 (before tables). Each entry creates an entity table with membership modules and security.'),
|
|
374
389
|
]), 'The complete blueprint definition -- the JSONB shape accepted by construct_blueprint().');
|
|
375
390
|
}
|
|
376
391
|
// ---------------------------------------------------------------------------
|
|
@@ -424,6 +439,7 @@ function buildProgram(meta) {
|
|
|
424
439
|
statements.push(buildBlueprintTableIndex());
|
|
425
440
|
statements.push(buildBlueprintUniqueConstraint());
|
|
426
441
|
statements.push(buildBlueprintTableUniqueConstraint());
|
|
442
|
+
statements.push(buildBlueprintMembershipType());
|
|
427
443
|
// -- Node types discriminated union --
|
|
428
444
|
statements.push(sectionComment('Node types -- discriminated union for nodes[] entries'));
|
|
429
445
|
statements.push(...buildNodeTypes(dataNodes));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-type-registry",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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",
|
|
@@ -27,7 +27,6 @@
|
|
|
27
27
|
"lint": "eslint . --fix",
|
|
28
28
|
"test": "jest",
|
|
29
29
|
"test:watch": "jest --watch",
|
|
30
|
-
"generate:seed": "ts-node src/codegen/generate-seed.ts",
|
|
31
30
|
"generate:types": "ts-node src/codegen/generate-types.ts"
|
|
32
31
|
},
|
|
33
32
|
"dependencies": {
|
|
@@ -48,5 +47,5 @@
|
|
|
48
47
|
"registry",
|
|
49
48
|
"graphile"
|
|
50
49
|
],
|
|
51
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "0e97757923bc862adb144cfe8a34ec7ecf7aec8a"
|
|
52
51
|
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Generate SQL seed scripts from TypeScript node type definitions.
|
|
3
|
-
*
|
|
4
|
-
* Uses pgsql-deparser to produce individual INSERT statements per node type,
|
|
5
|
-
* suitable for use as separate pgpm migration files.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* npx ts-node src/codegen/generate-seed.ts [--outdir <dir>] [--single] [--pgpm <dir>]
|
|
9
|
-
*
|
|
10
|
-
* --outdir <dir> Directory to write individual SQL files (default: stdout)
|
|
11
|
-
* --single Emit a single combined seed.sql instead of per-node files
|
|
12
|
-
* --pgpm <dir> Generate deploy/revert/verify files in pgpm package layout.
|
|
13
|
-
* <dir> is the pgpm package root (e.g. packages/metaschema).
|
|
14
|
-
* Files are written relative to this root at:
|
|
15
|
-
* deploy/schemas/metaschema_public/tables/node_type_registry/data/seed.sql
|
|
16
|
-
* revert/schemas/metaschema_public/tables/node_type_registry/data/seed.sql
|
|
17
|
-
* verify/schemas/metaschema_public/tables/node_type_registry/data/seed.sql
|
|
18
|
-
*
|
|
19
|
-
* Examples:
|
|
20
|
-
* # Print all INSERT statements to stdout
|
|
21
|
-
* npx ts-node src/codegen/generate-seed.ts
|
|
22
|
-
*
|
|
23
|
-
* # Generate individual migration files
|
|
24
|
-
* npx ts-node src/codegen/generate-seed.ts --outdir ./deploy/seed
|
|
25
|
-
*
|
|
26
|
-
* # Generate a single combined seed file
|
|
27
|
-
* npx ts-node src/codegen/generate-seed.ts --single --outdir ./deploy
|
|
28
|
-
*
|
|
29
|
-
* # Generate pgpm deploy/revert/verify files
|
|
30
|
-
* npx ts-node src/codegen/generate-seed.ts --pgpm ../../constructive-db/packages/metaschema
|
|
31
|
-
*/
|
|
32
|
-
export {};
|
package/codegen/generate-seed.js
DELETED
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Generate SQL seed scripts from TypeScript node type definitions.
|
|
4
|
-
*
|
|
5
|
-
* Uses pgsql-deparser to produce individual INSERT statements per node type,
|
|
6
|
-
* suitable for use as separate pgpm migration files.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* npx ts-node src/codegen/generate-seed.ts [--outdir <dir>] [--single] [--pgpm <dir>]
|
|
10
|
-
*
|
|
11
|
-
* --outdir <dir> Directory to write individual SQL files (default: stdout)
|
|
12
|
-
* --single Emit a single combined seed.sql instead of per-node files
|
|
13
|
-
* --pgpm <dir> Generate deploy/revert/verify files in pgpm package layout.
|
|
14
|
-
* <dir> is the pgpm package root (e.g. packages/metaschema).
|
|
15
|
-
* Files are written relative to this root at:
|
|
16
|
-
* deploy/schemas/metaschema_public/tables/node_type_registry/data/seed.sql
|
|
17
|
-
* revert/schemas/metaschema_public/tables/node_type_registry/data/seed.sql
|
|
18
|
-
* verify/schemas/metaschema_public/tables/node_type_registry/data/seed.sql
|
|
19
|
-
*
|
|
20
|
-
* Examples:
|
|
21
|
-
* # Print all INSERT statements to stdout
|
|
22
|
-
* npx ts-node src/codegen/generate-seed.ts
|
|
23
|
-
*
|
|
24
|
-
* # Generate individual migration files
|
|
25
|
-
* npx ts-node src/codegen/generate-seed.ts --outdir ./deploy/seed
|
|
26
|
-
*
|
|
27
|
-
* # Generate a single combined seed file
|
|
28
|
-
* npx ts-node src/codegen/generate-seed.ts --single --outdir ./deploy
|
|
29
|
-
*
|
|
30
|
-
* # Generate pgpm deploy/revert/verify files
|
|
31
|
-
* npx ts-node src/codegen/generate-seed.ts --pgpm ../../constructive-db/packages/metaschema
|
|
32
|
-
*/
|
|
33
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
-
const fs_1 = require("fs");
|
|
35
|
-
const path_1 = require("path");
|
|
36
|
-
const pgsql_deparser_1 = require("pgsql-deparser");
|
|
37
|
-
const utils_1 = require("@pgsql/utils");
|
|
38
|
-
const index_1 = require("../index");
|
|
39
|
-
// ---------------------------------------------------------------------------
|
|
40
|
-
// Constants
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
42
|
-
const MIGRATION_PATH = 'schemas/metaschema_public/tables/node_type_registry/data/seed';
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
// AST helpers
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
const astr = (val) => utils_1.nodes.aConst({ sval: utils_1.ast.string({ sval: val }) });
|
|
47
|
-
const makeCast = (arg, typeName) => ({
|
|
48
|
-
TypeCast: {
|
|
49
|
-
arg,
|
|
50
|
-
typeName: {
|
|
51
|
-
names: [{ String: { sval: typeName } }],
|
|
52
|
-
typemod: -1,
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
const makeArrayExpr = (elements) => ({
|
|
57
|
-
A_ArrayExpr: { elements },
|
|
58
|
-
});
|
|
59
|
-
// ---------------------------------------------------------------------------
|
|
60
|
-
// Build a single INSERT statement for one node type
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
function buildInsertStmt(nt) {
|
|
63
|
-
const cols = [
|
|
64
|
-
'name',
|
|
65
|
-
'slug',
|
|
66
|
-
'category',
|
|
67
|
-
'display_name',
|
|
68
|
-
'description',
|
|
69
|
-
'parameter_schema',
|
|
70
|
-
'tags',
|
|
71
|
-
];
|
|
72
|
-
const vals = [
|
|
73
|
-
astr(nt.name),
|
|
74
|
-
astr(nt.slug),
|
|
75
|
-
astr(nt.category),
|
|
76
|
-
astr(nt.display_name),
|
|
77
|
-
astr(nt.description),
|
|
78
|
-
makeCast(astr(JSON.stringify(nt.parameter_schema)), 'jsonb'),
|
|
79
|
-
makeArrayExpr(nt.tags.map((t) => astr(t))),
|
|
80
|
-
];
|
|
81
|
-
return {
|
|
82
|
-
RawStmt: {
|
|
83
|
-
stmt: {
|
|
84
|
-
InsertStmt: {
|
|
85
|
-
relation: {
|
|
86
|
-
schemaname: 'metaschema_public',
|
|
87
|
-
relname: 'node_type_registry',
|
|
88
|
-
inh: true,
|
|
89
|
-
relpersistence: 'p',
|
|
90
|
-
},
|
|
91
|
-
cols: cols.map((name) => utils_1.nodes.resTarget({ name })),
|
|
92
|
-
selectStmt: {
|
|
93
|
-
SelectStmt: {
|
|
94
|
-
valuesLists: [
|
|
95
|
-
{
|
|
96
|
-
List: { items: vals },
|
|
97
|
-
},
|
|
98
|
-
],
|
|
99
|
-
op: 'SETOP_NONE',
|
|
100
|
-
limitOption: 'LIMIT_OPTION_DEFAULT',
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
onConflictClause: {
|
|
104
|
-
action: 'ONCONFLICT_NOTHING',
|
|
105
|
-
infer: {
|
|
106
|
-
indexElems: [
|
|
107
|
-
{
|
|
108
|
-
IndexElem: {
|
|
109
|
-
name: 'slug',
|
|
110
|
-
ordering: 'SORTBY_DEFAULT',
|
|
111
|
-
nulls_ordering: 'SORTBY_NULLS_DEFAULT',
|
|
112
|
-
},
|
|
113
|
-
},
|
|
114
|
-
],
|
|
115
|
-
},
|
|
116
|
-
},
|
|
117
|
-
override: 'OVERRIDING_NOT_SET',
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
|
-
stmt_len: 1,
|
|
121
|
-
},
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
// ---------------------------------------------------------------------------
|
|
125
|
-
// pgpm file generators
|
|
126
|
-
// ---------------------------------------------------------------------------
|
|
127
|
-
const GENERATED_HEADER = [
|
|
128
|
-
'-- GENERATED FILE — DO NOT EDIT',
|
|
129
|
-
'--',
|
|
130
|
-
'-- Regenerate with:',
|
|
131
|
-
'-- cd graphql/node-type-registry && pnpm generate:seed --pgpm ../../constructive-db/packages/metaschema',
|
|
132
|
-
'--',
|
|
133
|
-
'',
|
|
134
|
-
].join('\n');
|
|
135
|
-
async function buildDeploySql() {
|
|
136
|
-
const header = [
|
|
137
|
-
`-- Deploy ${MIGRATION_PATH} to pg`,
|
|
138
|
-
'',
|
|
139
|
-
GENERATED_HEADER,
|
|
140
|
-
'-- requires: schemas/metaschema_public/tables/node_type_registry/table',
|
|
141
|
-
'',
|
|
142
|
-
].join('\n');
|
|
143
|
-
const stmts = index_1.allNodeTypes.map(buildInsertStmt);
|
|
144
|
-
const body = await (0, pgsql_deparser_1.deparse)(stmts);
|
|
145
|
-
return header + body + '\n';
|
|
146
|
-
}
|
|
147
|
-
function buildRevertSql() {
|
|
148
|
-
const names = index_1.allNodeTypes.map((nt) => ` '${nt.name}'`);
|
|
149
|
-
// Wrap names at ~4 per line for readability
|
|
150
|
-
const chunks = [];
|
|
151
|
-
for (let i = 0; i < names.length; i += 4) {
|
|
152
|
-
chunks.push(names.slice(i, i + 4).join(', '));
|
|
153
|
-
}
|
|
154
|
-
return [
|
|
155
|
-
`-- Revert ${MIGRATION_PATH} from pg`,
|
|
156
|
-
'',
|
|
157
|
-
GENERATED_HEADER,
|
|
158
|
-
'DELETE FROM metaschema_public.node_type_registry',
|
|
159
|
-
'WHERE name IN (',
|
|
160
|
-
chunks.join(',\n'),
|
|
161
|
-
');',
|
|
162
|
-
'',
|
|
163
|
-
].join('\n');
|
|
164
|
-
}
|
|
165
|
-
function buildVerifySql() {
|
|
166
|
-
// Pick one representative from each category
|
|
167
|
-
const categories = new Map();
|
|
168
|
-
for (const nt of index_1.allNodeTypes) {
|
|
169
|
-
if (!categories.has(nt.category)) {
|
|
170
|
-
categories.set(nt.category, nt);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
const checks = Array.from(categories.values()).map((nt) => `SELECT 1 FROM metaschema_public.node_type_registry WHERE name = '${nt.name}';`);
|
|
174
|
-
return [
|
|
175
|
-
`-- Verify ${MIGRATION_PATH} on pg`,
|
|
176
|
-
'',
|
|
177
|
-
GENERATED_HEADER,
|
|
178
|
-
...checks,
|
|
179
|
-
'',
|
|
180
|
-
].join('\n');
|
|
181
|
-
}
|
|
182
|
-
// ---------------------------------------------------------------------------
|
|
183
|
-
// File writer helper
|
|
184
|
-
// ---------------------------------------------------------------------------
|
|
185
|
-
function writeFile(filePath, content) {
|
|
186
|
-
const dir = (0, path_1.join)(filePath, '..');
|
|
187
|
-
if (!(0, fs_1.existsSync)(dir))
|
|
188
|
-
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
189
|
-
(0, fs_1.writeFileSync)(filePath, content);
|
|
190
|
-
}
|
|
191
|
-
// ---------------------------------------------------------------------------
|
|
192
|
-
// CLI
|
|
193
|
-
// ---------------------------------------------------------------------------
|
|
194
|
-
async function main() {
|
|
195
|
-
const args = process.argv.slice(2);
|
|
196
|
-
const outdirIdx = args.indexOf('--outdir');
|
|
197
|
-
const outdir = outdirIdx !== -1 ? args[outdirIdx + 1] : undefined;
|
|
198
|
-
const single = args.includes('--single');
|
|
199
|
-
const pgpmIdx = args.indexOf('--pgpm');
|
|
200
|
-
const pgpmRoot = pgpmIdx !== -1 ? args[pgpmIdx + 1] : undefined;
|
|
201
|
-
// --pgpm mode: generate deploy/revert/verify in pgpm package layout
|
|
202
|
-
if (pgpmRoot) {
|
|
203
|
-
const relPath = 'schemas/metaschema_public/tables/node_type_registry/data/seed.sql';
|
|
204
|
-
const deployPath = (0, path_1.join)(pgpmRoot, 'deploy', relPath);
|
|
205
|
-
const revertPath = (0, path_1.join)(pgpmRoot, 'revert', relPath);
|
|
206
|
-
const verifyPath = (0, path_1.join)(pgpmRoot, 'verify', relPath);
|
|
207
|
-
writeFile(deployPath, await buildDeploySql());
|
|
208
|
-
writeFile(revertPath, buildRevertSql());
|
|
209
|
-
writeFile(verifyPath, buildVerifySql());
|
|
210
|
-
console.log(`Wrote ${index_1.allNodeTypes.length} node types to pgpm layout:`);
|
|
211
|
-
console.log(` deploy: ${deployPath}`);
|
|
212
|
-
console.log(` revert: ${revertPath}`);
|
|
213
|
-
console.log(` verify: ${verifyPath}`);
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
if (single) {
|
|
217
|
-
// Emit all INSERT statements as a single SQL string
|
|
218
|
-
const stmts = index_1.allNodeTypes.map(buildInsertStmt);
|
|
219
|
-
const sql = await (0, pgsql_deparser_1.deparse)(stmts);
|
|
220
|
-
if (outdir) {
|
|
221
|
-
if (!(0, fs_1.existsSync)(outdir))
|
|
222
|
-
(0, fs_1.mkdirSync)(outdir, { recursive: true });
|
|
223
|
-
(0, fs_1.writeFileSync)((0, path_1.join)(outdir, 'seed.sql'), sql + '\n');
|
|
224
|
-
console.log(`Wrote ${index_1.allNodeTypes.length} node types to ${(0, path_1.join)(outdir, 'seed.sql')}`);
|
|
225
|
-
}
|
|
226
|
-
else {
|
|
227
|
-
process.stdout.write(sql + '\n');
|
|
228
|
-
}
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
// Emit individual SQL files per node type
|
|
232
|
-
const stmts = await Promise.all(index_1.allNodeTypes.map(async (nt) => ({
|
|
233
|
-
nt,
|
|
234
|
-
sql: await (0, pgsql_deparser_1.deparse)([buildInsertStmt(nt)]),
|
|
235
|
-
})));
|
|
236
|
-
if (outdir) {
|
|
237
|
-
if (!(0, fs_1.existsSync)(outdir))
|
|
238
|
-
(0, fs_1.mkdirSync)(outdir, { recursive: true });
|
|
239
|
-
for (const { nt, sql } of stmts) {
|
|
240
|
-
const filename = `${nt.slug}.sql`;
|
|
241
|
-
(0, fs_1.writeFileSync)((0, path_1.join)(outdir, filename), sql + '\n');
|
|
242
|
-
}
|
|
243
|
-
console.log(`Wrote ${stmts.length} individual migration files to ${outdir}/`);
|
|
244
|
-
}
|
|
245
|
-
else {
|
|
246
|
-
for (const { nt, sql } of stmts) {
|
|
247
|
-
console.log(`-- ${nt.name} (${nt.slug})`);
|
|
248
|
-
console.log(sql + ';\n');
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
main();
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Generate SQL seed scripts from TypeScript node type definitions.
|
|
3
|
-
*
|
|
4
|
-
* Uses pgsql-deparser to produce individual INSERT statements per node type,
|
|
5
|
-
* suitable for use as separate pgpm migration files.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* npx ts-node src/codegen/generate-seed.ts [--outdir <dir>] [--single] [--pgpm <dir>]
|
|
9
|
-
*
|
|
10
|
-
* --outdir <dir> Directory to write individual SQL files (default: stdout)
|
|
11
|
-
* --single Emit a single combined seed.sql instead of per-node files
|
|
12
|
-
* --pgpm <dir> Generate deploy/revert/verify files in pgpm package layout.
|
|
13
|
-
* <dir> is the pgpm package root (e.g. packages/metaschema).
|
|
14
|
-
* Files are written relative to this root at:
|
|
15
|
-
* deploy/schemas/metaschema_public/tables/node_type_registry/data/seed.sql
|
|
16
|
-
* revert/schemas/metaschema_public/tables/node_type_registry/data/seed.sql
|
|
17
|
-
* verify/schemas/metaschema_public/tables/node_type_registry/data/seed.sql
|
|
18
|
-
*
|
|
19
|
-
* Examples:
|
|
20
|
-
* # Print all INSERT statements to stdout
|
|
21
|
-
* npx ts-node src/codegen/generate-seed.ts
|
|
22
|
-
*
|
|
23
|
-
* # Generate individual migration files
|
|
24
|
-
* npx ts-node src/codegen/generate-seed.ts --outdir ./deploy/seed
|
|
25
|
-
*
|
|
26
|
-
* # Generate a single combined seed file
|
|
27
|
-
* npx ts-node src/codegen/generate-seed.ts --single --outdir ./deploy
|
|
28
|
-
*
|
|
29
|
-
* # Generate pgpm deploy/revert/verify files
|
|
30
|
-
* npx ts-node src/codegen/generate-seed.ts --pgpm ../../constructive-db/packages/metaschema
|
|
31
|
-
*/
|
|
32
|
-
export {};
|
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Generate SQL seed scripts from TypeScript node type definitions.
|
|
3
|
-
*
|
|
4
|
-
* Uses pgsql-deparser to produce individual INSERT statements per node type,
|
|
5
|
-
* suitable for use as separate pgpm migration files.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* npx ts-node src/codegen/generate-seed.ts [--outdir <dir>] [--single] [--pgpm <dir>]
|
|
9
|
-
*
|
|
10
|
-
* --outdir <dir> Directory to write individual SQL files (default: stdout)
|
|
11
|
-
* --single Emit a single combined seed.sql instead of per-node files
|
|
12
|
-
* --pgpm <dir> Generate deploy/revert/verify files in pgpm package layout.
|
|
13
|
-
* <dir> is the pgpm package root (e.g. packages/metaschema).
|
|
14
|
-
* Files are written relative to this root at:
|
|
15
|
-
* deploy/schemas/metaschema_public/tables/node_type_registry/data/seed.sql
|
|
16
|
-
* revert/schemas/metaschema_public/tables/node_type_registry/data/seed.sql
|
|
17
|
-
* verify/schemas/metaschema_public/tables/node_type_registry/data/seed.sql
|
|
18
|
-
*
|
|
19
|
-
* Examples:
|
|
20
|
-
* # Print all INSERT statements to stdout
|
|
21
|
-
* npx ts-node src/codegen/generate-seed.ts
|
|
22
|
-
*
|
|
23
|
-
* # Generate individual migration files
|
|
24
|
-
* npx ts-node src/codegen/generate-seed.ts --outdir ./deploy/seed
|
|
25
|
-
*
|
|
26
|
-
* # Generate a single combined seed file
|
|
27
|
-
* npx ts-node src/codegen/generate-seed.ts --single --outdir ./deploy
|
|
28
|
-
*
|
|
29
|
-
* # Generate pgpm deploy/revert/verify files
|
|
30
|
-
* npx ts-node src/codegen/generate-seed.ts --pgpm ../../constructive-db/packages/metaschema
|
|
31
|
-
*/
|
|
32
|
-
import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
33
|
-
import { join } from 'path';
|
|
34
|
-
import { deparse } from 'pgsql-deparser';
|
|
35
|
-
import { ast, nodes } from '@pgsql/utils';
|
|
36
|
-
import { allNodeTypes } from '../index';
|
|
37
|
-
// ---------------------------------------------------------------------------
|
|
38
|
-
// Constants
|
|
39
|
-
// ---------------------------------------------------------------------------
|
|
40
|
-
const MIGRATION_PATH = 'schemas/metaschema_public/tables/node_type_registry/data/seed';
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
42
|
-
// AST helpers
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
const astr = (val) => nodes.aConst({ sval: ast.string({ sval: val }) });
|
|
45
|
-
const makeCast = (arg, typeName) => ({
|
|
46
|
-
TypeCast: {
|
|
47
|
-
arg,
|
|
48
|
-
typeName: {
|
|
49
|
-
names: [{ String: { sval: typeName } }],
|
|
50
|
-
typemod: -1,
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
});
|
|
54
|
-
const makeArrayExpr = (elements) => ({
|
|
55
|
-
A_ArrayExpr: { elements },
|
|
56
|
-
});
|
|
57
|
-
// ---------------------------------------------------------------------------
|
|
58
|
-
// Build a single INSERT statement for one node type
|
|
59
|
-
// ---------------------------------------------------------------------------
|
|
60
|
-
function buildInsertStmt(nt) {
|
|
61
|
-
const cols = [
|
|
62
|
-
'name',
|
|
63
|
-
'slug',
|
|
64
|
-
'category',
|
|
65
|
-
'display_name',
|
|
66
|
-
'description',
|
|
67
|
-
'parameter_schema',
|
|
68
|
-
'tags',
|
|
69
|
-
];
|
|
70
|
-
const vals = [
|
|
71
|
-
astr(nt.name),
|
|
72
|
-
astr(nt.slug),
|
|
73
|
-
astr(nt.category),
|
|
74
|
-
astr(nt.display_name),
|
|
75
|
-
astr(nt.description),
|
|
76
|
-
makeCast(astr(JSON.stringify(nt.parameter_schema)), 'jsonb'),
|
|
77
|
-
makeArrayExpr(nt.tags.map((t) => astr(t))),
|
|
78
|
-
];
|
|
79
|
-
return {
|
|
80
|
-
RawStmt: {
|
|
81
|
-
stmt: {
|
|
82
|
-
InsertStmt: {
|
|
83
|
-
relation: {
|
|
84
|
-
schemaname: 'metaschema_public',
|
|
85
|
-
relname: 'node_type_registry',
|
|
86
|
-
inh: true,
|
|
87
|
-
relpersistence: 'p',
|
|
88
|
-
},
|
|
89
|
-
cols: cols.map((name) => nodes.resTarget({ name })),
|
|
90
|
-
selectStmt: {
|
|
91
|
-
SelectStmt: {
|
|
92
|
-
valuesLists: [
|
|
93
|
-
{
|
|
94
|
-
List: { items: vals },
|
|
95
|
-
},
|
|
96
|
-
],
|
|
97
|
-
op: 'SETOP_NONE',
|
|
98
|
-
limitOption: 'LIMIT_OPTION_DEFAULT',
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
onConflictClause: {
|
|
102
|
-
action: 'ONCONFLICT_NOTHING',
|
|
103
|
-
infer: {
|
|
104
|
-
indexElems: [
|
|
105
|
-
{
|
|
106
|
-
IndexElem: {
|
|
107
|
-
name: 'slug',
|
|
108
|
-
ordering: 'SORTBY_DEFAULT',
|
|
109
|
-
nulls_ordering: 'SORTBY_NULLS_DEFAULT',
|
|
110
|
-
},
|
|
111
|
-
},
|
|
112
|
-
],
|
|
113
|
-
},
|
|
114
|
-
},
|
|
115
|
-
override: 'OVERRIDING_NOT_SET',
|
|
116
|
-
},
|
|
117
|
-
},
|
|
118
|
-
stmt_len: 1,
|
|
119
|
-
},
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
// ---------------------------------------------------------------------------
|
|
123
|
-
// pgpm file generators
|
|
124
|
-
// ---------------------------------------------------------------------------
|
|
125
|
-
const GENERATED_HEADER = [
|
|
126
|
-
'-- GENERATED FILE — DO NOT EDIT',
|
|
127
|
-
'--',
|
|
128
|
-
'-- Regenerate with:',
|
|
129
|
-
'-- cd graphql/node-type-registry && pnpm generate:seed --pgpm ../../constructive-db/packages/metaschema',
|
|
130
|
-
'--',
|
|
131
|
-
'',
|
|
132
|
-
].join('\n');
|
|
133
|
-
async function buildDeploySql() {
|
|
134
|
-
const header = [
|
|
135
|
-
`-- Deploy ${MIGRATION_PATH} to pg`,
|
|
136
|
-
'',
|
|
137
|
-
GENERATED_HEADER,
|
|
138
|
-
'-- requires: schemas/metaschema_public/tables/node_type_registry/table',
|
|
139
|
-
'',
|
|
140
|
-
].join('\n');
|
|
141
|
-
const stmts = allNodeTypes.map(buildInsertStmt);
|
|
142
|
-
const body = await deparse(stmts);
|
|
143
|
-
return header + body + '\n';
|
|
144
|
-
}
|
|
145
|
-
function buildRevertSql() {
|
|
146
|
-
const names = allNodeTypes.map((nt) => ` '${nt.name}'`);
|
|
147
|
-
// Wrap names at ~4 per line for readability
|
|
148
|
-
const chunks = [];
|
|
149
|
-
for (let i = 0; i < names.length; i += 4) {
|
|
150
|
-
chunks.push(names.slice(i, i + 4).join(', '));
|
|
151
|
-
}
|
|
152
|
-
return [
|
|
153
|
-
`-- Revert ${MIGRATION_PATH} from pg`,
|
|
154
|
-
'',
|
|
155
|
-
GENERATED_HEADER,
|
|
156
|
-
'DELETE FROM metaschema_public.node_type_registry',
|
|
157
|
-
'WHERE name IN (',
|
|
158
|
-
chunks.join(',\n'),
|
|
159
|
-
');',
|
|
160
|
-
'',
|
|
161
|
-
].join('\n');
|
|
162
|
-
}
|
|
163
|
-
function buildVerifySql() {
|
|
164
|
-
// Pick one representative from each category
|
|
165
|
-
const categories = new Map();
|
|
166
|
-
for (const nt of allNodeTypes) {
|
|
167
|
-
if (!categories.has(nt.category)) {
|
|
168
|
-
categories.set(nt.category, nt);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
const checks = Array.from(categories.values()).map((nt) => `SELECT 1 FROM metaschema_public.node_type_registry WHERE name = '${nt.name}';`);
|
|
172
|
-
return [
|
|
173
|
-
`-- Verify ${MIGRATION_PATH} on pg`,
|
|
174
|
-
'',
|
|
175
|
-
GENERATED_HEADER,
|
|
176
|
-
...checks,
|
|
177
|
-
'',
|
|
178
|
-
].join('\n');
|
|
179
|
-
}
|
|
180
|
-
// ---------------------------------------------------------------------------
|
|
181
|
-
// File writer helper
|
|
182
|
-
// ---------------------------------------------------------------------------
|
|
183
|
-
function writeFile(filePath, content) {
|
|
184
|
-
const dir = join(filePath, '..');
|
|
185
|
-
if (!existsSync(dir))
|
|
186
|
-
mkdirSync(dir, { recursive: true });
|
|
187
|
-
writeFileSync(filePath, content);
|
|
188
|
-
}
|
|
189
|
-
// ---------------------------------------------------------------------------
|
|
190
|
-
// CLI
|
|
191
|
-
// ---------------------------------------------------------------------------
|
|
192
|
-
async function main() {
|
|
193
|
-
const args = process.argv.slice(2);
|
|
194
|
-
const outdirIdx = args.indexOf('--outdir');
|
|
195
|
-
const outdir = outdirIdx !== -1 ? args[outdirIdx + 1] : undefined;
|
|
196
|
-
const single = args.includes('--single');
|
|
197
|
-
const pgpmIdx = args.indexOf('--pgpm');
|
|
198
|
-
const pgpmRoot = pgpmIdx !== -1 ? args[pgpmIdx + 1] : undefined;
|
|
199
|
-
// --pgpm mode: generate deploy/revert/verify in pgpm package layout
|
|
200
|
-
if (pgpmRoot) {
|
|
201
|
-
const relPath = 'schemas/metaschema_public/tables/node_type_registry/data/seed.sql';
|
|
202
|
-
const deployPath = join(pgpmRoot, 'deploy', relPath);
|
|
203
|
-
const revertPath = join(pgpmRoot, 'revert', relPath);
|
|
204
|
-
const verifyPath = join(pgpmRoot, 'verify', relPath);
|
|
205
|
-
writeFile(deployPath, await buildDeploySql());
|
|
206
|
-
writeFile(revertPath, buildRevertSql());
|
|
207
|
-
writeFile(verifyPath, buildVerifySql());
|
|
208
|
-
console.log(`Wrote ${allNodeTypes.length} node types to pgpm layout:`);
|
|
209
|
-
console.log(` deploy: ${deployPath}`);
|
|
210
|
-
console.log(` revert: ${revertPath}`);
|
|
211
|
-
console.log(` verify: ${verifyPath}`);
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
if (single) {
|
|
215
|
-
// Emit all INSERT statements as a single SQL string
|
|
216
|
-
const stmts = allNodeTypes.map(buildInsertStmt);
|
|
217
|
-
const sql = await deparse(stmts);
|
|
218
|
-
if (outdir) {
|
|
219
|
-
if (!existsSync(outdir))
|
|
220
|
-
mkdirSync(outdir, { recursive: true });
|
|
221
|
-
writeFileSync(join(outdir, 'seed.sql'), sql + '\n');
|
|
222
|
-
console.log(`Wrote ${allNodeTypes.length} node types to ${join(outdir, 'seed.sql')}`);
|
|
223
|
-
}
|
|
224
|
-
else {
|
|
225
|
-
process.stdout.write(sql + '\n');
|
|
226
|
-
}
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
// Emit individual SQL files per node type
|
|
230
|
-
const stmts = await Promise.all(allNodeTypes.map(async (nt) => ({
|
|
231
|
-
nt,
|
|
232
|
-
sql: await deparse([buildInsertStmt(nt)]),
|
|
233
|
-
})));
|
|
234
|
-
if (outdir) {
|
|
235
|
-
if (!existsSync(outdir))
|
|
236
|
-
mkdirSync(outdir, { recursive: true });
|
|
237
|
-
for (const { nt, sql } of stmts) {
|
|
238
|
-
const filename = `${nt.slug}.sql`;
|
|
239
|
-
writeFileSync(join(outdir, filename), sql + '\n');
|
|
240
|
-
}
|
|
241
|
-
console.log(`Wrote ${stmts.length} individual migration files to ${outdir}/`);
|
|
242
|
-
}
|
|
243
|
-
else {
|
|
244
|
-
for (const { nt, sql } of stmts) {
|
|
245
|
-
console.log(`-- ${nt.name} (${nt.slug})`);
|
|
246
|
-
console.log(sql + ';\n');
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
main();
|