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 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=group (or string name resolved via membership_types_module)"
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=group (or string name resolved via membership_types_module)"
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,2 @@
1
+ import type { NodeTypeDefinition } from '../types';
2
+ export declare const AuthzNotReadOnly: NodeTypeDefinition;
@@ -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=group (or string name resolved via membership_types_module)"
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=group (or string name resolved via membership_types_module)"
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=group (or string name resolved via membership_types_module)"
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: number | string;
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=group (or string name resolved via membership_types_module)"
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=group (or string name resolved via membership_types_module)"
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,2 @@
1
+ import type { NodeTypeDefinition } from '../types';
2
+ export declare const AuthzNotReadOnly: NodeTypeDefinition;
@@ -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=group (or string name resolved via membership_types_module)"
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=group (or string name resolved via membership_types_module)"
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=group (or string name resolved via membership_types_module)"
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",
@@ -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';
@@ -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: number | string;
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.10.1",
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": "fe60f7b81252eea53dce227bb581d5ae2ef0ec36"
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 {};
@@ -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();