@ankhorage/data-sources 0.5.5 → 1.0.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +12 -5
  3. package/dist/discovery/index.d.ts +64 -0
  4. package/dist/discovery/index.d.ts.map +1 -0
  5. package/dist/discovery/index.js +195 -0
  6. package/dist/discovery/index.js.map +1 -0
  7. package/dist/generated-api/index.d.ts +10 -0
  8. package/dist/generated-api/index.d.ts.map +1 -0
  9. package/dist/generated-api/index.js +272 -0
  10. package/dist/generated-api/index.js.map +1 -0
  11. package/dist/graphql/index.d.ts +3 -3
  12. package/dist/graphql/index.d.ts.map +1 -1
  13. package/dist/graphql/index.js +24 -60
  14. package/dist/graphql/index.js.map +1 -1
  15. package/dist/index.d.ts +10 -3
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +18 -4
  18. package/dist/index.js.map +1 -1
  19. package/dist/openapi/index.d.ts +2 -2
  20. package/dist/openapi/index.d.ts.map +1 -1
  21. package/dist/openapi/index.js +19 -29
  22. package/dist/openapi/index.js.map +1 -1
  23. package/dist/rest/index.d.ts +3 -3
  24. package/dist/rest/index.d.ts.map +1 -1
  25. package/dist/rest/index.js +51 -51
  26. package/dist/rest/index.js.map +1 -1
  27. package/dist/test-runner/index.d.ts.map +1 -1
  28. package/dist/test-runner/index.js +60 -104
  29. package/dist/test-runner/index.js.map +1 -1
  30. package/package.json +2 -2
  31. package/src/discovery/index.test.ts +133 -0
  32. package/src/discovery/index.ts +340 -0
  33. package/src/{managed-api → generated-api}/index.test.ts +59 -67
  34. package/src/generated-api/index.ts +410 -0
  35. package/src/graphql/index.test.ts +28 -134
  36. package/src/graphql/index.ts +29 -89
  37. package/src/index.test.ts +25 -18
  38. package/src/index.ts +29 -7
  39. package/src/openapi/index.test.ts +36 -246
  40. package/src/openapi/index.ts +22 -60
  41. package/src/rest/index.test.ts +22 -173
  42. package/src/rest/index.ts +62 -59
  43. package/src/test-runner/index.test.ts +82 -150
  44. package/src/test-runner/index.ts +73 -143
  45. package/dist/managed-api/index.d.ts +0 -30
  46. package/dist/managed-api/index.d.ts.map +0 -1
  47. package/dist/managed-api/index.js +0 -257
  48. package/dist/managed-api/index.js.map +0 -1
  49. package/src/managed-api/index.ts +0 -364
@@ -9,7 +9,7 @@ import type {
9
9
  DataSchemaRegistry,
10
10
  DataSourceDiagnostic,
11
11
  DataSourceDiagnosticResult,
12
- GraphQlDataSourceConfig,
12
+ ExternalGraphQlApiDataSourceConfig,
13
13
  OperationId,
14
14
  } from '@ankhorage/contracts/data';
15
15
 
@@ -46,7 +46,6 @@ export const GRAPHQL_INTROSPECTION_QUERY = `query AnkhorageGraphQlIntrospection
46
46
  }`;
47
47
 
48
48
  export type GraphQlOperationKind = 'mutation' | 'query' | 'subscription';
49
-
50
49
  type DataContractRecord = Record<string, DataContractValue>;
51
50
 
