graphile-settings 4.10.3 → 4.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.
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Node Type Registry Plugin
3
+ *
4
+ * Exports the gather-phase plugin and preset for generating @oneOf typed
5
+ * input types for blueprint definitions from node_type_registry.
6
+ *
7
+ * The NodeTypeRegistryPlugin queries node_type_registry through the existing
8
+ * pgService connection during the gather phase — no separate pool needed.
9
+ */
10
+ export { NodeTypeRegistryPlugin, NodeTypeRegistryPreset, createBlueprintTypesPlugin, BlueprintTypesPreset, } from './plugin';
11
+ export type { NodeTypeRegistryEntry } from './plugin';
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Node Type Registry Plugin
3
+ *
4
+ * Exports the gather-phase plugin and preset for generating @oneOf typed
5
+ * input types for blueprint definitions from node_type_registry.
6
+ *
7
+ * The NodeTypeRegistryPlugin queries node_type_registry through the existing
8
+ * pgService connection during the gather phase — no separate pool needed.
9
+ */
10
+ export { NodeTypeRegistryPlugin, NodeTypeRegistryPreset,
11
+ // Legacy exports for backward compatibility
12
+ createBlueprintTypesPlugin, BlueprintTypesPreset, } from './plugin';
@@ -0,0 +1,64 @@
1
+ /**
2
+ * JSON Schema to GraphQL Input Field Spec Converter
3
+ *
4
+ * Converts JSON Schema objects (from node_type_registry.parameter_schema)
5
+ * into field spec objects compatible with PostGraphile v5's fieldWithHooks API.
6
+ *
7
+ * Handles:
8
+ * - string, integer, number, boolean primitives
9
+ * - arrays (items -> GraphQLList)
10
+ * - enums -> GraphQLEnumType
11
+ * - required fields -> GraphQLNonNull
12
+ * - union types (["integer", "string"]) -> JSON scalar fallback
13
+ */
14
+ import type { GraphQLInputType, GraphQLScalarType, GraphQLNullableType } from 'graphql';
15
+ interface JsonSchema {
16
+ type: string;
17
+ properties?: Record<string, unknown>;
18
+ required?: string[];
19
+ anyOf?: JsonSchema[];
20
+ description?: string;
21
+ }
22
+ /**
23
+ * A field spec that mirrors GraphQLInputFieldConfig.
24
+ */
25
+ export interface FieldSpec {
26
+ type: GraphQLInputType;
27
+ description?: string;
28
+ }
29
+ /**
30
+ * Minimal build interface — only what we need from PostGraphile's Build object.
31
+ *
32
+ * Uses permissive constructor signatures to avoid variance issues between
33
+ * GraphQLNullableType and GraphQLInputType in the graphql-js type hierarchy.
34
+ */
35
+ export interface BuildLike {
36
+ graphql: {
37
+ GraphQLString: GraphQLScalarType;
38
+ GraphQLInt: GraphQLScalarType;
39
+ GraphQLFloat: GraphQLScalarType;
40
+ GraphQLBoolean: GraphQLScalarType;
41
+ GraphQLNonNull: new (type: any) => GraphQLInputType;
42
+ GraphQLList: new (type: any) => GraphQLInputType & GraphQLNullableType;
43
+ GraphQLEnumType: new (config: {
44
+ name: string;
45
+ values: Record<string, {
46
+ value: string;
47
+ }>;
48
+ }) => GraphQLInputType & GraphQLNullableType;
49
+ };
50
+ getTypeByName: (name: string) => unknown;
51
+ }
52
+ /**
53
+ * Convert a full JSON Schema (from parameter_schema) to field specs.
54
+ *
55
+ * Uses the build object from PostGraphile v5 to access GraphQL types,
56
+ * avoiding direct graphql imports (which can cause version mismatches).
57
+ *
58
+ * @param schema - The JSON Schema object
59
+ * @param typeName - The name prefix for generated enum types
60
+ * @param build - The PostGraphile build object
61
+ * @returns Record of field name -> FieldSpec
62
+ */
63
+ export declare function jsonSchemaToGraphQLFieldSpecs(schema: JsonSchema, typeName: string, build: BuildLike): Record<string, FieldSpec>;
64
+ export {};
@@ -0,0 +1,89 @@
1
+ /**
2
+ * JSON Schema to GraphQL Input Field Spec Converter
3
+ *
4
+ * Converts JSON Schema objects (from node_type_registry.parameter_schema)
5
+ * into field spec objects compatible with PostGraphile v5's fieldWithHooks API.
6
+ *
7
+ * Handles:
8
+ * - string, integer, number, boolean primitives
9
+ * - arrays (items -> GraphQLList)
10
+ * - enums -> GraphQLEnumType
11
+ * - required fields -> GraphQLNonNull
12
+ * - union types (["integer", "string"]) -> JSON scalar fallback
13
+ */
14
+ /**
15
+ * Convert a JSON Schema type string to a GraphQL scalar type.
16
+ */
17
+ function jsonTypeToGraphQL(jsonType, graphql, jsonScalar) {
18
+ if (Array.isArray(jsonType)) {
19
+ return jsonScalar;
20
+ }
21
+ switch (jsonType) {
22
+ case 'string':
23
+ return graphql.GraphQLString;
24
+ case 'integer':
25
+ return graphql.GraphQLInt;
26
+ case 'number':
27
+ return graphql.GraphQLFloat;
28
+ case 'boolean':
29
+ return graphql.GraphQLBoolean;
30
+ case 'object':
31
+ return jsonScalar;
32
+ case 'array':
33
+ return new graphql.GraphQLList(jsonScalar);
34
+ default:
35
+ return jsonScalar;
36
+ }
37
+ }
38
+ /**
39
+ * Convert a full JSON Schema (from parameter_schema) to field specs.
40
+ *
41
+ * Uses the build object from PostGraphile v5 to access GraphQL types,
42
+ * avoiding direct graphql imports (which can cause version mismatches).
43
+ *
44
+ * @param schema - The JSON Schema object
45
+ * @param typeName - The name prefix for generated enum types
46
+ * @param build - The PostGraphile build object
47
+ * @returns Record of field name -> FieldSpec
48
+ */
49
+ export function jsonSchemaToGraphQLFieldSpecs(schema, typeName, build) {
50
+ const fields = {};
51
+ const properties = schema.properties;
52
+ if (!properties) {
53
+ return fields;
54
+ }
55
+ const { graphql } = build;
56
+ const jsonScalar = (build.getTypeByName('JSON') ?? graphql.GraphQLString);
57
+ const required = new Set(schema.required ?? []);
58
+ for (const [propName, prop] of Object.entries(properties)) {
59
+ let fieldType;
60
+ if (prop.enum && prop.enum.length > 0) {
61
+ const values = {};
62
+ for (const val of prop.enum) {
63
+ const safeName = val.replace(/[^_a-zA-Z0-9]/g, '_').toUpperCase();
64
+ values[safeName] = { value: val };
65
+ }
66
+ fieldType = new graphql.GraphQLEnumType({
67
+ name: `${typeName}_${propName}`,
68
+ values,
69
+ });
70
+ }
71
+ else if (prop.type === 'array' && prop.items) {
72
+ const innerType = prop.items.type
73
+ ? jsonTypeToGraphQL(prop.items.type, graphql, jsonScalar)
74
+ : jsonScalar;
75
+ fieldType = new graphql.GraphQLList(innerType);
76
+ }
77
+ else {
78
+ fieldType = jsonTypeToGraphQL(prop.type ?? 'string', graphql, jsonScalar);
79
+ }
80
+ if (required.has(propName)) {
81
+ fieldType = new graphql.GraphQLNonNull(fieldType);
82
+ }
83
+ fields[propName] = {
84
+ type: fieldType,
85
+ description: prop.description ?? undefined,
86
+ };
87
+ }
88
+ return fields;
89
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Node Type Registry Plugin
3
+ *
4
+ * Generic PostGraphile v5 plugin that queries node_type_registry at schema
5
+ * build time through the existing pgService connection (gather phase) and
6
+ * generates @oneOf typed input types with SuperCase node type names as
7
+ * discriminant keys.
8
+ *
9
+ * Architecture (correct PostGraphile v5 pattern):
10
+ * 1. `gather` phase: hooks into pgIntrospection_introspection to query
11
+ * node_type_registry through the existing pgService (no separate pool —
12
+ * uses withPgClientFromPgService like PgIntrospectionPlugin)
13
+ * 2. `init` hook: registers all input type shells from gathered entries
14
+ * 3. `GraphQLInputObjectType_fields` hook: populates fields on those types
15
+ *
16
+ * Generated type hierarchy:
17
+ *
18
+ * BlueprintDefinitionInput
19
+ * +-- tables: [BlueprintTableInput!]
20
+ * | +-- ref: String!
21
+ * | +-- tableName: String!
22
+ * | +-- nodes: [BlueprintNodeInput!]! <-- @oneOf
23
+ * | +-- shorthand: String
24
+ * | +-- DataId: DataIdParams
25
+ * | +-- DataTimestamps: DataTimestampsParams
26
+ * | +-- AuthzEntityMembership: AuthzEntityMembershipParams
27
+ * | +-- ...
28
+ * +-- relations: [BlueprintRelationInput!] <-- @oneOf
29
+ * +-- RelationBelongsTo: RelationBelongsToParams
30
+ * +-- ...
31
+ *
32
+ * When codegen runs, @oneOf types become discriminated union types:
33
+ *
34
+ * type BlueprintNodeInput =
35
+ * | { DataId: DataIdParams }
36
+ * | { DataTimestamps: DataTimestampsParams }
37
+ * | { AuthzEntityMembership: AuthzEntityMembershipParams }
38
+ */
39
+ import type { GraphileConfig } from 'graphile-config';
40
+ export interface NodeTypeRegistryEntry {
41
+ name: string;
42
+ slug: string;
43
+ category: string;
44
+ display_name: string;
45
+ description: string;
46
+ parameter_schema: Record<string, unknown>;
47
+ tags: string[];
48
+ }
49
+ declare global {
50
+ namespace GraphileBuild {
51
+ interface ScopeInputObject {
52
+ isBlueprintOneOf?: boolean;
53
+ isBlueprintNodeParams?: boolean;
54
+ isBlueprintTable?: boolean;
55
+ isBlueprintDefinition?: boolean;
56
+ isBlueprintRelation?: boolean;
57
+ blueprintNodeTypeName?: string;
58
+ }
59
+ interface BuildInput {
60
+ nodeTypeRegistryEntries?: NodeTypeRegistryEntry[];
61
+ }
62
+ }
63
+ }
64
+ /**
65
+ * NodeTypeRegistryPlugin
66
+ *
67
+ * Gather-phase plugin that queries node_type_registry through the existing
68
+ * pgService connection, then generates @oneOf typed input types in the
69
+ * schema phase. No separate PG pool -- uses the same connection PostGraphile
70
+ * already has (same pattern as PgIntrospectionPlugin / PgEnumTablesPlugin).
71
+ *
72
+ * If the table doesn't exist (42P01) or the query fails, the plugin
73
+ * gracefully skips and no types are registered.
74
+ */
75
+ export declare const NodeTypeRegistryPlugin: GraphileConfig.Plugin;
76
+ /**
77
+ * Preset that includes the NodeTypeRegistryPlugin.
78
+ *
79
+ * Add to ConstructivePreset.extends[] -- the plugin will automatically
80
+ * query node_type_registry through the existing pgService connection
81
+ * during the gather phase. No separate pool, no manual wiring.
82
+ */
83
+ export declare const NodeTypeRegistryPreset: GraphileConfig.Preset;
84
+ /**
85
+ * @deprecated Use NodeTypeRegistryPlugin directly. The gather-phase plugin
86
+ * queries node_type_registry automatically via pgService. This factory is
87
+ * kept for backward compatibility and tests that pass static entries.
88
+ */
89
+ export declare function createBlueprintTypesPlugin(nodeTypes: NodeTypeRegistryEntry[]): GraphileConfig.Plugin;
90
+ /**
91
+ * @deprecated Use NodeTypeRegistryPreset directly. The gather-phase plugin
92
+ * queries node_type_registry automatically via pgService. This factory is
93
+ * kept for backward compatibility and tests that pass static entries.
94
+ */
95
+ export declare function BlueprintTypesPreset(nodeTypes: NodeTypeRegistryEntry[]): GraphileConfig.Preset;
@@ -0,0 +1,287 @@
1
+ import { withPgClientFromPgService } from 'graphile-build-pg';
2
+ import { jsonSchemaToGraphQLFieldSpecs } from './json-schema-to-graphql';
3
+ // ============================================================================
4
+ // Constants
5
+ // ============================================================================
6
+ const BLUEPRINT_NODE_INPUT = 'BlueprintNodeInput';
7
+ const BLUEPRINT_TABLE_INPUT = 'BlueprintTableInput';
8
+ const BLUEPRINT_RELATION_INPUT = 'BlueprintRelationInput';
9
+ const BLUEPRINT_DEFINITION_INPUT = 'BlueprintDefinitionInput';
10
+ const NODE_TYPE_REGISTRY_QUERY = `
11
+ SELECT
12
+ name,
13
+ slug,
14
+ category,
15
+ COALESCE(display_name, name) as display_name,
16
+ COALESCE(description, '') as description,
17
+ COALESCE(parameter_schema, '{}'::jsonb) as parameter_schema,
18
+ COALESCE(tags, '{}') as tags
19
+ FROM metaschema_public.node_type_registry
20
+ ORDER BY category, name
21
+ `;
22
+ // ============================================================================
23
+ // Shared schema hooks
24
+ // ============================================================================
25
+ /**
26
+ * Builds the schema hooks (init + GraphQLInputObjectType_fields) from
27
+ * a function that retrieves node type entries. Used by both the gather-phase
28
+ * plugin (reads from build.input) and the static/legacy plugin (closure).
29
+ */
30
+ function buildSchemaHooks(getNodeTypes) {
31
+ return {
32
+ hooks: {
33
+ init(_, build) {
34
+ const nodeTypes = getNodeTypes(build);
35
+ if (nodeTypes.length === 0)
36
+ return _;
37
+ for (const nt of nodeTypes) {
38
+ const paramsTypeName = nt.name + 'Params';
39
+ build.registerInputObjectType(paramsTypeName, {
40
+ isBlueprintNodeParams: true,
41
+ blueprintNodeTypeName: nt.name,
42
+ }, () => ({
43
+ description: nt.description,
44
+ }), 'NodeTypeRegistryPlugin: ' + paramsTypeName);
45
+ }
46
+ build.registerInputObjectType(BLUEPRINT_NODE_INPUT, { isBlueprintOneOf: true }, () => ({
47
+ description: 'A single node in a blueprint definition. Exactly one field must be set. Use the SuperCase node type name as the key with its parameters as the value, or use "shorthand" with just the node type name string.',
48
+ isOneOf: true,
49
+ }), 'NodeTypeRegistryPlugin: BlueprintNodeInput');
50
+ build.registerInputObjectType(BLUEPRINT_RELATION_INPUT, { isBlueprintOneOf: true, isBlueprintRelation: true }, () => ({
51
+ description: 'A relation in a blueprint definition. Exactly one field must be set. Use the SuperCase relation type name as the key.',
52
+ isOneOf: true,
53
+ }), 'NodeTypeRegistryPlugin: BlueprintRelationInput');
54
+ build.registerInputObjectType(BLUEPRINT_TABLE_INPUT, { isBlueprintTable: true }, () => ({
55
+ description: 'A table definition within a blueprint. Specifies the table reference, name, and an array of typed nodes.',
56
+ }), 'NodeTypeRegistryPlugin: BlueprintTableInput');
57
+ build.registerInputObjectType(BLUEPRINT_DEFINITION_INPUT, { isBlueprintDefinition: true }, () => ({
58
+ description: 'The complete blueprint definition. Contains tables with typed nodes and relations.',
59
+ }), 'NodeTypeRegistryPlugin: BlueprintDefinitionInput');
60
+ return _;
61
+ },
62
+ GraphQLInputObjectType_fields(fields, build, context) {
63
+ const nodeTypes = getNodeTypes(build);
64
+ if (nodeTypes.length === 0)
65
+ return fields;
66
+ const nodeTypesByName = new Map();
67
+ const relationNodeTypes = [];
68
+ const nonRelationNodeTypes = [];
69
+ for (const nt of nodeTypes) {
70
+ nodeTypesByName.set(nt.name, nt);
71
+ if (nt.category === 'relation') {
72
+ relationNodeTypes.push(nt);
73
+ }
74
+ else {
75
+ nonRelationNodeTypes.push(nt);
76
+ }
77
+ }
78
+ const { extend, graphql: { GraphQLString, GraphQLNonNull, GraphQLList }, } = build;
79
+ const { fieldWithHooks, scope: { isBlueprintOneOf, isBlueprintNodeParams, isBlueprintTable, isBlueprintDefinition, isBlueprintRelation, blueprintNodeTypeName, }, } = context;
80
+ // --- Per-node-type params (e.g., DataIdParams) ---
81
+ if (isBlueprintNodeParams && blueprintNodeTypeName) {
82
+ const nt = nodeTypesByName.get(blueprintNodeTypeName);
83
+ if (!nt)
84
+ return fields;
85
+ const schema = nt.parameter_schema;
86
+ const fieldSpecs = jsonSchemaToGraphQLFieldSpecs(schema, nt.name + 'Params', build);
87
+ if (Object.keys(fieldSpecs).length > 0) {
88
+ let result = fields;
89
+ for (const [fieldName, spec] of Object.entries(fieldSpecs)) {
90
+ result = extend(result, {
91
+ [fieldName]: fieldWithHooks({ fieldName }, () => spec),
92
+ }, 'NodeTypeRegistryPlugin: ' + blueprintNodeTypeName + '.' + fieldName);
93
+ }
94
+ return result;
95
+ }
96
+ return extend(fields, {
97
+ _: fieldWithHooks({ fieldName: '_' }, () => ({
98
+ type: build.graphql.GraphQLBoolean,
99
+ description: 'No parameters required. Pass true or omit entirely.',
100
+ })),
101
+ }, 'NodeTypeRegistryPlugin: ' + blueprintNodeTypeName + '._');
102
+ }
103
+ // --- BlueprintNodeInput (@oneOf with all non-relation node types) ---
104
+ if (isBlueprintOneOf && !isBlueprintRelation) {
105
+ let result = fields;
106
+ result = extend(result, {
107
+ shorthand: fieldWithHooks({ fieldName: 'shorthand' }, () => ({
108
+ type: GraphQLString,
109
+ description: 'String shorthand: just the SuperCase node type name (e.g., "DataTimestamps"). Use when the node type has no required parameters.',
110
+ })),
111
+ }, 'NodeTypeRegistryPlugin: BlueprintNodeInput.shorthand');
112
+ for (const nt of nonRelationNodeTypes) {
113
+ const paramsTypeName = nt.name + 'Params';
114
+ const ParamsType = build.getTypeByName(paramsTypeName);
115
+ if (!ParamsType)
116
+ continue;
117
+ result = extend(result, {
118
+ [nt.name]: fieldWithHooks({ fieldName: nt.name }, () => ({
119
+ type: ParamsType,
120
+ description: 'Parameters for ' + nt.name + ' (' + nt.display_name + ')',
121
+ })),
122
+ }, 'NodeTypeRegistryPlugin: BlueprintNodeInput.' + nt.name);
123
+ }
124
+ return result;
125
+ }
126
+ // --- BlueprintRelationInput (@oneOf with relation node types) ---
127
+ if (isBlueprintOneOf && isBlueprintRelation) {
128
+ let result = fields;
129
+ for (const nt of relationNodeTypes) {
130
+ const paramsTypeName = nt.name + 'Params';
131
+ const ParamsType = build.getTypeByName(paramsTypeName);
132
+ if (!ParamsType)
133
+ continue;
134
+ result = extend(result, {
135
+ [nt.name]: fieldWithHooks({ fieldName: nt.name }, () => ({
136
+ type: ParamsType,
137
+ description: 'Parameters for ' + nt.name + ' (' + nt.display_name + ')',
138
+ })),
139
+ }, 'NodeTypeRegistryPlugin: BlueprintRelationInput.' + nt.name);
140
+ }
141
+ return result;
142
+ }
143
+ // --- BlueprintTableInput ---
144
+ if (isBlueprintTable) {
145
+ const BlueprintNodeInputType = build.getTypeByName(BLUEPRINT_NODE_INPUT);
146
+ return extend(fields, {
147
+ ref: fieldWithHooks({ fieldName: 'ref' }, () => ({
148
+ type: new GraphQLNonNull(GraphQLString),
149
+ description: 'Unique reference key for this table within the blueprint (used for cross-references in relations).',
150
+ })),
151
+ tableName: fieldWithHooks({ fieldName: 'tableName' }, () => ({
152
+ type: new GraphQLNonNull(GraphQLString),
153
+ description: 'The PostgreSQL table name to create.',
154
+ })),
155
+ nodes: fieldWithHooks({ fieldName: 'nodes' }, () => ({
156
+ type: BlueprintNodeInputType
157
+ ? new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(BlueprintNodeInputType)))
158
+ : new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLString))),
159
+ description: 'Array of node type entries (data, authz, field behaviors) to apply to this table.',
160
+ })),
161
+ }, 'NodeTypeRegistryPlugin: BlueprintTableInput fields');
162
+ }
163
+ // --- BlueprintDefinitionInput ---
164
+ if (isBlueprintDefinition) {
165
+ const BlueprintTableInputType = build.getTypeByName(BLUEPRINT_TABLE_INPUT);
166
+ const BlueprintRelationInputType = build.getTypeByName(BLUEPRINT_RELATION_INPUT);
167
+ return extend(fields, {
168
+ tables: fieldWithHooks({ fieldName: 'tables' }, () => ({
169
+ type: BlueprintTableInputType
170
+ ? new GraphQLList(new GraphQLNonNull(BlueprintTableInputType))
171
+ : new GraphQLList(GraphQLString),
172
+ description: 'Array of table definitions.',
173
+ })),
174
+ relations: fieldWithHooks({ fieldName: 'relations' }, () => ({
175
+ type: BlueprintRelationInputType
176
+ ? new GraphQLList(new GraphQLNonNull(BlueprintRelationInputType))
177
+ : new GraphQLList(GraphQLString),
178
+ description: 'Array of relation definitions.',
179
+ })),
180
+ }, 'NodeTypeRegistryPlugin: BlueprintDefinitionInput fields');
181
+ }
182
+ return fields;
183
+ },
184
+ },
185
+ };
186
+ }
187
+ // ============================================================================
188
+ // Gather-phase plugin (production -- queries DB through existing pgService)
189
+ // ============================================================================
190
+ /**
191
+ * NodeTypeRegistryPlugin
192
+ *
193
+ * Gather-phase plugin that queries node_type_registry through the existing
194
+ * pgService connection, then generates @oneOf typed input types in the
195
+ * schema phase. No separate PG pool -- uses the same connection PostGraphile
196
+ * already has (same pattern as PgIntrospectionPlugin / PgEnumTablesPlugin).
197
+ *
198
+ * If the table doesn't exist (42P01) or the query fails, the plugin
199
+ * gracefully skips and no types are registered.
200
+ */
201
+ export const NodeTypeRegistryPlugin = {
202
+ name: 'NodeTypeRegistryPlugin',
203
+ version: '1.0.0',
204
+ description: 'Queries node_type_registry via the existing pgService and generates @oneOf typed input types with SuperCase keys',
205
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
206
+ gather: {
207
+ namespace: 'nodeTypeRegistry',
208
+ version: 1,
209
+ initialState() {
210
+ return { entries: [] };
211
+ },
212
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
213
+ async finalize(info) {
214
+ info.buildInput.nodeTypeRegistryEntries =
215
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
216
+ info.state.entries ?? [];
217
+ },
218
+ hooks: {
219
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
220
+ async pgIntrospection_introspection(info, event) {
221
+ const { serviceName } = event;
222
+ const pgService = info.resolvedPreset.pgServices?.find((svc) => svc.name === serviceName);
223
+ if (!pgService)
224
+ return;
225
+ try {
226
+ const result = await withPgClientFromPgService(pgService, null, (client) => client.query({ text: NODE_TYPE_REGISTRY_QUERY }));
227
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
228
+ const state = info.state;
229
+ if (!state.entries) {
230
+ state.entries = [];
231
+ }
232
+ state.entries.push(...result.rows);
233
+ }
234
+ catch (error) {
235
+ const pgError = error;
236
+ if (pgError.code === '42P01') {
237
+ // 42P01 = undefined_table -- expected when DB hasn't been migrated
238
+ return;
239
+ }
240
+ // Warn but don't fail the schema build
241
+ console.warn('[NodeTypeRegistryPlugin] Failed to query node_type_registry:', pgError.message ?? String(error));
242
+ }
243
+ },
244
+ },
245
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
246
+ },
247
+ schema: buildSchemaHooks((build) => build.input.nodeTypeRegistryEntries ?? []),
248
+ };
249
+ // ============================================================================
250
+ // Preset
251
+ // ============================================================================
252
+ /**
253
+ * Preset that includes the NodeTypeRegistryPlugin.
254
+ *
255
+ * Add to ConstructivePreset.extends[] -- the plugin will automatically
256
+ * query node_type_registry through the existing pgService connection
257
+ * during the gather phase. No separate pool, no manual wiring.
258
+ */
259
+ export const NodeTypeRegistryPreset = {
260
+ plugins: [NodeTypeRegistryPlugin],
261
+ };
262
+ // ============================================================================
263
+ // Legacy exports for backward compatibility
264
+ // ============================================================================
265
+ /**
266
+ * @deprecated Use NodeTypeRegistryPlugin directly. The gather-phase plugin
267
+ * queries node_type_registry automatically via pgService. This factory is
268
+ * kept for backward compatibility and tests that pass static entries.
269
+ */
270
+ export function createBlueprintTypesPlugin(nodeTypes) {
271
+ return {
272
+ name: 'BlueprintTypesPlugin',
273
+ version: '1.0.0',
274
+ description: 'Generates @oneOf typed input types from static node_type_registry entries',
275
+ schema: buildSchemaHooks(() => nodeTypes),
276
+ };
277
+ }
278
+ /**
279
+ * @deprecated Use NodeTypeRegistryPreset directly. The gather-phase plugin
280
+ * queries node_type_registry automatically via pgService. This factory is
281
+ * kept for backward compatibility and tests that pass static entries.
282
+ */
283
+ export function BlueprintTypesPreset(nodeTypes) {
284
+ return {
285
+ plugins: [createBlueprintTypesPlugin(nodeTypes)],
286
+ };
287
+ }
@@ -20,3 +20,5 @@ export { _pgTypeToGqlType, _buildFieldMeta, _cachedTablesMeta } from './meta-sch
20
20
  export { RequiredInputPlugin, RequiredInputPreset, } from './required-input-plugin';
