@ankhorage/data-sources 0.6.0 → 1.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 (55) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +5 -5
  3. package/dist/discovery/index.d.ts +3 -3
  4. package/dist/discovery/index.d.ts.map +1 -1
  5. package/dist/discovery/index.js +19 -28
  6. package/dist/discovery/index.js.map +1 -1
  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 +274 -0
  10. package/dist/generated-api/index.js.map +1 -0
  11. package/dist/generated-api/validateGeneratedApiCollection.d.ts +3 -0
  12. package/dist/generated-api/validateGeneratedApiCollection.d.ts.map +1 -0
  13. package/dist/generated-api/validateGeneratedApiCollection.js +35 -0
  14. package/dist/generated-api/validateGeneratedApiCollection.js.map +1 -0
  15. package/dist/graphql/index.d.ts +3 -3
  16. package/dist/graphql/index.d.ts.map +1 -1
  17. package/dist/graphql/index.js +24 -60
  18. package/dist/graphql/index.js.map +1 -1
  19. package/dist/index.d.ts +9 -3
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +17 -4
  22. package/dist/index.js.map +1 -1
  23. package/dist/openapi/index.d.ts +2 -2
  24. package/dist/openapi/index.d.ts.map +1 -1
  25. package/dist/openapi/index.js +19 -29
  26. package/dist/openapi/index.js.map +1 -1
  27. package/dist/rest/index.d.ts +3 -3
  28. package/dist/rest/index.d.ts.map +1 -1
  29. package/dist/rest/index.js +51 -51
  30. package/dist/rest/index.js.map +1 -1
  31. package/dist/test-runner/index.d.ts.map +1 -1
  32. package/dist/test-runner/index.js +60 -104
  33. package/dist/test-runner/index.js.map +1 -1
  34. package/package.json +2 -2
  35. package/src/discovery/index.test.ts +25 -101
  36. package/src/discovery/index.ts +22 -35
  37. package/src/{managed-api → generated-api}/index.test.ts +59 -67
  38. package/src/generated-api/index.ts +414 -0
  39. package/src/generated-api/validateGeneratedApiCollection.test.ts +91 -0
  40. package/src/generated-api/validateGeneratedApiCollection.ts +76 -0
  41. package/src/graphql/index.test.ts +28 -134
  42. package/src/graphql/index.ts +29 -89
  43. package/src/index.test.ts +25 -18
  44. package/src/index.ts +28 -7
  45. package/src/openapi/index.test.ts +36 -246
  46. package/src/openapi/index.ts +22 -60
  47. package/src/rest/index.test.ts +22 -173
  48. package/src/rest/index.ts +62 -59
  49. package/src/test-runner/index.test.ts +82 -150
  50. package/src/test-runner/index.ts +73 -143
  51. package/dist/managed-api/index.d.ts +0 -30
  52. package/dist/managed-api/index.d.ts.map +0 -1
  53. package/dist/managed-api/index.js +0 -257
  54. package/dist/managed-api/index.js.map +0 -1
  55. package/src/managed-api/index.ts +0 -364
@@ -9,11 +9,7 @@ import {
9
9
  normalizeGraphQlOperationId,
10
10
  } from './index';
11
11
 