52
51
  export interface GraphQlIntrospectionTypeRef {
@@ -137,17 +136,16 @@ export function normalizeGraphQlOperationId(kind: GraphQlOperationKind, name: st
137
136
  .replace(/[^A-Za-z0-9]+/g, '-')
138
137
  .replace(/^-+|-+$/g, '')
139
138
  .toLowerCase();
140
-
141
139
  return normalizedName.length > 0 ? `${kind}.${normalizedName}` : `${kind}.operation`;
142
140
  }
143
141
 
144
142
  export function createGraphQlDataSource(
145
143
  definition: GraphQlDataSourceDefinition,
146
- ): DataSourceDiagnosticResult<GraphQlDataSourceConfig> {
144
+ ): DataSourceDiagnosticResult<ExternalGraphQlApiDataSourceConfig> {
147
145
  const diagnostics = validateGraphQlDataSource(definition);
148
- const hasErrors = diagnostics.some((diagnostic) => diagnostic.severity === 'error');
149
-
150
- if (hasErrors) return { ok: false, diagnostics };
146
+ if (diagnostics.some((diagnostic) => diagnostic.severity === 'error')) {
147
+ return { ok: false, diagnostics };
148
+ }
151
149
 
152
150
  return {
153
151
  ok: true,
@@ -197,13 +195,12 @@ export function validateGraphQlDataSource(
197
195
 
198
196
  export function normalizeGraphQlDataSource(
199
197
  definition: GraphQlDataSourceDefinition,
200
- ): GraphQlDataSourceConfig {
198
+ ): ExternalGraphQlApiDataSourceConfig {
201
199
  const schemas = normalizeGraphQlIntrospectionSchemas(definition.introspection);
202
- const introspectionOperations = normalizeGraphQlIntrospectionOperations(definition.introspection);
203
- const manualOperations = definition.operations ?? [];
200
+ const discovered = normalizeGraphQlIntrospectionOperations(definition.introspection);
204
201
  const operations: Record<OperationId, DataOperationConfig> = {};
205
202
 
206
- for (const operation of [...introspectionOperations, ...manualOperations]) {
203
+ for (const operation of [...discovered, ...(definition.operations ?? [])]) {
207
204
  operations[operation.id] = normalizeGraphQlOperation(operation);
208
205
  }
209
206
 
@@ -213,21 +210,19 @@ export function normalizeGraphQlDataSource(
213
210
  baseUrl: definition.endpointUrl,
214
211
  credential: definition.credential,
215
212
  operations,
216
- metadata: {
217
- source: 'graphql',
218
- },
213
+ metadata: { source: 'graphql' },
219
214
  };
220
215
 
221
216
  return {
222
217
  id: definition.id,
223
- kind: 'graphql',
218
+ kind: 'api',
219
+ origin: 'external',
220
+ protocol: 'graphql',
224
221
  name: definition.name,
225
222
  description: definition.description,
226
223
  endpointUrl: definition.endpointUrl,
227
224
  credential: definition.credential,
228
- endpoints: {
229
- graphql: endpoint,
230
- },
225
+ endpoints: { graphql: endpoint },
231
226
  schemas,
232
227
  introspection: {
233
228
  enabled: definition.introspectionEnabled ?? definition.introspection !== undefined,
@@ -244,12 +239,10 @@ export function normalizeGraphQlIntrospectionSchemas(
244
239
  if (types.length === 0) return undefined;
245
240
 
246
241
  const schemas: Record<string, DataSchema> = {};
247
-
248
242
  for (const type of types) {
249
243
  if (type.name === undefined || type.name === null || type.name.startsWith('__')) continue;
250
244
  schemas[type.name] = normalizeGraphQlType(type);
251
245
  }
252
-
253
246
  return schemas;
254
247
  }
255
248
 
@@ -261,11 +254,9 @@ export function normalizeGraphQlIntrospectionOperations(
261
254
 
262
255
  const types = schema.types?.filter(isNamedGraphQlType) ?? [];
263
256
  const operations: GraphQlOperationDefinition[] = [];
264
-
265
257
  appendRootOperations(operations, 'query', schema.queryType?.name, types);
266
258
  appendRootOperations(operations, 'mutation', schema.mutationType?.name, types);
267
259
  appendRootOperations(operations, 'subscription', schema.subscriptionType?.name, types);
268
-
269
260
  return operations;
270
261
  }
271
262
 
@@ -276,8 +267,8 @@ function appendRootOperations(
276
267
  types: readonly GraphQlIntrospectionType[],
277
268
  ): void {
278
269
  if (rootTypeName === undefined || rootTypeName === null) return;
279
-
280
270
  const rootType = types.find((type) => type.name === rootTypeName);
271
+
281
272
  for (const field of rootType?.fields ?? []) {
282
273
  operations.push({
283
274
  id: normalizeGraphQlOperationId(kind, field.name),
@@ -287,21 +278,14 @@ function appendRootOperations(
287
278
  variables: normalizeGraphQlVariablesSchema(field.args ?? []),
288
279
  response: normalizeGraphQlTypeRef(field.type),
289
280
  selectionPath: `$.data.${field.name}`,
290
- metadata: {
291
- rootType: rootTypeName,
292
- source: 'introspection',
293
- },
281
+ metadata: { rootType: rootTypeName, source: 'introspection' },
294
282
  });
295
283
  }
296
284
  }
297
285
 
298
286
  function normalizeGraphQlOperation(operation: GraphQlOperationDefinition): DataOperationConfig {
299
287
  const response: DataOperationResponse | undefined =
300
- operation.response === undefined
301
- ? undefined
302
- : {
303
- schema: operation.response,
304
- };
288
+ operation.response === undefined ? undefined : { schema: operation.response };
305
289
 
306
290
  return {
307
291
  id: operation.id,
@@ -310,17 +294,14 @@ function normalizeGraphQlOperation(operation: GraphQlOperationDefinition): DataO
310
294
  description: operation.description,
311
295
  protocol: 'graphql',
312
296
  intent: mapGraphQlOperationKindToIntent(operation.kind),
313
- request: {
314
- schema: operation.variables,
315
- },
297
+ request: { schema: operation.variables },
316
298
  response,
317
299
  metadata: createGraphQlOperationMetadata(operation),
318
300
  };
319
301
  }
320
302
 
321
303
  function mapGraphQlOperationKindToIntent(kind: GraphQlOperationKind): DataOperationIntent {
322
- if (kind === 'query' || kind === 'subscription') return 'read';
323
- return 'action';
304
+ return kind === 'query' || kind === 'subscription' ? 'read' : 'action';
324
305
  }
325
306
 
326
307
  function normalizeGraphQlVariablesSchema(
@@ -349,7 +330,6 @@ function normalizeGraphQlType(type: GraphQlIntrospectionType): DataSchema {
349
330
  if (type.kind === 'OBJECT' || type.kind === 'INTERFACE' || type.kind === 'INPUT_OBJECT') {
350
331
  return normalizeGraphQlObjectType(type);
351
332
  }
352
-
353
333
  if (type.kind === 'ENUM') {
354
334
  return {
355
335
  type: 'string',
@@ -358,7 +338,6 @@ function normalizeGraphQlType(type: GraphQlIntrospectionType): DataSchema {
358
338
  enum: type.enumValues?.map((value) => value.name),
359
339
  };
360
340
  }
361
-
362
341
  if (type.kind === 'SCALAR') {
363
342
  return {
364
343
  ...normalizeGraphQlNamedScalar(type.name),
@@ -366,19 +345,14 @@ function normalizeGraphQlType(type: GraphQlIntrospectionType): DataSchema {
366
345
  description: type.description ?? undefined,
367
346
  };
368
347
  }
369
-
370
348
  if (type.kind === 'UNION') {
371
349
  return {
372
350
  title: type.name ?? undefined,
373
351
  description: type.description ?? undefined,
374
- anyOf: type.possibleTypes?.map((possibleType) => normalizeGraphQlTypeRef(possibleType)),
352
+ anyOf: type.possibleTypes?.map(normalizeGraphQlTypeRef),
375
353
  };
376
354
  }
377
-
378
- return {
379
- title: type.name ?? undefined,
380
- description: type.description ?? undefined,
381
- };
355
+ return { title: type.name ?? undefined, description: type.description ?? undefined };
382
356
  }
383
357
 
384
358
  function normalizeGraphQlObjectType(type: GraphQlIntrospectionType): DataSchema {
@@ -387,14 +361,11 @@ function normalizeGraphQlObjectType(type: GraphQlIntrospectionType): DataSchema
387
361
  const required: string[] = [];
388
362
 
389
363
  for (const field of fields ?? []) {
390
- const fieldType = 'type' in field ? field.type : undefined;
391
- if (fieldType === undefined) continue;
392
-
364
+ const fieldType = field.type;
393
365
  properties[field.name] = {
394
366
  ...normalizeGraphQlTypeRef(fieldType),
395
367
  description: field.description ?? undefined,
396
368
  };
397
-
398
369
  if (isGraphQlNonNull(fieldType)) required.push(field.name);
399
370
  }
400
371
 
@@ -409,27 +380,13 @@ function normalizeGraphQlObjectType(type: GraphQlIntrospectionType): DataSchema
409
380
 
410
381
  function normalizeGraphQlTypeRef(type: GraphQlIntrospectionTypeRef): DataSchema {
411
382
  if (type.kind === 'NON_NULL' && type.ofType !== undefined && type.ofType !== null) {
412
- return {
413
- ...normalizeGraphQlTypeRef(type.ofType),
414
- nullable: false,
415
- };
383
+ return { ...normalizeGraphQlTypeRef(type.ofType), nullable: false };
416
384
  }
417
-
418
385
  if (type.kind === 'LIST' && type.ofType !== undefined && type.ofType !== null) {
419
- return {
420
- type: 'array',
421
- items: normalizeGraphQlTypeRef(type.ofType),
422
- };
386
+ return { type: 'array', items: normalizeGraphQlTypeRef(type.ofType) };
423
387
  }
424
-
425
- if (type.kind === 'SCALAR') {
426
- return normalizeGraphQlNamedScalar(type.name);
427
- }
428
-
429
- if (type.name !== undefined && type.name !== null) {
430
- return { ref: { id: type.name } };
431
- }
432
-
388
+ if (type.kind === 'SCALAR') return normalizeGraphQlNamedScalar(type.name);
389
+ if (type.name !== undefined && type.name !== null) return { ref: { id: type.name } };
433
390
  return {};
434
391
  }
435
392
 
@@ -438,11 +395,7 @@ function normalizeGraphQlNamedScalar(name: string | null | undefined): DataSchem
438
395
  if (name === 'Float') return { type: 'number' };
439
396
  if (name === 'ID' || name === 'String') return { type: 'string' };
440
397
  if (name === 'Int') return { type: 'integer' };
441
-
442
- return {
443
- type: 'string',
444
- format: name ?? undefined,
445
- };
398
+ return { type: 'string', format: name ?? undefined };
446
399
  }
447
400
 
448
401
  function isGraphQlNonNull(type: GraphQlIntrospectionTypeRef): boolean {
@@ -456,27 +409,14 @@ function isNamedGraphQlType(type: GraphQlIntrospectionType): boolean {
456
409
  function createGraphQlOperationMetadata(operation: GraphQlOperationDefinition): DataContractRecord {
457
410
  const metadata = toMetadataRecord(operation.metadata);
458
411
  metadata.kind = operation.kind;
459
-
460
- if (operation.document !== undefined) {
461
- metadata.document = operation.document;
462
- }
463
-
464
- if (operation.selectionPath !== undefined) {
465
- metadata.selectionPath = operation.selectionPath;
466
- }
467
-
412
+ if (operation.document !== undefined) metadata.document = operation.document;
413
+ if (operation.selectionPath !== undefined) metadata.selectionPath = operation.selectionPath;
468
414
  return metadata;
469
415
  }
470
416
 
471
417
  function toMetadataRecord(value: DataContractValue | undefined): DataContractRecord {
472
418
  if (!isDataContractRecord(value)) return {};
473
-
474
- const metadata: DataContractRecord = {};
475
- for (const [key, item] of Object.entries(value)) {
476
- metadata[key] = item;
477
- }
478
-
479
- return metadata;
419
+ return { ...value };
480
420
  }
481
421
 
482
422
  function isDataContractRecord(value: DataContractValue | undefined): value is DataContractRecord {
package/src/index.test.ts CHANGED
@@ -1,39 +1,46 @@
1
- import type { RestDataSourceConfig } from '@ankhorage/contracts/data';
1
+ import type { ExternalRestApiDataSourceConfig } from '@ankhorage/contracts/data';
2
2
  import { describe, expect, it } from 'bun:test';
3
3
 
4
4
  import {
5
5
  DATA_SOURCES_PACKAGE_NAME,
6
6
  getDataSourceKind,
7
7
  getDataSourcesPackageInfo,
8
+ isSupportedApiOrigin,
9
+ isSupportedApiProtocol,
8
10
  isSupportedDataSourceKind,
9
- SUPPORTED_DATA_SOURCE_KINDS,
10
11
  } from './index';
11
12
 
12
- describe('data-sources package baseline', () => {
13
- it('exports package metadata', () => {
14
- const info = getDataSourcesPackageInfo();
15
-
16
- expect(info.packageName).toBe(DATA_SOURCES_PACKAGE_NAME);
17
- expect(info.supportedKinds).toEqual(SUPPORTED_DATA_SOURCE_KINDS);
13
+ describe('data-sources package model', () => {
14
+ it('exports orthogonal package capabilities', () => {
15
+ expect(getDataSourcesPackageInfo()).toEqual({
16
+ packageName: DATA_SOURCES_PACKAGE_NAME,
17
+ supportedKinds: ['api', 'database'],
18
+ supportedApiOrigins: ['external', 'generated'],
19
+ supportedApiProtocols: ['graphql', 'rest'],
20
+ });
18
21
  });
19
22
 
20
- it('recognizes supported shared contract data-source kinds', () => {
21
- expect(isSupportedDataSourceKind('rest')).toBe(true);
22
- expect(isSupportedDataSourceKind('openapi')).toBe(true);
23
- expect(isSupportedDataSourceKind('graphql')).toBe(true);
24
- expect(isSupportedDataSourceKind('managed-api')).toBe(true);
23
+ it('recognizes only canonical source dimensions', () => {
24
+ expect(isSupportedDataSourceKind('api')).toBe(true);
25
25
  expect(isSupportedDataSourceKind('database')).toBe(true);
26
- expect(isSupportedDataSourceKind('studio')).toBe(false);
26
+ expect(isSupportedDataSourceKind('managed-api')).toBe(false);
27
+ expect(isSupportedDataSourceKind('openapi')).toBe(false);
28
+ expect(isSupportedApiOrigin('external')).toBe(true);
29
+ expect(isSupportedApiOrigin('generated')).toBe(true);
30
+ expect(isSupportedApiProtocol('rest')).toBe(true);
31
+ expect(isSupportedApiProtocol('graphql')).toBe(true);
27
32
  });
28
33
 
29
- it('consumes data-source contracts from @ankhorage/contracts', () => {
30
- const source: RestDataSourceConfig = {
34
+ it('consumes canonical contracts from @ankhorage/contracts', () => {
35
+ const source: ExternalRestApiDataSourceConfig = {
31
36
  id: 'cms',
32
- kind: 'rest',
37
+ kind: 'api',
38
+ origin: 'external',
39
+ protocol: 'rest',
33
40
  baseUrl: 'https://cms.example.com',
34
41
  endpoints: {},
35
42
  };
36
43
 
37
- expect(getDataSourceKind(source)).toBe('rest');
44
+ expect(getDataSourceKind(source)).toBe('api');
38
45
  });
39
46
  });
package/src/index.ts CHANGED
@@ -1,35 +1,57 @@
1
- import type { DataSourceConfig, DataSourceKind } from '@ankhorage/contracts/data';
1
+ import type {
2
+ ApiOrigin,
3
+ ApiProtocol,
4
+ DataSourceConfig,
5
+ DataSourceKind,
6
+ } from '@ankhorage/contracts/data';
2
7
 
8
+ export * from './discovery';
9
+ export * from './generated-api';
3
10
  export * from './graphql';
4
- export * from './managed-api';
5
11
  export * from './openapi';
6
12
  export * from './rest';
7
13
  export * from './test-runner';
8
14
 
9
15
  export const DATA_SOURCES_PACKAGE_NAME = '@ankhorage/data-sources' as const;
10
-
11
16
  export const SUPPORTED_DATA_SOURCE_KINDS = [
17
+ 'api',
12
18
  'database',
19
+ ] as const satisfies readonly DataSourceKind[];
20
+ export const SUPPORTED_API_ORIGINS = [
21
+ 'external',
22
+ 'generated',
23
+ ] as const satisfies readonly ApiOrigin[];
24
+ export const SUPPORTED_API_PROTOCOLS = [
13
25
  'graphql',
14
- 'managed-api',
15
- 'openapi',
16
26
  'rest',
17
- ] as const satisfies readonly DataSourceKind[];
27
+ ] as const satisfies readonly ApiProtocol[];
18
28
 
19
29
  export interface DataSourcesPackageInfo {
20
30
  readonly packageName: typeof DATA_SOURCES_PACKAGE_NAME;
21
31
  readonly supportedKinds: typeof SUPPORTED_DATA_SOURCE_KINDS;
32
+ readonly supportedApiOrigins: typeof SUPPORTED_API_ORIGINS;
33
+ readonly supportedApiProtocols: typeof SUPPORTED_API_PROTOCOLS;
22
34
  }
23
35
 
24
36
  export function getDataSourcesPackageInfo(): DataSourcesPackageInfo {
25
37
  return {
26
38
  packageName: DATA_SOURCES_PACKAGE_NAME,
27
39
  supportedKinds: SUPPORTED_DATA_SOURCE_KINDS,
40
+ supportedApiOrigins: SUPPORTED_API_ORIGINS,
41
+ supportedApiProtocols: SUPPORTED_API_PROTOCOLS,
28
42
  };
29
43
  }
30
44
 
31
45
  export function isSupportedDataSourceKind(kind: string): kind is DataSourceKind {
32
- return SUPPORTED_DATA_SOURCE_KINDS.some((supportedKind) => supportedKind === kind);
46
+ return SUPPORTED_DATA_SOURCE_KINDS.some((supported) => supported === kind);
47
+ }
48
+
49
+ export function isSupportedApiOrigin(origin: string): origin is ApiOrigin {
50
+ return SUPPORTED_API_ORIGINS.some((supported) => supported === origin);
51
+ }
52
+
53
+ export function isSupportedApiProtocol(protocol: string): protocol is ApiProtocol {
54
+ return SUPPORTED_API_PROTOCOLS.some((supported) => supported === protocol);
33
55
  }
34
56
 
35
57
  export function getDataSourceKind(source: DataSourceConfig): DataSourceKind {