21
21
  export { createUnifiedSearchPlugin, UnifiedSearchPreset, TsvectorCodecPlugin, TsvectorCodecPreset, createTsvectorCodecPlugin, Bm25CodecPlugin, Bm25CodecPreset, bm25IndexStore, VectorCodecPlugin, VectorCodecPreset, createTsvectorAdapter, createBm25Adapter, createTrgmAdapter, createPgvectorAdapter, createMatchesOperatorFactory, createTrgmOperatorFactories, } from 'graphile-search';
22
22
  export type { SearchAdapter, SearchableColumn, UnifiedSearchOptions, UnifiedSearchPresetOptions, TsvectorCodecPluginOptions, Bm25IndexInfo, TsvectorAdapterOptions, Bm25AdapterOptions, TrgmAdapterOptions, PgvectorAdapterOptions, } from 'graphile-search';
23
+ export { NodeTypeRegistryPlugin, NodeTypeRegistryPreset, createBlueprintTypesPlugin, BlueprintTypesPreset, } from './blueprint-types';
24
+ export type { NodeTypeRegistryEntry } from './blueprint-types';
@@ -37,3 +37,8 @@ TsvectorCodecPlugin, TsvectorCodecPreset, createTsvectorCodecPlugin, Bm25CodecPl
37
37
  createTsvectorAdapter, createBm25Adapter, createTrgmAdapter, createPgvectorAdapter,