12
- function assertSerializable<TValue>(value: TValue): void {
13
- expect(JSON.parse(JSON.stringify(value))).toEqual(value);
14
- }
15
-
16
- function createIntrospection(): GraphQlIntrospectionResult {
12
+ function introspection(): GraphQlIntrospectionResult {
17
13
  return {
18
14
  __schema: {
19
15
  queryType: { name: 'Query' },
@@ -25,36 +21,15 @@ function createIntrospection(): GraphQlIntrospectionResult {
25
21
  fields: [
26
22
  {
27
23
  name: 'posts',
28
- description: 'List posts.',
29
- args: [
30
- {
31
- name: 'limit',
32
- type: { kind: 'SCALAR', name: 'Int' },
33
- defaultValue: '20',
34
- },
35
- ],
36
- type: {
37
- kind: 'LIST',
38
- ofType: { kind: 'OBJECT', name: 'Post' },
39
- },
24
+ args: [],
25
+ type: { kind: 'LIST', ofType: { kind: 'OBJECT', name: 'Post' } },
40
26
  },
41
27
  ],
42
28
  },
43
29
  {
44
30
  kind: 'OBJECT',
45
31
  name: 'Mutation',
46
- fields: [
47
- {
48
- name: 'createPost',
49
- args: [
50
- {
51
- name: 'title',
52
- type: { kind: 'NON_NULL', ofType: { kind: 'SCALAR', name: 'String' } },
53
- },
54
- ],
55
- type: { kind: 'OBJECT', name: 'Post' },
56
- },
57
- ],
32
+ fields: [{ name: 'createPost', args: [], type: { kind: 'OBJECT', name: 'Post' } }],
58
33
  },
59
34
  {
60
35
  kind: 'OBJECT',
@@ -64,10 +39,6 @@ function createIntrospection(): GraphQlIntrospectionResult {
64
39
  name: 'id',
65
40
  type: { kind: 'NON_NULL', ofType: { kind: 'SCALAR', name: 'ID' } },
66
41
  },
67
- {
68
- name: 'title',
69
- type: { kind: 'SCALAR', name: 'String' },
70
- },
71
42
  ],
72
43
  },
73
44
  ],
@@ -75,128 +46,51 @@ function createIntrospection(): GraphQlIntrospectionResult {
75
46
  };
76
47
  }
77
48
 
78
- describe('GraphQL introspection normalization', () => {
79
- it('creates the standard introspection request', () => {
80
- const request = createGraphQlIntrospectionRequest();
81
-
82
- expect(request.operationName).toBe('AnkhorageGraphQlIntrospection');
83
- expect(request.query).toContain('__schema');
84
- });
85
-
86
- it('normalizes operation ids', () => {
49
+ describe('GraphQL normalization', () => {
50
+ it('creates the standard introspection request and stable IDs', () => {
51
+ expect(createGraphQlIntrospectionRequest().query).toContain('__schema');
87
52
  expect(normalizeGraphQlOperationId('query', 'Posts')).toBe('query.posts');
88
- expect(normalizeGraphQlOperationId('mutation', 'Create Post')).toBe('mutation.create-post');
89
53
  });
90
54
 
91
- it('normalizes schemas from introspection results', () => {
92
- const schemas = normalizeGraphQlIntrospectionSchemas(createIntrospection());
93
-
94
- assertSerializable(schemas);
95
- expect(schemas?.Post?.properties?.id?.type).toBe('string');
96
- expect(schemas?.Post?.required).toContain('id');
97
- });
98
-
99
- it('normalizes query and mutation operations from introspection results', () => {
100
- const operations = normalizeGraphQlIntrospectionOperations(createIntrospection());
101
-
102
- assertSerializable(operations);
103
- expect(operations.map((operation) => operation.id)).toEqual([
104
- 'query.posts',
105
- 'mutation.createpost',
106
- ]);
107
- expect(operations[0]?.variables?.properties?.limit?.type).toBe('integer');
108
- expect(operations[1]?.variables?.required).toContain('title');
55
+ it('normalizes introspection schemas and operations', () => {
56
+ expect(normalizeGraphQlIntrospectionSchemas(introspection())?.Post?.required).toContain('id');
57
+ expect(normalizeGraphQlIntrospectionOperations(introspection()).map((item) => item.id)).toEqual(
58
+ ['query.posts', 'mutation.createpost'],
59
+ );
109
60
  });
110
61
 
111
- it('creates a serializable GraphQL data source from introspection', () => {
62
+ it('creates an external GraphQL API source', () => {
112
63
  const result = createGraphQlDataSource({
113
- id: 'content-graphql',
114
- endpointUrl: 'https://content.example.com/graphql',
115
- introspection: createIntrospection(),
116
- schemaVersion: '2026-05-19',
117
- credential: {
118
- id: 'content-token',
119
- kind: 'bearer',
120
- },
64
+ id: 'content',
65
+ endpointUrl: 'https://example.com/graphql',
66
+ introspection: introspection(),
121
67
  });
122
-
123
68
  expect(result.ok).toBe(true);
124
69
  if (result.ok) {
125
- assertSerializable(result.data);
126
- expect(result.data.kind).toBe('graphql');
127
- expect(result.data.endpointUrl).toBe('https://content.example.com/graphql');
128
- expect(result.data.introspection?.enabled).toBe(true);
70
+ expect(result.data).toMatchObject({
71
+ kind: 'api',
72
+ origin: 'external',
73
+ protocol: 'graphql',
74
+ });
129
75
  expect(result.data.endpoints.graphql?.operations['query.posts']?.intent).toBe('read');
130
- expect(result.data.endpoints.graphql?.operations['mutation.createpost']?.intent).toBe(
131
- 'action',
132
- );
133
- expect(
134
- result.data.endpoints.graphql?.operations['mutation.createpost']?.request?.schema?.required,
135
- ).toContain('title');
136
76
  }
137
77
  });
138
78
 
139
- it('creates a data source with manual GraphQL operations when introspection is unavailable', () => {
140
- const result = createGraphQlDataSource({
141
- id: 'manual-graphql',
142
- endpointUrl: 'https://manual.example.com/graphql',
79
+ it('supports manual operations and validates endpoint URLs', () => {
80
+ const manual = createGraphQlDataSource({
81
+ id: 'manual',
82
+ endpointUrl: 'https://example.com/graphql',
143
83
  introspectionEnabled: false,
144
84
  operations: [
145
85
  {
146
86
  id: 'query.viewer',
147
87
  kind: 'query',
148
- name: 'viewer',
149
88
  document: 'query Viewer { viewer { id } }',
150
- variables: { type: 'object' },
151
- response: {
152
- type: 'object',
153
- properties: {
154
- viewer: { type: 'object' },
155
- },
156
- },
157
- selectionPath: '$.data.viewer',
158
89
  },
159
90
  ],
160
91
  });
161
-
162
- expect(result.ok).toBe(true);
163
- if (result.ok) {
164
- const metadata = result.data.endpoints.graphql?.operations['query.viewer']?.metadata;
165
-
166
- expect(metadata).toBeDefined();
167
- if (metadata !== undefined && typeof metadata === 'object' && !Array.isArray(metadata)) {
168
- expect(metadata.selectionPath).toBe('$.data.viewer');
169
- }
170
- expect(result.diagnostics).toEqual([]);
171
- }
172
- });
173
-
174
- it('reports invalid endpoint URLs and disabled-introspection mismatches', () => {
175
- const result = createGraphQlDataSource({
176
- id: 'broken-graphql',
177
- endpointUrl: ' ',
178
- introspectionEnabled: false,
179
- introspection: createIntrospection(),
180
- });
181
-
182
- expect(result.ok).toBe(false);
183
- if (!result.ok) {
184
- const paths = result.diagnostics.map((diagnostic) => diagnostic.path);
185
-
186
- expect(paths).toContain('endpointUrl');
187
- expect(paths).toContain('introspection');
188
- }
189
- });
190
-
191
- it('reports missing introspection as info for manual operation workflows', () => {
192
- const result = createGraphQlDataSource({
193
- id: 'manual-graphql',
194
- endpointUrl: 'https://manual.example.com/graphql',
195
- });
196
-
197
- expect(result.ok).toBe(true);
198
- if (result.ok) {
199
- expect(result.diagnostics?.map((diagnostic) => diagnostic.code)).toContain('missing-schema');
200
- }
92
+ const invalid = createGraphQlDataSource({ id: 'broken', endpointUrl: ' ' });
93
+ expect(manual.ok).toBe(true);
94
+ expect(invalid.ok).toBe(false);
201
95
  });
202
96
  });
@@ -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,36 +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
 
3
8
  export * from './discovery';
9
+ export * from './generated-api';
4
10
  export * from './graphql';
5
- export * from './managed-api';
6
11
  export * from './openapi';
7
12
  export * from './rest';
8
13
  export * from './test-runner';
9
14
 
10
15
  export const DATA_SOURCES_PACKAGE_NAME = '@ankhorage/data-sources' as const;
11
-
12
16
  export const SUPPORTED_DATA_SOURCE_KINDS = [
17
+ 'api',
13
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 = [
14
25
  'graphql',
15
- 'managed-api',
16
- 'openapi',
17
26
  'rest',
18
- ] as const satisfies readonly DataSourceKind[];
27
+ ] as const satisfies readonly ApiProtocol[];
19
28
 
20
29
  export interface DataSourcesPackageInfo {
21
30
  readonly packageName: typeof DATA_SOURCES_PACKAGE_NAME;
22
31
  readonly supportedKinds: typeof SUPPORTED_DATA_SOURCE_KINDS;
32
+ readonly supportedApiOrigins: typeof SUPPORTED_API_ORIGINS;
33
+ readonly supportedApiProtocols: typeof SUPPORTED_API_PROTOCOLS;
23
34
  }
24
35
 
25
36
  export function getDataSourcesPackageInfo(): DataSourcesPackageInfo {
26
37
  return {
27
38
  packageName: DATA_SOURCES_PACKAGE_NAME,
28
39
  supportedKinds: SUPPORTED_DATA_SOURCE_KINDS,
40
+ supportedApiOrigins: SUPPORTED_API_ORIGINS,
41
+ supportedApiProtocols: SUPPORTED_API_PROTOCOLS,
29
42
  };
30
43
  }
31
44
 
32
45
  export function isSupportedDataSourceKind(kind: string): kind is DataSourceKind {
33
- 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);
34
55
  }
35
56
 
36
57
  export function getDataSourceKind(source: DataSourceConfig): DataSourceKind {