@ankhorage/data-sources 1.0.1 → 2.0.1

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.
Files changed (120) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +6 -41
  3. package/dist/ankh.provider.js +1 -1
  4. package/dist/ankh.provider.js.map +1 -1
  5. package/dist/discovery/http.d.ts +10 -0
  6. package/dist/discovery/http.d.ts.map +1 -0
  7. package/dist/discovery/http.js +35 -0
  8. package/dist/discovery/http.js.map +1 -0
  9. package/dist/discovery/index.d.ts +9 -9
  10. package/dist/discovery/index.d.ts.map +1 -1
  11. package/dist/discovery/index.js +27 -55
  12. package/dist/discovery/index.js.map +1 -1
  13. package/dist/graphql/index.d.ts +8 -76
  14. package/dist/graphql/index.d.ts.map +1 -1
  15. package/dist/graphql/index.js +17 -197
  16. package/dist/graphql/index.js.map +1 -1
  17. package/dist/graphql/introspection.d.ts +5 -0
  18. package/dist/graphql/introspection.d.ts.map +1 -0
  19. package/dist/graphql/introspection.js +136 -0
  20. package/dist/graphql/introspection.js.map +1 -0
  21. package/dist/graphql/operation.d.ts +5 -0
  22. package/dist/graphql/operation.d.ts.map +1 -0
  23. package/dist/graphql/operation.js +39 -0
  24. package/dist/graphql/operation.js.map +1 -0
  25. package/dist/graphql/types.d.ts +71 -0
  26. package/dist/graphql/types.d.ts.map +1 -0
  27. package/dist/graphql/types.js +2 -0
  28. package/dist/graphql/types.js.map +1 -0
  29. package/dist/index.d.ts +1 -17
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +1 -34
  32. package/dist/index.js.map +1 -1
  33. package/dist/openapi/endpoints.d.ts +5 -0
  34. package/dist/openapi/endpoints.d.ts.map +1 -0
  35. package/dist/openapi/endpoints.js +77 -0
  36. package/dist/openapi/endpoints.js.map +1 -0
  37. package/dist/openapi/index.d.ts +7 -87
  38. package/dist/openapi/index.d.ts.map +1 -1
  39. package/dist/openapi/index.js +8 -267
  40. package/dist/openapi/index.js.map +1 -1
  41. package/dist/openapi/operation.d.ts +5 -0
  42. package/dist/openapi/operation.d.ts.map +1 -0
  43. package/dist/openapi/operation.js +141 -0
  44. package/dist/openapi/operation.js.map +1 -0
  45. package/dist/openapi/schema.d.ts +5 -0
  46. package/dist/openapi/schema.d.ts.map +1 -0
  47. package/dist/openapi/schema.js +84 -0
  48. package/dist/openapi/schema.js.map +1 -0
  49. package/dist/openapi/types.d.ts +82 -0
  50. package/dist/openapi/types.d.ts.map +1 -0
  51. package/dist/openapi/types.js +2 -0
  52. package/dist/openapi/types.js.map +1 -0
  53. package/dist/packageInfo.d.ts +18 -0
  54. package/dist/packageInfo.d.ts.map +1 -0
  55. package/dist/packageInfo.js +30 -0
  56. package/dist/packageInfo.js.map +1 -0
  57. package/dist/rest/index.d.ts +5 -5
  58. package/dist/rest/index.d.ts.map +1 -1
  59. package/dist/rest/index.js +49 -48
  60. package/dist/rest/index.js.map +1 -1
  61. package/dist/test-runner/index.d.ts +2 -57
  62. package/dist/test-runner/index.d.ts.map +1 -1
  63. package/dist/test-runner/index.js +58 -160
  64. package/dist/test-runner/index.js.map +1 -1
  65. package/dist/test-runner/response.d.ts +3 -0
  66. package/dist/test-runner/response.d.ts.map +1 -0
  67. package/dist/test-runner/response.js +29 -0
  68. package/dist/test-runner/response.js.map +1 -0
  69. package/dist/test-runner/result.d.ts +6 -0
  70. package/dist/test-runner/result.d.ts.map +1 -0
  71. package/dist/test-runner/result.js +17 -0
  72. package/dist/test-runner/result.js.map +1 -0
  73. package/dist/test-runner/serialization.d.ts +12 -0
  74. package/dist/test-runner/serialization.d.ts.map +1 -0
  75. package/dist/test-runner/serialization.js +99 -0
  76. package/dist/test-runner/serialization.js.map +1 -0
  77. package/dist/test-runner/types.d.ts +58 -0
  78. package/dist/test-runner/types.d.ts.map +1 -0
  79. package/dist/test-runner/types.js +2 -0
  80. package/dist/test-runner/types.js.map +1 -0
  81. package/package.json +12 -13
  82. package/src/ankh.provider.ts +1 -1
  83. package/src/api-phase1.contract.test.ts +24 -0
  84. package/src/discovery/http.ts +38 -0
  85. package/src/discovery/index.test.ts +76 -75
  86. package/src/discovery/index.ts +49 -72
  87. package/src/graphql/index.test.ts +10 -11
  88. package/src/graphql/index.ts +35 -315
  89. package/src/graphql/introspection.ts +163 -0
  90. package/src/graphql/operation.ts +60 -0
  91. package/src/graphql/types.ts +79 -0
  92. package/src/index.test.ts +21 -13
  93. package/src/index.ts +1 -54
  94. package/src/openapi/endpoints.ts +124 -0
  95. package/src/openapi/index.test.ts +47 -47
  96. package/src/openapi/index.ts +12 -467
  97. package/src/openapi/operation.ts +231 -0
  98. package/src/openapi/schema.ts +127 -0
  99. package/src/openapi/types.ts +93 -0
  100. package/src/packageInfo.ts +50 -0
  101. package/src/rest/index.test.ts +17 -14
  102. package/src/rest/index.ts +67 -61
  103. package/src/test-runner/index.test.ts +153 -120
  104. package/src/test-runner/index.ts +97 -299
  105. package/src/test-runner/response.ts +32 -0
  106. package/src/test-runner/result.ts +35 -0
  107. package/src/test-runner/serialization.ts +151 -0
  108. package/src/test-runner/types.ts +81 -0
  109. package/dist/generated-api/index.d.ts +0 -10
  110. package/dist/generated-api/index.d.ts.map +0 -1
  111. package/dist/generated-api/index.js +0 -274
  112. package/dist/generated-api/index.js.map +0 -1
  113. package/dist/generated-api/validateGeneratedApiCollection.d.ts +0 -3
  114. package/dist/generated-api/validateGeneratedApiCollection.d.ts.map +0 -1
  115. package/dist/generated-api/validateGeneratedApiCollection.js +0 -35
  116. package/dist/generated-api/validateGeneratedApiCollection.js.map +0 -1
  117. package/src/generated-api/index.test.ts +0 -173
  118. package/src/generated-api/index.ts +0 -414
  119. package/src/generated-api/validateGeneratedApiCollection.test.ts +0 -91
  120. package/src/generated-api/validateGeneratedApiCollection.ts +0 -76