38
38
  // Operator factories for connection filter integration
39
39
  createMatchesOperatorFactory, createTrgmOperatorFactories, } from 'graphile-search';
40
+ // Node type registry — @oneOf typed input types for blueprint definitions
41
+ // Gather-phase plugin queries node_type_registry through existing pgService
42
+ export { NodeTypeRegistryPlugin, NodeTypeRegistryPreset,
43
+ // Legacy exports for backward compatibility
44
+ createBlueprintTypesPlugin, BlueprintTypesPreset, } from './blueprint-types';
@@ -1,5 +1,5 @@
1
1
  import { ConnectionFilterPreset } from 'graphile-connection-filter';
2
- import { MinimalPreset, InflektPreset, ConflictDetectorPreset, InflectorLoggerPreset, NoUniqueLookupPreset, EnableAllFilterColumnsPreset, ManyToManyOptInPreset, MetaSchemaPreset, PgTypeMappingsPreset, RequiredInputPreset, } from '../plugins';
2
+ import { MinimalPreset, InflektPreset, ConflictDetectorPreset, InflectorLoggerPreset, NoUniqueLookupPreset, EnableAllFilterColumnsPreset, ManyToManyOptInPreset, MetaSchemaPreset, PgTypeMappingsPreset, RequiredInputPreset, NodeTypeRegistryPreset, } from '../plugins';
3
3
  import { UnifiedSearchPreset, createMatchesOperatorFactory, createTrgmOperatorFactories } from 'graphile-search';
4
4
  import { GraphilePostgisPreset, createPostgisOperatorFactory } from 'graphile-postgis';
5
5
  import { UploadPreset } from 'graphile-upload-plugin';
@@ -77,6 +77,7 @@ export const ConstructivePreset = {
77
77
  SqlExpressionValidatorPreset(),
78
78
  PgTypeMappingsPreset,
79
79
  RequiredInputPreset,
80
+ NodeTypeRegistryPreset,
80
81
  ],
81
82
  /**
82
83
  * Disable PostGraphile core's condition argument entirely.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graphile-settings",
3
- "version": "4.10.3",
3
+ "version": "4.12.0",
4
4
  "author": "Constructive <developers@constructive.io>",
5
5
  "description": "graphile settings",
6
6
  "main": "index.js",
@@ -47,7 +47,7 @@
47
47
  "graphile-build-pg": "5.0.0-rc.8",
48
48
  "graphile-config": "1.0.0-rc.6",
49
49
  "graphile-connection-filter": "^1.1.6",
50
- "graphile-postgis": "^2.6.6",
50
+ "graphile-postgis": "^2.7.0",
51
51
  "graphile-search": "^1.3.1",
52
52
  "graphile-sql-expression-validator": "^2.5.0",
53
53
  "graphile-upload-plugin": "^2.4.4",
@@ -80,5 +80,5 @@
80
80
  "constructive",
81
81
  "graphql"
82
82
  ],
83
- "gitHead": "3b3735292589a49601f40645ea7880f584a23b77"
83
+ "gitHead": "b8d4ae2b36e37e7f3533f858f19ec000febaa04b"
84
84
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Node Type Registry Plugin
3
+ *
4
+ * Exports the gather-phase plugin and preset for generating @oneOf typed
5
+ * input types for blueprint definitions from node_type_registry.
6
+ *
7
+ * The NodeTypeRegistryPlugin queries node_type_registry through the existing
8
+ * pgService connection during the gather phase — no separate pool needed.
9
+ */
10
+ export { NodeTypeRegistryPlugin, NodeTypeRegistryPreset, createBlueprintTypesPlugin, BlueprintTypesPreset, } from './plugin';
11
+ export type { NodeTypeRegistryEntry } from './plugin';
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BlueprintTypesPreset = exports.createBlueprintTypesPlugin = exports.NodeTypeRegistryPreset = exports.NodeTypeRegistryPlugin = void 0;
4
+ /**
5
+ * Node Type Registry Plugin
6
+ *
7
+ * Exports the gather-phase plugin and preset for generating @oneOf typed
8
+ * input types for blueprint definitions from node_type_registry.
9
+ *
10
+ * The NodeTypeRegistryPlugin queries node_type_registry through the existing
11
+ * pgService connection during the gather phase — no separate pool needed.
12
+ */
13
+ var plugin_1 = require("./plugin");
14
+ Object.defineProperty(exports, "NodeTypeRegistryPlugin", { enumerable: true, get: function () { return plugin_1.NodeTypeRegistryPlugin; } });
15
+ Object.defineProperty(exports, "NodeTypeRegistryPreset", { enumerable: true, get: function () { return plugin_1.NodeTypeRegistryPreset; } });
16
+ // Legacy exports for backward compatibility
17
+ Object.defineProperty(exports, "createBlueprintTypesPlugin", { enumerable: true, get: function () { return plugin_1.createBlueprintTypesPlugin; } });
18
+ Object.defineProperty(exports, "BlueprintTypesPreset", { enumerable: true, get: function () { return plugin_1.BlueprintTypesPreset; } });
@@ -0,0 +1,64 @@
1
+ /**
2
+ * JSON Schema to GraphQL Input Field Spec Converter
3
+ *
4
+ * Converts JSON Schema objects (from node_type_registry.parameter_schema)
5
+ * into field spec objects compatible with PostGraphile v5's fieldWithHooks API.
6
+ *
7
+ * Handles:
8
+ * - string, integer, number, boolean primitives
9
+ * - arrays (items -> GraphQLList)
10
+ * - enums -> GraphQLEnumType
11
+ * - required fields -> GraphQLNonNull
12
+ * - union types (["integer", "string"]) -> JSON scalar fallback
13
+ */
14
+ import type { GraphQLInputType, GraphQLScalarType, GraphQLNullableType } from 'graphql';
15
+ interface JsonSchema {
16
+ type: string;
17
+ properties?: Record<string, unknown>;
18
+ required?: string[];
19
+ anyOf?: JsonSchema[];
20
+ description?: string;
21
+ }
22
+ /**
23
+ * A field spec that mirrors GraphQLInputFieldConfig.
24
+ */
25
+ export interface FieldSpec {
26
+ type: GraphQLInputType;
27
+ description?: string;
28
+ }
29
+ /**
30
+ * Minimal build interface — only what we need from PostGraphile's Build object.
31
+ *
32
+ * Uses permissive constructor signatures to avoid variance issues between
33
+ * GraphQLNullableType and GraphQLInputType in the graphql-js type hierarchy.
34
+ */
35
+ export interface BuildLike {
36
+ graphql: {
37
+ GraphQLString: GraphQLScalarType;
38
+ GraphQLInt: GraphQLScalarType;
39
+ GraphQLFloat: GraphQLScalarType;
40
+ GraphQLBoolean: GraphQLScalarType;
41
+ GraphQLNonNull: new (type: any) => GraphQLInputType;
42
+ GraphQLList: new (type: any) => GraphQLInputType & GraphQLNullableType;
43
+ GraphQLEnumType: new (config: {
44
+ name: string;
45
+ values: Record<string, {
46
+ value: string;
47
+ }>;
48
+ }) => GraphQLInputType & GraphQLNullableType;
49
+ };
50
+ getTypeByName: (name: string) => unknown;
51
+ }
52
+ /**
53
+ * Convert a full JSON Schema (from parameter_schema) to field specs.
54
+ *
55
+ * Uses the build object from PostGraphile v5 to access GraphQL types,
56
+ * avoiding direct graphql imports (which can cause version mismatches).
57
+ *
58
+ * @param schema - The JSON Schema object
59
+ * @param typeName - The name prefix for generated enum types
60
+ * @param build - The PostGraphile build object
61
+ * @returns Record of field name -> FieldSpec
62
+ */
63
+ export declare function jsonSchemaToGraphQLFieldSpecs(schema: JsonSchema, typeName: string, build: BuildLike): Record<string, FieldSpec>;
64
+ export {};
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ /**
3
+ * JSON Schema to GraphQL Input Field Spec Converter
4
+ *
5
+ * Converts JSON Schema objects (from node_type_registry.parameter_schema)
6
+ * into field spec objects compatible with PostGraphile v5's fieldWithHooks API.
7
+ *
8
+ * Handles:
9
+ * - string, integer, number, boolean primitives
10
+ * - arrays (items -> GraphQLList)
11
+ * - enums -> GraphQLEnumType
12
+ * - required fields -> GraphQLNonNull
13
+ * - union types (["integer", "string"]) -> JSON scalar fallback
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.jsonSchemaToGraphQLFieldSpecs = jsonSchemaToGraphQLFieldSpecs;
17
+ /**
18
+ * Convert a JSON Schema type string to a GraphQL scalar type.
19
+ */
20
+ function jsonTypeToGraphQL(jsonType, graphql, jsonScalar) {
21
+ if (Array.isArray(jsonType)) {
22
+ return jsonScalar;
23
+ }
24
+ switch (jsonType) {
25
+ case 'string':
26
+ return graphql.GraphQLString;
27
+ case 'integer':
28
+ return graphql.GraphQLInt;
29
+ case 'number':
30
+ return graphql.GraphQLFloat;
31
+ case 'boolean':
32
+ return graphql.GraphQLBoolean;
33
+ case 'object':
34
+ return jsonScalar;
35
+ case 'array':
36
+ return new graphql.GraphQLList(jsonScalar);
37
+ default:
38
+ return jsonScalar;
39
+ }
40
+ }
41
+ /**
42
+ * Convert a full JSON Schema (from parameter_schema) to field specs.
43
+ *
44
+ * Uses the build object from PostGraphile v5 to access GraphQL types,
45
+ * avoiding direct graphql imports (which can cause version mismatches).
46
+ *
47
+ * @param schema - The JSON Schema object
48
+ * @param typeName - The name prefix for generated enum types
49
+ * @param build - The PostGraphile build object
50
+ * @returns Record of field name -> FieldSpec
51
+ */
52
+ function jsonSchemaToGraphQLFieldSpecs(schema, typeName, build) {
53
+ const fields = {};
54
+ const properties = schema.properties;
55
+ if (!properties) {
56
+ return fields;
57
+ }
58
+ const { graphql } = build;
59
+ const jsonScalar = (build.getTypeByName('JSON') ?? graphql.GraphQLString);
60
+ const required = new Set(schema.required ?? []);
61
+ for (const [propName, prop] of Object.entries(properties)) {
62
+ let fieldType;
63
+ if (prop.enum && prop.enum.length > 0) {
64
+ const values = {};
65
+ for (const val of prop.enum) {
66
+ const safeName = val.replace(/[^_a-zA-Z0-9]/g, '_').toUpperCase();
67
+ values[safeName] = { value: val };
68
+ }
69
+ fieldType = new graphql.GraphQLEnumType({
70
+ name: `${typeName}_${propName}`,
71
+ values,
72
+ });
73
+ }
74
+ else if (prop.type === 'array' && prop.items) {
75
+ const innerType = prop.items.type
76
+ ? jsonTypeToGraphQL(prop.items.type, graphql, jsonScalar)
77
+ : jsonScalar;
78
+ fieldType = new graphql.GraphQLList(innerType);
79
+ }
80
+ else {
81
+ fieldType = jsonTypeToGraphQL(prop.type ?? 'string', graphql, jsonScalar);
82
+ }
83
+ if (required.has(propName)) {
84
+ fieldType = new graphql.GraphQLNonNull(fieldType);
85
+ }
86
+ fields[propName] = {
87
+ type: fieldType,
88
+ description: prop.description ?? undefined,
89
+ };
90
+ }
91
+ return fields;
92
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Node Type Registry Plugin
3
+ *
4
+ * Generic PostGraphile v5 plugin that queries node_type_registry at schema
5
+ * build time through the existing pgService connection (gather phase) and
6
+ * generates @oneOf typed input types with SuperCase node type names as
7
+ * discriminant keys.
8
+ *
9
+ * Architecture (correct PostGraphile v5 pattern):
10
+ * 1. `gather` phase: hooks into pgIntrospection_introspection to query
11
+ * node_type_registry through the existing pgService (no separate pool —
12
+ * uses withPgClientFromPgService like PgIntrospectionPlugin)
13
+ * 2. `init` hook: registers all input type shells from gathered entries
14
+ * 3. `GraphQLInputObjectType_fields` hook: populates fields on those types
15
+ *
16
+ * Generated type hierarchy:
17
+ *
18
+ * BlueprintDefinitionInput
19
+ * +-- tables: [BlueprintTableInput!]
20
+ * | +-- ref: String!
21
+ * | +-- tableName: String!
22
+ * | +-- nodes: [BlueprintNodeInput!]! <-- @oneOf
23
+ * | +-- shorthand: String
24
+ * | +-- DataId: DataIdParams
25
+ * | +-- DataTimestamps: DataTimestampsParams
26
+ * | +-- AuthzEntityMembership: AuthzEntityMembershipParams
27
+ * | +-- ...
28
+ * +-- relations: [BlueprintRelationInput!] <-- @oneOf
29
+ * +-- RelationBelongsTo: RelationBelongsToParams
30
+ * +-- ...
31
+ *
32
+ * When codegen runs, @oneOf types become discriminated union types:
33
+ *
34
+ * type BlueprintNodeInput =
35
+ * | { DataId: DataIdParams }
36
+ * | { DataTimestamps: DataTimestampsParams }
37
+ * | { AuthzEntityMembership: AuthzEntityMembershipParams }
38
+ */
39
+ import type { GraphileConfig } from 'graphile-config';
40
+ export interface NodeTypeRegistryEntry {
41
+ name: string;
42
+ slug: string;
43
+ category: string;
44
+ display_name: string;
45
+ description: string;
46
+ parameter_schema: Record<string, unknown>;
47
+ tags: string[];
48
+ }
49
+ declare global {
50
+ namespace GraphileBuild {
51
+ interface ScopeInputObject {
52
+ isBlueprintOneOf?: boolean;
53
+ isBlueprintNodeParams?: boolean;
54
+ isBlueprintTable?: boolean;
55
+ isBlueprintDefinition?: boolean;
56
+ isBlueprintRelation?: boolean;
57
+ blueprintNodeTypeName?: string;
58
+ }
59
+ interface BuildInput {
60
+ nodeTypeRegistryEntries?: NodeTypeRegistryEntry[];
61
+ }
62
+ }
63
+ }
64
+ /**
65
+ * NodeTypeRegistryPlugin
66
+ *
67
+ * Gather-phase plugin that queries node_type_registry through the existing
68
+ * pgService connection, then generates @oneOf typed input types in the
69
+ * schema phase. No separate PG pool -- uses the same connection PostGraphile
70
+ * already has (same pattern as PgIntrospectionPlugin / PgEnumTablesPlugin).
71
+ *
72
+ * If the table doesn't exist (42P01) or the query fails, the plugin
73
+ * gracefully skips and no types are registered.
74
+ */
75
+ export declare const NodeTypeRegistryPlugin: GraphileConfig.Plugin;
76
+ /**
77
+ * Preset that includes the NodeTypeRegistryPlugin.
78
+ *
79
+ * Add to ConstructivePreset.extends[] -- the plugin will automatically
80
+ * query node_type_registry through the existing pgService connection
81
+ * during the gather phase. No separate pool, no manual wiring.
82
+ */
83
+ export declare const NodeTypeRegistryPreset: GraphileConfig.Preset;
84
+ /**
85
+ * @deprecated Use NodeTypeRegistryPlugin directly. The gather-phase plugin
86
+ * queries node_type_registry automatically via pgService. This factory is
87
+ * kept for backward compatibility and tests that pass static entries.
88
+ */
89
+ export declare function createBlueprintTypesPlugin(nodeTypes: NodeTypeRegistryEntry[]): GraphileConfig.Plugin;
90
+ /**
91
+ * @deprecated Use NodeTypeRegistryPreset directly. The gather-phase plugin
92
+ * queries node_type_registry automatically via pgService. This factory is
93
+ * kept for backward compatibility and tests that pass static entries.
94
+ */
95
+ export declare function BlueprintTypesPreset(nodeTypes: NodeTypeRegistryEntry[]): GraphileConfig.Preset;
@@ -0,0 +1,292 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NodeTypeRegistryPreset = exports.NodeTypeRegistryPlugin = void 0;
4
+ exports.createBlueprintTypesPlugin = createBlueprintTypesPlugin;
5
+ exports.BlueprintTypesPreset = BlueprintTypesPreset;
6
+ const graphile_build_pg_1 = require("graphile-build-pg");
7
+ const json_schema_to_graphql_1 = require("./json-schema-to-graphql");
8
+ // ============================================================================
9
+ // Constants
10
+ // ============================================================================
11
+ const BLUEPRINT_NODE_INPUT = 'BlueprintNodeInput';
12
+ const BLUEPRINT_TABLE_INPUT = 'BlueprintTableInput';
13
+ const BLUEPRINT_RELATION_INPUT = 'BlueprintRelationInput';
14
+ const BLUEPRINT_DEFINITION_INPUT = 'BlueprintDefinitionInput';
15
+ const NODE_TYPE_REGISTRY_QUERY = `
16
+ SELECT
17
+ name,
18
+ slug,
19
+ category,
20
+ COALESCE(display_name, name) as display_name,
21
+ COALESCE(description, '') as description,
22
+ COALESCE(parameter_schema, '{}'::jsonb) as parameter_schema,
23
+ COALESCE(tags, '{}') as tags
24
+ FROM metaschema_public.node_type_registry
25
+ ORDER BY category, name
26
+ `;
27
+ // ============================================================================
28
+ // Shared schema hooks
29
+ // ============================================================================
30
+ /**
31
+ * Builds the schema hooks (init + GraphQLInputObjectType_fields) from
32
+ * a function that retrieves node type entries. Used by both the gather-phase
33
+ * plugin (reads from build.input) and the static/legacy plugin (closure).
34
+ */
35
+ function buildSchemaHooks(getNodeTypes) {
36
+ return {
37
+ hooks: {
38
+ init(_, build) {
39
+ const nodeTypes = getNodeTypes(build);
40
+ if (nodeTypes.length === 0)
41
+ return _;
42
+ for (const nt of nodeTypes) {
43
+ const paramsTypeName = nt.name + 'Params';
44
+ build.registerInputObjectType(paramsTypeName, {
45
+ isBlueprintNodeParams: true,
46
+ blueprintNodeTypeName: nt.name,
47
+ }, () => ({
48
+ description: nt.description,
49
+ }), 'NodeTypeRegistryPlugin: ' + paramsTypeName);
50
+ }
51
+ build.registerInputObjectType(BLUEPRINT_NODE_INPUT, { isBlueprintOneOf: true }, () => ({
52
+ description: 'A single node in a blueprint definition. Exactly one field must be set. Use the SuperCase node type name as the key with its parameters as the value, or use "shorthand" with just the node type name string.',
53
+ isOneOf: true,
54
+ }), 'NodeTypeRegistryPlugin: BlueprintNodeInput');
55
+ build.registerInputObjectType(BLUEPRINT_RELATION_INPUT, { isBlueprintOneOf: true, isBlueprintRelation: true }, () => ({
56
+ description: 'A relation in a blueprint definition. Exactly one field must be set. Use the SuperCase relation type name as the key.',
57
+ isOneOf: true,
58
+ }), 'NodeTypeRegistryPlugin: BlueprintRelationInput');
59
+ build.registerInputObjectType(BLUEPRINT_TABLE_INPUT, { isBlueprintTable: true }, () => ({
60
+ description: 'A table definition within a blueprint. Specifies the table reference, name, and an array of typed nodes.',
61
+ }), 'NodeTypeRegistryPlugin: BlueprintTableInput');
62
+ build.registerInputObjectType(BLUEPRINT_DEFINITION_INPUT, { isBlueprintDefinition: true }, () => ({
63
+ description: 'The complete blueprint definition. Contains tables with typed nodes and relations.',
64
+ }), 'NodeTypeRegistryPlugin: BlueprintDefinitionInput');
65
+ return _;
66
+ },
67
+ GraphQLInputObjectType_fields(fields, build, context) {
68
+ const nodeTypes = getNodeTypes(build);
69
+ if (nodeTypes.length === 0)
70
+ return fields;
71
+ const nodeTypesByName = new Map();
72
+ const relationNodeTypes = [];
73
+ const nonRelationNodeTypes = [];
74
+ for (const nt of nodeTypes) {
75
+ nodeTypesByName.set(nt.name, nt);
76
+ if (nt.category === 'relation') {
77
+ relationNodeTypes.push(nt);
78
+ }
79
+ else {
80
+ nonRelationNodeTypes.push(nt);
81
+ }
82
+ }
83
+ const { extend, graphql: { GraphQLString, GraphQLNonNull, GraphQLList }, } = build;
84
+ const { fieldWithHooks, scope: { isBlueprintOneOf, isBlueprintNodeParams, isBlueprintTable, isBlueprintDefinition, isBlueprintRelation, blueprintNodeTypeName, }, } = context;
85
+ // --- Per-node-type params (e.g., DataIdParams) ---
86
+ if (isBlueprintNodeParams && blueprintNodeTypeName) {
87
+ const nt = nodeTypesByName.get(blueprintNodeTypeName);
88
+ if (!nt)
89
+ return fields;
90
+ const schema = nt.parameter_schema;
91
+ const fieldSpecs = (0, json_schema_to_graphql_1.jsonSchemaToGraphQLFieldSpecs)(schema, nt.name + 'Params', build);
92
+ if (Object.keys(fieldSpecs).length > 0) {
93
+ let result = fields;
94
+ for (const [fieldName, spec] of Object.entries(fieldSpecs)) {
95
+ result = extend(result, {
96
+ [fieldName]: fieldWithHooks({ fieldName }, () => spec),
97
+ }, 'NodeTypeRegistryPlugin: ' + blueprintNodeTypeName + '.' + fieldName);
98
+ }
99
+ return result;
100
+ }
101
+ return extend(fields, {
102
+ _: fieldWithHooks({ fieldName: '_' }, () => ({
103
+ type: build.graphql.GraphQLBoolean,
104
+ description: 'No parameters required. Pass true or omit entirely.',
105
+ })),
106
+ }, 'NodeTypeRegistryPlugin: ' + blueprintNodeTypeName + '._');
107
+ }
108
+ // --- BlueprintNodeInput (@oneOf with all non-relation node types) ---
109
+ if (isBlueprintOneOf && !isBlueprintRelation) {
110
+ let result = fields;
111
+ result = extend(result, {
112
+ shorthand: fieldWithHooks({ fieldName: 'shorthand' }, () => ({
113
+ type: GraphQLString,
114
+ description: 'String shorthand: just the SuperCase node type name (e.g., "DataTimestamps"). Use when the node type has no required parameters.',
115
+ })),
116
+ }, 'NodeTypeRegistryPlugin: BlueprintNodeInput.shorthand');
117
+ for (const nt of nonRelationNodeTypes) {
118
+ const paramsTypeName = nt.name + 'Params';
119
+ const ParamsType = build.getTypeByName(paramsTypeName);
120
+ if (!ParamsType)
121
+ continue;
122
+ result = extend(result, {
123
+ [nt.name]: fieldWithHooks({ fieldName: nt.name }, () => ({
124
+ type: ParamsType,
125
+ description: 'Parameters for ' + nt.name + ' (' + nt.display_name + ')',
126
+ })),
127
+ }, 'NodeTypeRegistryPlugin: BlueprintNodeInput.' + nt.name);
128
+ }
129
+ return result;
130
+ }
131
+ // --- BlueprintRelationInput (@oneOf with relation node types) ---
132
+ if (isBlueprintOneOf && isBlueprintRelation) {
133
+ let result = fields;
134
+ for (const nt of relationNodeTypes) {
135
+ const paramsTypeName = nt.name + 'Params';
136
+ const ParamsType = build.getTypeByName(paramsTypeName);
137
+ if (!ParamsType)
138
+ continue;
139
+ result = extend(result, {
140
+ [nt.name]: fieldWithHooks({ fieldName: nt.name }, () => ({
141
+ type: ParamsType,
142
+ description: 'Parameters for ' + nt.name + ' (' + nt.display_name + ')',
143
+ })),
144
+ }, 'NodeTypeRegistryPlugin: BlueprintRelationInput.' + nt.name);
145
+ }
146
+ return result;
147
+ }
148
+ // --- BlueprintTableInput ---
149
+ if (isBlueprintTable) {
150
+ const BlueprintNodeInputType = build.getTypeByName(BLUEPRINT_NODE_INPUT);
151
+ return extend(fields, {
152
+ ref: fieldWithHooks({ fieldName: 'ref' }, () => ({
153
+ type: new GraphQLNonNull(GraphQLString),
154
+ description: 'Unique reference key for this table within the blueprint (used for cross-references in relations).',
155
+ })),
156
+ tableName: fieldWithHooks({ fieldName: 'tableName' }, () => ({
157
+ type: new GraphQLNonNull(GraphQLString),
158
+ description: 'The PostgreSQL table name to create.',
159
+ })),
160
+ nodes: fieldWithHooks({ fieldName: 'nodes' }, () => ({
161
+ type: BlueprintNodeInputType
162
+ ? new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(BlueprintNodeInputType)))
163
+ : new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLString))),
164
+ description: 'Array of node type entries (data, authz, field behaviors) to apply to this table.',
165
+ })),
166
+ }, 'NodeTypeRegistryPlugin: BlueprintTableInput fields');
167
+ }
168
+ // --- BlueprintDefinitionInput ---
169
+ if (isBlueprintDefinition) {
170
+ const BlueprintTableInputType = build.getTypeByName(BLUEPRINT_TABLE_INPUT);
171
+ const BlueprintRelationInputType = build.getTypeByName(BLUEPRINT_RELATION_INPUT);
172
+ return extend(fields, {
173
+ tables: fieldWithHooks({ fieldName: 'tables' }, () => ({
174
+ type: BlueprintTableInputType
175
+ ? new GraphQLList(new GraphQLNonNull(BlueprintTableInputType))
176
+ : new GraphQLList(GraphQLString),
177
+ description: 'Array of table definitions.',
178
+ })),
179
+ relations: fieldWithHooks({ fieldName: 'relations' }, () => ({
180
+ type: BlueprintRelationInputType
181
+ ? new GraphQLList(new GraphQLNonNull(BlueprintRelationInputType))
182
+ : new GraphQLList(GraphQLString),
183
+ description: 'Array of relation definitions.',
184
+ })),
185
+ }, 'NodeTypeRegistryPlugin: BlueprintDefinitionInput fields');
186
+ }
187
+ return fields;
188
+ },
189
+ },
190
+ };
191
+ }
192
+ // ============================================================================
193
+ // Gather-phase plugin (production -- queries DB through existing pgService)
194
+ // ============================================================================
195
+ /**
196
+ * NodeTypeRegistryPlugin
197
+ *
198
+ * Gather-phase plugin that queries node_type_registry through the existing
199
+ * pgService connection, then generates @oneOf typed input types in the
200
+ * schema phase. No separate PG pool -- uses the same connection PostGraphile
201
+ * already has (same pattern as PgIntrospectionPlugin / PgEnumTablesPlugin).
202
+ *
203
+ * If the table doesn't exist (42P01) or the query fails, the plugin
204
+ * gracefully skips and no types are registered.
205
+ */
206
+ exports.NodeTypeRegistryPlugin = {
207
+ name: 'NodeTypeRegistryPlugin',
208
+ version: '1.0.0',
209
+ description: 'Queries node_type_registry via the existing pgService and generates @oneOf typed input types with SuperCase keys',
210
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
211
+ gather: {
212
+ namespace: 'nodeTypeRegistry',
213
+ version: 1,
214
+ initialState() {
215
+ return { entries: [] };
216
+ },
217
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
218
+ async finalize(info) {
219
+ info.buildInput.nodeTypeRegistryEntries =
220
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
221
+ info.state.entries ?? [];
222
+ },
223
+ hooks: {
224
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
225
+ async pgIntrospection_introspection(info, event) {
226
+ const { serviceName } = event;
227
+ const pgService = info.resolvedPreset.pgServices?.find((svc) => svc.name === serviceName);
228
+ if (!pgService)
229
+ return;
230
+ try {
231
+ const result = await (0, graphile_build_pg_1.withPgClientFromPgService)(pgService, null, (client) => client.query({ text: NODE_TYPE_REGISTRY_QUERY }));
232
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
233
+ const state = info.state;
234
+ if (!state.entries) {
235
+ state.entries = [];
236
+ }
237
+ state.entries.push(...result.rows);
238
+ }
239
+ catch (error) {
240
+ const pgError = error;
241
+ if (pgError.code === '42P01') {
242
+ // 42P01 = undefined_table -- expected when DB hasn't been migrated
243
+ return;
244
+ }
245
+ // Warn but don't fail the schema build
246
+ console.warn('[NodeTypeRegistryPlugin] Failed to query node_type_registry:', pgError.message ?? String(error));
247
+ }
248
+ },
249
+ },
250
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
251
+ },
252
+ schema: buildSchemaHooks((build) => build.input.nodeTypeRegistryEntries ?? []),
253
+ };
254
+ // ============================================================================
255
+ // Preset
256
+ // ============================================================================
257
+ /**
258
+ * Preset that includes the NodeTypeRegistryPlugin.
259
+ *
260
+ * Add to ConstructivePreset.extends[] -- the plugin will automatically
261
+ * query node_type_registry through the existing pgService connection
262
+ * during the gather phase. No separate pool, no manual wiring.
263
+ */
264
+ exports.NodeTypeRegistryPreset = {
265
+ plugins: [exports.NodeTypeRegistryPlugin],
266
+ };
267
+ // ============================================================================
268
+ // Legacy exports for backward compatibility
269
+ // ============================================================================
270
+ /**
271
+ * @deprecated Use NodeTypeRegistryPlugin directly. The gather-phase plugin
272
+ * queries node_type_registry automatically via pgService. This factory is
273
+ * kept for backward compatibility and tests that pass static entries.
274
+ */
275
+ function createBlueprintTypesPlugin(nodeTypes) {
276
+ return {
277
+ name: 'BlueprintTypesPlugin',
278
+ version: '1.0.0',
279
+ description: 'Generates @oneOf typed input types from static node_type_registry entries',
280
+ schema: buildSchemaHooks(() => nodeTypes),
281
+ };
282
+ }
283
+ /**
284
+ * @deprecated Use NodeTypeRegistryPreset directly. The gather-phase plugin
285
+ * queries node_type_registry automatically via pgService. This factory is
286
+ * kept for backward compatibility and tests that pass static entries.
287
+ */
288
+ function BlueprintTypesPreset(nodeTypes) {
289
+ return {
290
+ plugins: [createBlueprintTypesPlugin(nodeTypes)],
291
+ };
292
+ }
@@ -20,3 +20,5 @@ export { _pgTypeToGqlType, _buildFieldMeta, _cachedTablesMeta } from './meta-sch
20
20
  export { RequiredInputPlugin, RequiredInputPreset, } from './required-input-plugin';