@@ -1,18 +1,24 @@
1
1
  import type {
2
- CredentialRef,
3
- DataContractValue,
4
2
  DataEndpointConfig,
5
- DataOperationConfig,
6
- DataOperationIntent,
7
- DataOperationResponse,
8
- DataSchema,
9
- DataSchemaRegistry,
10
3
  DataSourceDiagnostic,
11
4
  DataSourceDiagnosticResult,
12
- ExternalGraphQlApiDataSourceConfig,
13
- OperationId,
5
+ ExternalGraphQlApiDefinition,
14
6
  } from '@ankhorage/contracts/data';
15
7
 
8
+ import {
9
+ normalizeGraphQlIntrospectionOperations,
10
+ normalizeGraphQlIntrospectionSchemas,
11
+ } from './introspection';
12
+ import { normalizeGraphQlOperation } from './operation';
13
+ import type { GraphQlApiDefinition } from './types';
14
+
15
+ export {
16
+ normalizeGraphQlIntrospectionOperations,
17
+ normalizeGraphQlIntrospectionSchemas,
18
+ } from './introspection';
19
+ export { normalizeGraphQlOperationId } from './operation';
20
+ export type * from './types';
21
+
16
22
  export const GRAPHQL_INTROSPECTION_QUERY = `query AnkhorageGraphQlIntrospection {
17
23
  __schema {
18
24
  queryType { name }
@@ -45,80 +51,6 @@ export const GRAPHQL_INTROSPECTION_QUERY = `query AnkhorageGraphQlIntrospection
45
51
  }