21
21
  export { createUnifiedSearchPlugin, UnifiedSearchPreset, TsvectorCodecPlugin, TsvectorCodecPreset, createTsvectorCodecPlugin, Bm25CodecPlugin, Bm25CodecPreset, bm25IndexStore, VectorCodecPlugin, VectorCodecPreset, createTsvectorAdapter, createBm25Adapter, createTrgmAdapter, createPgvectorAdapter, createMatchesOperatorFactory, createTrgmOperatorFactories, } from 'graphile-search';
22
22
  export type { SearchAdapter, SearchableColumn, UnifiedSearchOptions, UnifiedSearchPresetOptions, TsvectorCodecPluginOptions, Bm25IndexInfo, TsvectorAdapterOptions, Bm25AdapterOptions, TrgmAdapterOptions, PgvectorAdapterOptions, } from 'graphile-search';
23
+ export { NodeTypeRegistryPlugin, NodeTypeRegistryPreset, createBlueprintTypesPlugin, BlueprintTypesPreset, } from './blueprint-types';
24
+ export type { NodeTypeRegistryEntry } from './blueprint-types';
package/plugins/index.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * This module exports all custom plugins (consolidated from graphile-misc-plugins).
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.createTrgmOperatorFactories = exports.createMatchesOperatorFactory = exports.createPgvectorAdapter = exports.createTrgmAdapter = exports.createBm25Adapter = exports.createTsvectorAdapter = exports.VectorCodecPreset = exports.VectorCodecPlugin = exports.bm25IndexStore = exports.Bm25CodecPreset = exports.Bm25CodecPlugin = exports.createTsvectorCodecPlugin = exports.TsvectorCodecPreset = exports.TsvectorCodecPlugin = exports.UnifiedSearchPreset = exports.createUnifiedSearchPlugin = exports.RequiredInputPreset = exports.RequiredInputPlugin = exports._cachedTablesMeta = exports._buildFieldMeta = exports._pgTypeToGqlType = exports.PublicKeySignature = exports.PgTypeMappingsPreset = exports.PgTypeMappingsPlugin = exports.MetaSchemaPreset = exports.MetaSchemaPlugin = exports.NoUniqueLookupPreset = exports.PrimaryKeyOnlyPreset = exports.NoUniqueLookupPlugin = exports.PrimaryKeyOnlyPlugin = exports.createUniqueLookupPlugin = exports.ManyToManyOptInPreset = exports.ManyToManyOptInPlugin = exports.EnableAllFilterColumnsPreset = exports.EnableAllFilterColumnsPlugin = exports.InflectorLoggerPreset = exports.InflectorLoggerPlugin = exports.ConflictDetectorPreset = exports.ConflictDetectorPlugin = exports.CustomInflectorPreset = exports.CustomInflectorPlugin = exports.InflektPreset = exports.InflektPlugin = exports.MinimalPreset = void 0;
8
+ exports.BlueprintTypesPreset = exports.createBlueprintTypesPlugin = exports.NodeTypeRegistryPreset = exports.NodeTypeRegistryPlugin = exports.createTrgmOperatorFactories = exports.createMatchesOperatorFactory = exports.createPgvectorAdapter = exports.createTrgmAdapter = exports.createBm25Adapter = exports.createTsvectorAdapter = exports.VectorCodecPreset = exports.VectorCodecPlugin = exports.bm25IndexStore = exports.Bm25CodecPreset = exports.Bm25CodecPlugin = exports.createTsvectorCodecPlugin = exports.TsvectorCodecPreset = exports.TsvectorCodecPlugin = exports.UnifiedSearchPreset = exports.createUnifiedSearchPlugin = exports.RequiredInputPreset = exports.RequiredInputPlugin = exports._cachedTablesMeta = exports._buildFieldMeta = exports._pgTypeToGqlType = exports.PublicKeySignature = exports.PgTypeMappingsPreset = exports.PgTypeMappingsPlugin = exports.MetaSchemaPreset = exports.MetaSchemaPlugin = exports.NoUniqueLookupPreset = exports.PrimaryKeyOnlyPreset = exports.NoUniqueLookupPlugin = exports.PrimaryKeyOnlyPlugin = exports.createUniqueLookupPlugin = exports.ManyToManyOptInPreset = exports.ManyToManyOptInPlugin = exports.EnableAllFilterColumnsPreset = exports.EnableAllFilterColumnsPlugin = exports.InflectorLoggerPreset = exports.InflectorLoggerPlugin = exports.ConflictDetectorPreset = exports.ConflictDetectorPlugin = exports.CustomInflectorPreset = exports.CustomInflectorPlugin = exports.InflektPreset = exports.InflektPlugin = exports.MinimalPreset = void 0;
9
9
  // Minimal preset - PostGraphile without Node/Relay features
10
10
  var minimal_preset_1 = require("./minimal-preset");
11
11
  Object.defineProperty(exports, "MinimalPreset", { enumerable: true, get: function () { return minimal_preset_1.MinimalPreset; } });
@@ -80,3 +80,11 @@ Object.defineProperty(exports, "createPgvectorAdapter", { enumerable: true, get:
80
80
  // Operator factories for connection filter integration
81
81
  Object.defineProperty(exports, "createMatchesOperatorFactory", { enumerable: true, get: function () { return graphile_search_1.createMatchesOperatorFactory; } });
82
82
  Object.defineProperty(exports, "createTrgmOperatorFactories", { enumerable: true, get: function () { return graphile_search_1.createTrgmOperatorFactories; } });
83
+ // Node type registry — @oneOf typed input types for blueprint definitions
84
+ // Gather-phase plugin queries node_type_registry through existing pgService
85
+ var blueprint_types_1 = require("./blueprint-types");
86
+ Object.defineProperty(exports, "NodeTypeRegistryPlugin", { enumerable: true, get: function () { return blueprint_types_1.NodeTypeRegistryPlugin; } });
87
+ Object.defineProperty(exports, "NodeTypeRegistryPreset", { enumerable: true, get: function () { return blueprint_types_1.NodeTypeRegistryPreset; } });
88
+ // Legacy exports for backward compatibility
89
+ Object.defineProperty(exports, "createBlueprintTypesPlugin", { enumerable: true, get: function () { return blueprint_types_1.createBlueprintTypesPlugin; } });
90
+ Object.defineProperty(exports, "BlueprintTypesPreset", { enumerable: true, get: function () { return blueprint_types_1.BlueprintTypesPreset; } });
@@ -80,6 +80,7 @@ exports.ConstructivePreset = {
80
80
  (0, graphile_sql_expression_validator_1.SqlExpressionValidatorPreset)(),
81
81
  plugins_1.PgTypeMappingsPreset,
82
82
  plugins_1.RequiredInputPreset,
83
+ plugins_1.NodeTypeRegistryPreset,
83
84
  ],
84
85
  /**
85
86
  * Disable PostGraphile core's condition argument entirely.