46
52
  }`;
47
53
 
48
- export type GraphQlOperationKind = 'mutation' | 'query' | 'subscription';
49
- type DataContractRecord = Record<string, DataContractValue>;
50
-
51
- export interface GraphQlIntrospectionTypeRef {
52
- readonly kind: string;
53
- readonly name?: string | null;
54
- readonly ofType?: GraphQlIntrospectionTypeRef | null;
55
- }
56
-
57
- export interface GraphQlIntrospectionInputValue {
58
- readonly name: string;
59
- readonly description?: string | null;
60
- readonly type: GraphQlIntrospectionTypeRef;
61
- readonly defaultValue?: string | null;
62
- }
63
-
64
- export interface GraphQlIntrospectionField {
65
- readonly name: string;
66
- readonly description?: string | null;
67
- readonly args?: readonly GraphQlIntrospectionInputValue[] | null;
68
- readonly type: GraphQlIntrospectionTypeRef;
69
- }
70
-
71
- export interface GraphQlIntrospectionEnumValue {
72
- readonly name: string;
73
- readonly description?: string | null;
74
- }
75
-
76
- export interface GraphQlIntrospectionType {
77
- readonly kind: string;
78
- readonly name?: string | null;
79
- readonly description?: string | null;
80
- readonly fields?: readonly GraphQlIntrospectionField[] | null;
81
- readonly inputFields?: readonly GraphQlIntrospectionInputValue[] | null;
82
- readonly enumValues?: readonly GraphQlIntrospectionEnumValue[] | null;
83
- readonly possibleTypes?: readonly GraphQlIntrospectionTypeRef[] | null;
84
- }
85
-
86
- export interface GraphQlIntrospectionSchema {
87
- readonly queryType?: { readonly name?: string | null } | null;
88
- readonly mutationType?: { readonly name?: string | null } | null;
89
- readonly subscriptionType?: { readonly name?: string | null } | null;
90
- readonly types?: readonly GraphQlIntrospectionType[] | null;
91
- }
92
-
93
- export interface GraphQlIntrospectionResult {
94
- readonly __schema?: GraphQlIntrospectionSchema;
95
- }
96
-
97
- export interface GraphQlOperationDefinition {
98
- readonly id: OperationId;
99
- readonly kind: GraphQlOperationKind;
100
- readonly name?: string;
101
- readonly description?: string;
102
- readonly document?: string;
103
- readonly variables?: DataSchema;
104
- readonly response?: DataSchema;
105
- readonly selectionPath?: string;
106
- readonly metadata?: DataContractValue;
107
- }
108
-
109
- export interface GraphQlDataSourceDefinition {
110
- readonly id: string;
111
- readonly endpointUrl: string;
112
- readonly name?: string;
113
- readonly description?: string;
114
- readonly credential?: CredentialRef;
115
- readonly introspection?: GraphQlIntrospectionResult;
116
- readonly introspectionEnabled?: boolean;
117
- readonly schemaVersion?: string;
118
- readonly operations?: readonly GraphQlOperationDefinition[];
119
- readonly metadata?: DataContractValue;
120
- }
121
-
122
54
  export interface GraphQlIntrospectionRequest {
123
55
  readonly query: typeof GRAPHQL_INTROSPECTION_QUERY;
124
56
  readonly operationName: 'AnkhorageGraphQlIntrospection';
@@ -131,91 +63,72 @@ export function createGraphQlIntrospectionRequest(): GraphQlIntrospectionRequest
131
63
  };
132
64
  }
133
65
 
134
- export function normalizeGraphQlOperationId(kind: GraphQlOperationKind, name: string): OperationId {
135
- const normalizedName = name
136
- .replace(/[^A-Za-z0-9]+/g, '-')
137
- .replace(/^-+|-+$/g, '')
138
- .toLowerCase();
139
- return normalizedName.length > 0 ? `${kind}.${normalizedName}` : `${kind}.operation`;
140
- }
141
-
142
- export function createGraphQlDataSource(
143
- definition: GraphQlDataSourceDefinition,
144
- ): DataSourceDiagnosticResult<ExternalGraphQlApiDataSourceConfig> {
145
- const diagnostics = validateGraphQlDataSource(definition);
66
+ export function createGraphQlApi(
67
+ definition: GraphQlApiDefinition,
68
+ ): DataSourceDiagnosticResult<ExternalGraphQlApiDefinition> {
69
+ const diagnostics = validateGraphQlApi(definition);
146
70
  if (diagnostics.some((diagnostic) => diagnostic.severity === 'error')) {
147
71
  return { ok: false, diagnostics };
148
72
  }
149
-
150
- return {
151
- ok: true,
152
- data: normalizeGraphQlDataSource(definition),
153
- diagnostics,
154
- };
73
+ return { ok: true, data: normalizeGraphQlApi(definition), diagnostics };
155
74
  }
156
75
 
157
- export function validateGraphQlDataSource(
158
- definition: GraphQlDataSourceDefinition,
76
+ export function validateGraphQlApi(
77
+ definition: GraphQlApiDefinition,
159
78
  ): readonly DataSourceDiagnostic[] {
160
79
  const diagnostics: DataSourceDiagnostic[] = [];
161
80
 
162
81
  if (definition.endpointUrl.trim().length === 0) {
163
82
  diagnostics.push({
164
83
  code: 'invalid-config',
165
- dataSourceId: definition.id,
166
- message: 'GraphQL data source requires a non-empty endpointUrl.',
84
+ apiId: definition.id,
85
+ message: 'GraphQL API requires a non-empty endpointUrl.',
167
86
  path: 'endpointUrl',
168
87
  severity: 'error',
169
88
  });
170
89
  }
171
-
172
90
  if (definition.introspectionEnabled === false && definition.introspection !== undefined) {
173
91
  diagnostics.push({
174
92
  code: 'invalid-config',
175
- dataSourceId: definition.id,
93
+ apiId: definition.id,
176
94
  message: 'GraphQL introspection data was provided while introspection is disabled.',
177
95
  path: 'introspection',
178
96
  severity: 'warning',
179
97
  });
180
98
  }
181
-
182
99
  if (definition.introspectionEnabled !== false && definition.introspection === undefined) {
183
100
  diagnostics.push({
184
101
  code: 'missing-schema',
185
- dataSourceId: definition.id,
102
+ apiId: definition.id,
186
103
  message:
187
104
  'GraphQL introspection result was not provided. Manual operations can still be used.',
188
105
  path: 'introspection',
189
106
  severity: 'info',
190
107
  });
191
108
  }
192
-
193
109
  return diagnostics;
194
110
  }
195
111
 
196
- export function normalizeGraphQlDataSource(
197
- definition: GraphQlDataSourceDefinition,
198
- ): ExternalGraphQlApiDataSourceConfig {
112
+ export function normalizeGraphQlApi(
113
+ definition: GraphQlApiDefinition,
114
+ ): ExternalGraphQlApiDefinition {
199
115
  const schemas = normalizeGraphQlIntrospectionSchemas(definition.introspection);
200
116
  const discovered = normalizeGraphQlIntrospectionOperations(definition.introspection);
201
- const operations: Record<OperationId, DataOperationConfig> = {};
202
-
203
- for (const operation of [...discovered, ...(definition.operations ?? [])]) {
204
- operations[operation.id] = normalizeGraphQlOperation(operation);
205
- }
206
-
117
+ const operations = Object.fromEntries(
118
+ [...discovered, ...(definition.operations ?? [])].map((operation) => [
119
+ operation.id,
120
+ normalizeGraphQlOperation(operation),
121
+ ]),
122
+ );
207
123
  const endpoint: DataEndpointConfig = {
208
124
  id: 'graphql',
209
125
  kind: 'graphql',
210
- baseUrl: definition.endpointUrl,
211
126
  credential: definition.credential,
212
127
  operations,
213
128
  metadata: { source: 'graphql' },
214
129
  };
215
-
216
130
  return {
217
131
  id: definition.id,
218
- kind: 'api',
219
132
  origin: 'external',
220
133
  protocol: 'graphql',
221
134
  name: definition.name,
@@ -231,196 +144,3 @@ export function normalizeGraphQlDataSource(
231
144
  metadata: definition.metadata,
232
145
  };
233
146
  }
234
-
235
- export function normalizeGraphQlIntrospectionSchemas(
236
- introspection: GraphQlIntrospectionResult | undefined,
237
- ): DataSchemaRegistry | undefined {
238
- const types = introspection?.__schema?.types?.filter(isNamedGraphQlType) ?? [];
239
- if (types.length === 0) return undefined;
240
-
241
- const schemas: Record<string, DataSchema> = {};
242
- for (const type of types) {
243
- if (type.name === undefined || type.name === null || type.name.startsWith('__')) continue;
244
- schemas[type.name] = normalizeGraphQlType(type);
245
- }
246
- return schemas;
247
- }
248
-
249
- export function normalizeGraphQlIntrospectionOperations(
250
- introspection: GraphQlIntrospectionResult | undefined,
251
- ): readonly GraphQlOperationDefinition[] {
252
- const schema = introspection?.__schema;
253
- if (schema === undefined) return [];
254
-
255
- const types = schema.types?.filter(isNamedGraphQlType) ?? [];
256
- const operations: GraphQlOperationDefinition[] = [];
257
- appendRootOperations(operations, 'query', schema.queryType?.name, types);
258
- appendRootOperations(operations, 'mutation', schema.mutationType?.name, types);
259
- appendRootOperations(operations, 'subscription', schema.subscriptionType?.name, types);
260
- return operations;
261
- }
262
-
263
- function appendRootOperations(
264
- operations: GraphQlOperationDefinition[],
265
- kind: GraphQlOperationKind,
266
- rootTypeName: string | null | undefined,
267
- types: readonly GraphQlIntrospectionType[],
268
- ): void {
269
- if (rootTypeName === undefined || rootTypeName === null) return;
270
- const rootType = types.find((type) => type.name === rootTypeName);
271
-
272
- for (const field of rootType?.fields ?? []) {
273
- operations.push({
274
- id: normalizeGraphQlOperationId(kind, field.name),
275
- kind,
276
- name: field.name,
277
- description: field.description ?? undefined,
278
- variables: normalizeGraphQlVariablesSchema(field.args ?? []),
279
- response: normalizeGraphQlTypeRef(field.type),
280
- selectionPath: `$.data.${field.name}`,
281
- metadata: { rootType: rootTypeName, source: 'introspection' },
282
- });
283
- }
284
- }
285
-
286
- function normalizeGraphQlOperation(operation: GraphQlOperationDefinition): DataOperationConfig {
287
- const response: DataOperationResponse | undefined =
288
- operation.response === undefined ? undefined : { schema: operation.response };
289
-
290
- return {
291
- id: operation.id,
292
- endpointId: 'graphql',
293
- name: operation.name,
294
- description: operation.description,
295
- protocol: 'graphql',
296
- intent: mapGraphQlOperationKindToIntent(operation.kind),
297
- request: { schema: operation.variables },
298
- response,
299
- metadata: createGraphQlOperationMetadata(operation),
300
- };
301
- }
302
-
303
- function mapGraphQlOperationKindToIntent(kind: GraphQlOperationKind): DataOperationIntent {
304
- return kind === 'query' || kind === 'subscription' ? 'read' : 'action';
305
- }
306
-
307
- function normalizeGraphQlVariablesSchema(
308
- args: readonly GraphQlIntrospectionInputValue[],
309
- ): DataSchema {
310
- const properties: Record<string, DataSchema> = {};
311
- const required: string[] = [];
312
-
313
- for (const arg of args) {
314
- properties[arg.name] = {
315
- ...normalizeGraphQlTypeRef(arg.type),
316
- description: arg.description ?? undefined,
317
- default: arg.defaultValue ?? undefined,
318
- };
319
- if (isGraphQlNonNull(arg.type)) required.push(arg.name);
320
- }
321
-
322
- return {
323
- type: 'object',
324
- required: required.length > 0 ? required : undefined,
325
- properties,
326
- };
327
- }
328
-
329
- function normalizeGraphQlType(type: GraphQlIntrospectionType): DataSchema {
330
- if (type.kind === 'OBJECT' || type.kind === 'INTERFACE' || type.kind === 'INPUT_OBJECT') {
331
- return normalizeGraphQlObjectType(type);
332
- }
333
- if (type.kind === 'ENUM') {
334
- return {
335
- type: 'string',
336
- title: type.name ?? undefined,
337
- description: type.description ?? undefined,
338
- enum: type.enumValues?.map((value) => value.name),
339
- };
340
- }
341
- if (type.kind === 'SCALAR') {
342
- return {
343
- ...normalizeGraphQlNamedScalar(type.name),
344
- title: type.name ?? undefined,
345
- description: type.description ?? undefined,
346
- };
347
- }
348
- if (type.kind === 'UNION') {
349
- return {
350
- title: type.name ?? undefined,
351
- description: type.description ?? undefined,
352
- anyOf: type.possibleTypes?.map(normalizeGraphQlTypeRef),
353
- };
354
- }
355
- return { title: type.name ?? undefined, description: type.description ?? undefined };
356
- }
357
-
358
- function normalizeGraphQlObjectType(type: GraphQlIntrospectionType): DataSchema {
359
- const fields = type.kind === 'INPUT_OBJECT' ? type.inputFields : type.fields;
360
- const properties: Record<string, DataSchema> = {};
361
- const required: string[] = [];
362
-
363
- for (const field of fields ?? []) {
364
- const fieldType = field.type;
365
- properties[field.name] = {
366
- ...normalizeGraphQlTypeRef(fieldType),
367
- description: field.description ?? undefined,
368
- };
369
- if (isGraphQlNonNull(fieldType)) required.push(field.name);
370
- }
371
-
372
- return {
373
- type: 'object',
374
- title: type.name ?? undefined,
375
- description: type.description ?? undefined,
376
- required: required.length > 0 ? required : undefined,
377
- properties,
378
- };
379
- }
380
-
381
- function normalizeGraphQlTypeRef(type: GraphQlIntrospectionTypeRef): DataSchema {
382
- if (type.kind === 'NON_NULL' && type.ofType !== undefined && type.ofType !== null) {
383
- return { ...normalizeGraphQlTypeRef(type.ofType), nullable: false };
384
- }
385
- if (type.kind === 'LIST' && type.ofType !== undefined && type.ofType !== null) {
386
- return { type: 'array', items: normalizeGraphQlTypeRef(type.ofType) };
387
- }
388
- if (type.kind === 'SCALAR') return normalizeGraphQlNamedScalar(type.name);
389
- if (type.name !== undefined && type.name !== null) return { ref: { id: type.name } };
390
- return {};
391
- }
392
-
393
- function normalizeGraphQlNamedScalar(name: string | null | undefined): DataSchema {
394
- if (name === 'Boolean') return { type: 'boolean' };
395
- if (name === 'Float') return { type: 'number' };
396
- if (name === 'ID' || name === 'String') return { type: 'string' };
397
- if (name === 'Int') return { type: 'integer' };
398
- return { type: 'string', format: name ?? undefined };
399
- }
400
-
401
- function isGraphQlNonNull(type: GraphQlIntrospectionTypeRef): boolean {
402
- return type.kind === 'NON_NULL';
403
- }
404
-
405
- function isNamedGraphQlType(type: GraphQlIntrospectionType): boolean {
406
- return type.name !== undefined && type.name !== null;
407
- }
408
-
409
- function createGraphQlOperationMetadata(operation: GraphQlOperationDefinition): DataContractRecord {
410
- const metadata = toMetadataRecord(operation.metadata);
411
- metadata.kind = operation.kind;
412
- if (operation.document !== undefined) metadata.document = operation.document;
413
- if (operation.selectionPath !== undefined) metadata.selectionPath = operation.selectionPath;
414
- return metadata;
415
- }
416
-
417
- function toMetadataRecord(value: DataContractValue | undefined): DataContractRecord {
418
- if (!isDataContractRecord(value)) return {};
419
- return { ...value };
420
- }
421
-
422
- function isDataContractRecord(value: DataContractValue | undefined): value is DataContractRecord {
423
- return (
424
- value !== undefined && typeof value === 'object' && value !== null && !Array.isArray(value)
425
- );
426
- }
@@ -0,0 +1,163 @@
1
+ import type { DataSchema, DataSchemaRegistry } from '@ankhorage/contracts/data';
2
+
3
+ import { normalizeGraphQlOperationId } from './operation';
4
+ import type {
5
+ GraphQlIntrospectionInputValue,
6
+ GraphQlIntrospectionResult,
7
+ GraphQlIntrospectionType,
8
+ GraphQlIntrospectionTypeRef,
9
+ GraphQlOperationDefinition,
10
+ GraphQlOperationKind,
11
+ } from './types';
12
+
13
+ const OBJECT_TYPE_KINDS = new Set(['OBJECT', 'INTERFACE', 'INPUT_OBJECT']);
14
+
15
+ export function normalizeGraphQlIntrospectionSchemas(
16
+ introspection: GraphQlIntrospectionResult | undefined,
17
+ ): DataSchemaRegistry | undefined {
18
+ const types = introspection?.__schema?.types?.filter(isNamedGraphQlType) ?? [];
19
+ const entries = types.flatMap((type) => {
20
+ const { name } = type;
21
+ if (name === undefined || name === null || name.startsWith('__')) return [];
22
+ return [[name, normalizeGraphQlType(type)] as const];
23
+ });
24
+ return entries.length === 0 ? undefined : Object.fromEntries(entries);
25
+ }
26
+
27
+ export function normalizeGraphQlIntrospectionOperations(
28
+ introspection: GraphQlIntrospectionResult | undefined,
29
+ ): readonly GraphQlOperationDefinition[] {
30
+ const schema = introspection?.__schema;
31
+ if (schema === undefined) return [];
32
+
33
+ const types = schema.types?.filter(isNamedGraphQlType) ?? [];
34
+ return [
35
+ ...normalizeRootOperations('query', schema.queryType?.name, types),
36
+ ...normalizeRootOperations('mutation', schema.mutationType?.name, types),
37
+ ...normalizeRootOperations('subscription', schema.subscriptionType?.name, types),
38
+ ];
39
+ }
40
+
41
+ function normalizeRootOperations(
42
+ kind: GraphQlOperationKind,
43
+ rootTypeName: string | null | undefined,
44
+ types: readonly GraphQlIntrospectionType[],
45
+ ): readonly GraphQlOperationDefinition[] {
46
+ if (rootTypeName === undefined || rootTypeName === null) return [];
47
+ const rootType = types.find((type) => type.name === rootTypeName);
48
+ return (rootType?.fields ?? []).map((field) => ({
49
+ id: normalizeGraphQlOperationId(kind, field.name),
50
+ kind,
51
+ name: field.name,
52
+ description: field.description ?? undefined,
53
+ variables: normalizeGraphQlVariablesSchema(field.args ?? []),
54
+ response: normalizeGraphQlTypeRef(field.type),
55
+ selectionPath: `$.data.${field.name}`,
56
+ metadata: { rootType: rootTypeName, source: 'introspection' },
57
+ }));
58
+ }
59
+
60
+ function normalizeGraphQlVariablesSchema(
61
+ args: readonly GraphQlIntrospectionInputValue[],
62
+ ): DataSchema {
63
+ const properties = Object.fromEntries(
64
+ args.map((arg) => [
65
+ arg.name,
66
+ {
67
+ ...normalizeGraphQlTypeRef(arg.type),
68
+ description: arg.description ?? undefined,
69
+ default: arg.defaultValue ?? undefined,
70
+ },
71
+ ]),
72
+ );
73
+ const required = args.filter((arg) => isGraphQlNonNull(arg.type)).map((arg) => arg.name);
74
+ return {
75
+ type: 'object',
76
+ required: required.length > 0 ? required : undefined,
77
+ properties,
78
+ };
79
+ }
80
+
81
+ function normalizeGraphQlType(type: GraphQlIntrospectionType): DataSchema {
82
+ if (OBJECT_TYPE_KINDS.has(type.kind)) return normalizeGraphQlObjectType(type);
83
+ if (type.kind === 'ENUM') return normalizeGraphQlEnumType(type);
84
+ if (type.kind === 'SCALAR') return normalizeGraphQlScalarType(type);
85
+ if (type.kind === 'UNION') return normalizeGraphQlUnionType(type);
86
+ return { title: type.name ?? undefined, description: type.description ?? undefined };
87
+ }
88
+
89
+ function normalizeGraphQlEnumType(type: GraphQlIntrospectionType): DataSchema {
90
+ return {
91
+ type: 'string',
92
+ title: type.name ?? undefined,
93
+ description: type.description ?? undefined,
94
+ enum: type.enumValues?.map((value) => value.name),
95
+ };
96
+ }
97
+
98
+ function normalizeGraphQlScalarType(type: GraphQlIntrospectionType): DataSchema {
99
+ return {
100
+ ...normalizeGraphQlNamedScalar(type.name),
101
+ title: type.name ?? undefined,
102
+ description: type.description ?? undefined,
103
+ };
104
+ }
105
+
106
+ function normalizeGraphQlUnionType(type: GraphQlIntrospectionType): DataSchema {
107
+ return {
108
+ title: type.name ?? undefined,
109
+ description: type.description ?? undefined,
110
+ anyOf: type.possibleTypes?.map(normalizeGraphQlTypeRef),
111
+ };
112
+ }
113
+
114
+ function normalizeGraphQlObjectType(type: GraphQlIntrospectionType): DataSchema {
115
+ const fields = type.kind === 'INPUT_OBJECT' ? type.inputFields : type.fields;
116
+ const entries = (fields ?? []).map(
117
+ (field) =>
118
+ [
119
+ field.name,
120
+ {
121
+ ...normalizeGraphQlTypeRef(field.type),
122
+ description: field.description ?? undefined,
123
+ },
124
+ ] as const,
125
+ );
126
+ const required = (fields ?? [])
127
+ .filter((field) => isGraphQlNonNull(field.type))
128
+ .map((field) => field.name);
129
+ return {
130
+ type: 'object',
131
+ title: type.name ?? undefined,
132
+ description: type.description ?? undefined,
133
+ required: required.length > 0 ? required : undefined,
134
+ properties: Object.fromEntries(entries),
135
+ };
136
+ }
137
+
138
+ function normalizeGraphQlTypeRef(type: GraphQlIntrospectionTypeRef): DataSchema {
139
+ if (type.kind === 'NON_NULL' && type.ofType !== undefined && type.ofType !== null) {
140
+ return { ...normalizeGraphQlTypeRef(type.ofType), nullable: false };
141
+ }
142
+ if (type.kind === 'LIST' && type.ofType !== undefined && type.ofType !== null) {
143
+ return { type: 'array', items: normalizeGraphQlTypeRef(type.ofType) };
144
+ }
145
+ if (type.kind === 'SCALAR') return normalizeGraphQlNamedScalar(type.name);
146
+ return type.name === undefined || type.name === null ? {} : { ref: { id: type.name } };
147
+ }
148
+
149
+ function normalizeGraphQlNamedScalar(name: string | null | undefined): DataSchema {
150
+ if (name === 'Boolean') return { type: 'boolean' };
151
+ if (name === 'Float') return { type: 'number' };
152
+ if (name === 'ID' || name === 'String') return { type: 'string' };
153
+ if (name === 'Int') return { type: 'integer' };
154
+ return { type: 'string', format: name ?? undefined };
155
+ }
156
+
157
+ function isGraphQlNonNull(type: GraphQlIntrospectionTypeRef): boolean {
158
+ return type.kind === 'NON_NULL';
159
+ }
160
+
161
+ function isNamedGraphQlType(type: GraphQlIntrospectionType): boolean {
162
+ return type.name !== undefined && type.name !== null;
163
+ }
@@ -0,0 +1,60 @@
1
+ import type {
2
+ DataContractValue,
3
+ DataOperationConfig,
4
+ DataOperationIntent,
5
+ DataOperationResponse,
6
+ OperationId,
7
+ } from '@ankhorage/contracts/data';
8
+
9
+ import type { GraphQlOperationDefinition, GraphQlOperationKind } from './types';
10
+
11
+ type DataContractRecord = Record<string, DataContractValue>;
12
+
13
+ export function normalizeGraphQlOperationId(kind: GraphQlOperationKind, name: string): OperationId {
14
+ const normalizedName = name
15
+ .replace(/[^A-Za-z0-9]+/g, '-')
16
+ .replace(/^-+|-+$/g, '')
17
+ .toLowerCase();
18
+ return normalizedName.length > 0 ? `${kind}.${normalizedName}` : `${kind}.operation`;
19
+ }
20
+
21
+ export function normalizeGraphQlOperation(
22
+ operation: GraphQlOperationDefinition,
23
+ ): DataOperationConfig {
24
+ const response: DataOperationResponse | undefined =
25
+ operation.response === undefined ? undefined : { schema: operation.response };
26
+ return {
27
+ id: operation.id,
28
+ endpointId: 'graphql',
29
+ name: operation.name,
30
+ description: operation.description,
31
+ protocol: 'graphql',
32
+ intent: mapGraphQlOperationKindToIntent(operation.kind),
33
+ request: { schema: operation.variables },
34
+ response,
35
+ metadata: createGraphQlOperationMetadata(operation),
36
+ };
37
+ }
38
+
39
+ function mapGraphQlOperationKindToIntent(kind: GraphQlOperationKind): DataOperationIntent {
40
+ return kind === 'query' || kind === 'subscription' ? 'read' : 'action';
41
+ }
42
+
43
+ function createGraphQlOperationMetadata(operation: GraphQlOperationDefinition): DataContractRecord {
44
+ return {
45
+ ...toMetadataRecord(operation.metadata),
46
+ kind: operation.kind,
47
+ ...(operation.document === undefined ? {} : { document: operation.document }),
48
+ ...(operation.selectionPath === undefined ? {} : { selectionPath: operation.selectionPath }),
49
+ };
50
+ }
51
+
52
+ function toMetadataRecord(value: DataContractValue | undefined): DataContractRecord {
53
+ return isDataContractRecord(value) ? { ...value } : {};
54
+ }
55
+
56
+ function isDataContractRecord(value: DataContractValue | undefined): value is DataContractRecord {
57
+ return (
58
+ value !== undefined && typeof value === 'object' && value !== null && !Array.isArray(value)
59
+ );
60
+ }