@ankhorage/contracts 7.9.0 → 8.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 (68) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -1
  3. package/dist/appManifest/apis.d.ts +3 -0
  4. package/dist/appManifest/apis.d.ts.map +1 -0
  5. package/dist/appManifest/apis.js +81 -0
  6. package/dist/appManifest/apis.js.map +1 -0
  7. package/dist/appManifest/bindings.js +1 -1
  8. package/dist/appManifest/bindings.js.map +1 -1
  9. package/dist/appManifest/data.d.ts +5 -0
  10. package/dist/appManifest/data.d.ts.map +1 -0
  11. package/dist/appManifest/data.js +140 -0
  12. package/dist/appManifest/data.js.map +1 -0
  13. package/dist/appManifest/dataSources.d.ts.map +1 -1
  14. package/dist/appManifest/dataSources.js +23 -120
  15. package/dist/appManifest/dataSources.js.map +1 -1
  16. package/dist/appManifest/infra.d.ts.map +1 -1
  17. package/dist/appManifest/infra.js +2 -0
  18. package/dist/appManifest/infra.js.map +1 -1
  19. package/dist/appManifest.d.ts.map +1 -1
  20. package/dist/appManifest.js +4 -6
  21. package/dist/appManifest.js.map +1 -1
  22. package/dist/bindings.d.ts +2 -2
  23. package/dist/bindings.d.ts.map +1 -1
  24. package/dist/bindings.js.map +1 -1
  25. package/dist/data/apis.d.ts +42 -38
  26. package/dist/data/apis.d.ts.map +1 -1
  27. package/dist/data/apis.js +1 -7
  28. package/dist/data/apis.js.map +1 -1
  29. package/dist/data/diagnostics.d.ts +3 -2
  30. package/dist/data/diagnostics.d.ts.map +1 -1
  31. package/dist/data/diagnostics.js.map +1 -1
  32. package/dist/data/ids.d.ts +1 -0
  33. package/dist/data/ids.d.ts.map +1 -1
  34. package/dist/data/ids.js.map +1 -1
  35. package/dist/data/sources.d.ts +5 -42
  36. package/dist/data/sources.d.ts.map +1 -1
  37. package/dist/data/sources.js.map +1 -1
  38. package/dist/requirements.d.ts +1 -1
  39. package/dist/requirements.d.ts.map +1 -1
  40. package/dist/requirements.js +1 -0
  41. package/dist/requirements.js.map +1 -1
  42. package/dist/types.d.ts +2 -3
  43. package/dist/types.d.ts.map +1 -1
  44. package/dist/types.js.map +1 -1
  45. package/package.json +1 -1
  46. package/src/appManifest/apis.ts +98 -0
  47. package/src/appManifest/bindings.ts +1 -1
  48. package/src/appManifest/data.ts +179 -0
  49. package/src/appManifest/dataSources.ts +30 -157
  50. package/src/appManifest/infra.ts +2 -0
  51. package/src/appManifest.test.ts +96 -40
  52. package/src/appManifest.ts +4 -6
  53. package/src/bindings.test.ts +36 -43
  54. package/src/bindings.ts +2 -2
  55. package/src/data/apis.test.ts +55 -45
  56. package/src/data/apis.ts +49 -46
  57. package/src/data/data.test.ts +19 -160
  58. package/src/data/diagnostics.ts +3 -1
  59. package/src/data/ids.ts +1 -0
  60. package/src/data/sources.ts +5 -53
  61. package/src/requirements.test.ts +23 -0
  62. package/src/requirements.ts +1 -0
  63. package/src/types.ts +2 -3
  64. package/dist/appManifest/generatedApis.d.ts +0 -2
  65. package/dist/appManifest/generatedApis.d.ts.map +0 -1
  66. package/dist/appManifest/generatedApis.js +0 -87
  67. package/dist/appManifest/generatedApis.js.map +0 -1
  68. package/src/appManifest/generatedApis.ts +0 -122
@@ -43,6 +43,41 @@ function createManifest(): Record<string, unknown> {
43
43
  storage: { provider: 'auto', buckets: ['media'] },
44
44
  state: { provider: 'legend', persistence: 'local' },
45
45
  networking: { domain: 'example.test', cdn: false },
46
+ apis: [
47
+ {
48
+ id: 'nutrition',
49
+ origin: 'external',
50
+ protocol: 'rest',
51
+ baseUrl: 'https://api.ankhorage.com/v1/nutrition',
52
+ schemas: {
53
+ Product: {
54
+ type: 'object',
55
+ required: ['id', 'name'],
56
+ properties: {
57
+ id: { type: 'string', format: 'uuid' },
58
+ name: { type: 'string' },
59
+ },
60
+ },
61
+ },
62
+ endpoints: {
63
+ products: {
64
+ id: 'products',
65
+ kind: 'http',
66
+ path: '/products',
67
+ operations: {
68
+ 'products.list': {
69
+ id: 'products.list',
70
+ endpointId: 'products',
71
+ protocol: 'http',
72
+ intent: 'read',
73
+ method: 'GET',
74
+ path: '/products',
75
+ },
76
+ },
77
+ },
78
+ },
79
+ },
80
+ ],
46
81
  modules: ['expo-localization'],
47
82
  modulesConfig: { localization: { defaultLocale: 'en' } },
48
83
  },
@@ -76,52 +111,22 @@ function createManifest(): Record<string, unknown> {
76
111
  dataLoaders: [
77
112
  {
78
113
  kind: 'operation',
79
- id: 'load-users',
80
- operation: { dataSourceId: 'external', endpointId: 'main', operationId: 'list' },
114
+ id: 'load-products',
115
+ operation: { apiId: 'nutrition', endpointId: 'products', operationId: 'products.list' },
81
116
  },
82
117
  ],
83
118
  requires: {
84
119
  permissions: [{ permission: 'camera' }],
85
- capabilities: [{ capability: 'barcodeScanner' }],
120
+ capabilities: [{ capability: 'barcodeScanner' }, { capability: 'ebookReader' }],
86
121
  },
87
122
  },
88
123
  },
89
- generatedApis: {
90
- users: {
91
- id: 'users',
92
- protocol: 'rest',
93
- basePath: '/users',
94
- database: { id: 'primary', kind: 'database' },
95
- resources: [
96
- {
97
- id: 'users',
98
- path: '/users',
99
- collection: {
100
- name: 'users',
101
- fields: [{ name: 'id', type: 'uuid', required: true }],
102
- primaryKey: 'id',
103
- },
104
- operations: ['list', 'read'],
105
- },
106
- ],
107
- },
108
- },
109
124
  dataSources: {
110
- external: {
111
- id: 'external',
112
- kind: 'api',
113
- origin: 'external',
114
- protocol: 'rest',
115
- baseUrl: 'https://example.test',
116
- endpoints: {
117
- main: {
118
- id: 'main',
119
- kind: 'http',
120
- operations: {
121
- list: { id: 'list', protocol: 'rest', intent: 'read', path: '/users' },
122
- },
123
- },
124
- },
125
+ primary: {
126
+ id: 'primary',
127
+ kind: 'database',
128
+ adapter: { id: 'primary-db', kind: 'database' },
129
+ endpoints: {},
125
130
  },
126
131
  },
127
132
  dataBindings: {
@@ -134,14 +139,13 @@ function createManifest(): Record<string, unknown> {
134
139
  },
135
140
  },
136
141
  settings: {
137
- apiBaseUrl: 'https://example.test/api',
138
142
  localization: { defaultLocale: 'en', locales: ['en', 'de'] },
139
143
  },
140
144
  };
141
145
  }
142
146
 
143
147
  describe('AppManifest runtime parsing', () => {
144
- it('accepts the canonical manifest including optional nested sections', () => {
148
+ it('accepts the canonical manifest including infra APIs', () => {
145
149
  const manifest = createManifest();
146
150
 
147
151
  expect(isAppManifest(manifest)).toBe(true);
@@ -167,6 +171,58 @@ describe('AppManifest runtime parsing', () => {
167
171
  });
168
172
  });
169
173
 
174
+ it('rejects duplicate canonical API ids', () => {
175
+ const manifest = createManifest();
176
+ const infra = manifest.infra as Record<string, unknown>;
177
+ const apis = infra.apis as Record<string, unknown>[];
178
+ apis.push(structuredClone(apis[0]));
179
+
180
+ expect(isAppManifest(manifest)).toBe(false);
181
+ });
182
+
183
+ it('rejects generatedApis as removed parallel API state', () => {
184
+ const manifest = createManifest();
185
+ manifest.generatedApis = {};
186
+
187
+ expect(isAppManifest(manifest)).toBe(false);
188
+ });
189
+
190
+ it('rejects API-flavoured dataSources', () => {
191
+ const manifest = createManifest();
192
+ manifest.dataSources = {
193
+ external: {
194
+ id: 'external',
195
+ kind: 'api',
196
+ origin: 'external',
197
+ protocol: 'rest',
198
+ baseUrl: 'https://example.test',
199
+ endpoints: {},
200
+ },
201
+ };
202
+
203
+ expect(isAppManifest(manifest)).toBe(false);
204
+ });
205
+
206
+ it('rejects old dataSourceId API binding references', () => {
207
+ const manifest = createManifest();
208
+ const screens = manifest.screens as Record<string, Record<string, unknown>>;
209
+ const loaders = screens.home?.dataLoaders as Record<string, unknown>[];
210
+ loaders[0] = {
211
+ kind: 'operation',
212
+ operation: { dataSourceId: 'nutrition', operationId: 'products.list' },
213
+ };
214
+
215
+ expect(isAppManifest(manifest)).toBe(false);
216
+ });
217
+
218
+ it('rejects settings.apiBaseUrl as removed parallel API state', () => {
219
+ const manifest = createManifest();
220
+ const settings = manifest.settings as Record<string, unknown>;
221
+ settings.apiBaseUrl = 'https://example.test/api';
222
+
223
+ expect(isAppManifest(manifest)).toBe(false);
224
+ });
225
+
170
226
  it('rejects transient media URLs at the manifest boundary', () => {
171
227
  const manifest = createManifest();
172
228
  const media = manifest.media as Record<string, Record<string, Record<string, unknown>>>;
@@ -1,7 +1,6 @@
1
1
  import { isComponentDataBindingRegistry } from './appManifest/bindings';
2
2
  import { isDataSourceRegistry } from './appManifest/dataSources';
3
3
  import { isAppDeployManifest } from './appManifest/deploy';
4
- import { isGeneratedApiRegistry } from './appManifest/generatedApis';
5
4
  import { isInfraManifest } from './appManifest/infra';
6
5
  import { isMediaManifest } from './appManifest/media';
7
6
  import {
@@ -11,7 +10,7 @@ import {
11
10
  isSplashScreenSpec,
12
11
  isThemeConfig,
13
12
  } from './appManifest/screens';
14
- import { isOptionalString, isRecord, isStringArray } from './appManifest/shared';
13
+ import { isRecord, isStringArray } from './appManifest/shared';
15
14
  import type { AppManifest } from './types';
16
15
 
17
16
  export type AppManifestParseResult =
@@ -29,7 +28,6 @@ const APP_MANIFEST_KEY_POLICY = {
29
28
  infra: 'required',
30
29
  navigator: 'required',
31
30
  screens: 'required',
32
- generatedApis: 'optional',
33
31
  dataSources: 'optional',
34
32
  dataBindings: 'optional',
35
33
  settings: 'required',
@@ -53,6 +51,7 @@ export function isAppManifest(value: unknown): value is AppManifest {
53
51
  return (
54
52
  isRecord(value) &&
55
53
  hasRequiredManifestKeys(value) &&
54
+ !('generatedApis' in value) &&
56
55
  isManifestMetadata(value.metadata) &&
57
56
  Array.isArray(value.themes) &&
58
57
  value.themes.every(isThemeConfig) &&
@@ -64,7 +63,6 @@ export function isAppManifest(value: unknown): value is AppManifest {
64
63
  isInfraManifest(value.infra) &&
65
64
  isNavigatorSpec(value.navigator) &&
66
65
  isScreenRegistry(value.screens) &&
67
- (value.generatedApis === undefined || isGeneratedApiRegistry(value.generatedApis)) &&
68
66
  (value.dataSources === undefined || isDataSourceRegistry(value.dataSources)) &&
69
67
  (value.dataBindings === undefined || isComponentDataBindingRegistry(value.dataBindings)) &&
70
68
  isAppSettings(value.settings)
@@ -84,9 +82,9 @@ function isActiveThemeMode(value: unknown): boolean {
84
82
  function isAppSettings(value: unknown): boolean {
85
83
  return (
86
84
  isRecord(value) &&
85
+ !('apiBaseUrl' in value) &&
87
86
  isRecord(value.localization) &&
88
87
  typeof value.localization.defaultLocale === 'string' &&
89
- isStringArray(value.localization.locales) &&
90
- isOptionalString(value.apiBaseUrl)
88
+ isStringArray(value.localization.locales)
91
89
  );
92
90
  }
@@ -26,7 +26,7 @@ describe('component data-binding contracts', () => {
26
26
  source: {
27
27
  kind: 'operation',
28
28
  operation: {
29
- dataSourceId: 'cms',
29
+ apiId: 'cms',
30
30
  endpointId: 'pages',
31
31
  operationId: 'pages.getHome',
32
32
  },
@@ -66,7 +66,7 @@ describe('component data-binding contracts', () => {
66
66
  {
67
67
  kind: 'operation',
68
68
  operation: {
69
- dataSourceId: 'search-api',
69
+ apiId: 'search-api',
70
70
  operationId: 'search.query',
71
71
  },
72
72
  path: '$.results',
@@ -83,7 +83,7 @@ describe('component data-binding contracts', () => {
83
83
  ]);
84
84
  });
85
85
 
86
- it('serializes an event binding that executes a data-source operation', () => {
86
+ it('serializes an event binding that executes an API operation', () => {
87
87
  const input: BindingInputMap = {
88
88
  email: {
89
89
  kind: 'source',
@@ -122,7 +122,7 @@ describe('component data-binding contracts', () => {
122
122
  target: {
123
123
  kind: 'operation',
124
124
  operation: {
125
- dataSourceId: 'contact-api',
125
+ apiId: 'contact-api',
126
126
  endpointId: 'messages',
127
127
  operationId: 'messages.create',
128
128
  },
@@ -165,7 +165,7 @@ describe('component data-binding contracts', () => {
165
165
  kind: 'operation',
166
166
  id: 'product-detail',
167
167
  operation: {
168
- dataSourceId: 'nutrition-api',
168
+ apiId: 'nutrition',
169
169
  endpointId: 'products',
170
170
  operationId: 'nutrition.products.getById',
171
171
  },
@@ -186,7 +186,7 @@ describe('component data-binding contracts', () => {
186
186
 
187
187
  it('serializes scanner lookup and conditional navigation intent using existing generic bindings', () => {
188
188
  const productLookupOperation = {
189
- dataSourceId: 'nutrition-api',
189
+ apiId: 'nutrition',
190
190
  endpointId: 'products',
191
191
  operationId: 'products.lookupByBarcode',
192
192
  } as const;
@@ -308,7 +308,7 @@ describe('component data-binding contracts', () => {
308
308
 
309
309
  it('serializes repeated container nodes with generic item-context bindings', () => {
310
310
  const productsListOperation = {
311
- dataSourceId: 'catalog-api',
311
+ apiId: 'catalog-api',
312
312
  endpointId: 'products',
313
313
  operationId: 'products.list',
314
314
  } as const;
@@ -439,7 +439,7 @@ describe('component data-binding contracts', () => {
439
439
  target: {
440
440
  kind: 'operation',
441
441
  operation: {
442
- dataSourceId: 'contact-api',
442
+ apiId: 'contact-api',
443
443
  operationId: 'messages.create',
444
444
  },
445
445
  },
@@ -453,12 +453,12 @@ describe('component data-binding contracts', () => {
453
453
  expect(bindings['submit-button']?.events?.press?.[0]?.target.kind).toBe('operation');
454
454
  });
455
455
 
456
- it('serializes app-level dataSources and dataBindings on the manifest', () => {
456
+ it('serializes app-level APIs and dataBindings on the manifest', () => {
457
457
  const propBinding: PropBinding = {
458
458
  source: {
459
459
  kind: 'operation',
460
460
  operation: {
461
- dataSourceId: 'cms',
461
+ apiId: 'cms',
462
462
  endpointId: 'pages',
463
463
  operationId: 'pages.getHome',
464
464
  },
@@ -490,6 +490,31 @@ describe('component data-binding contracts', () => {
490
490
  ],
491
491
  activeThemeId: 'default',
492
492
  infra: {
493
+ apis: [
494
+ {
495
+ id: 'cms',
496
+ origin: 'external',
497
+ protocol: 'rest',
498
+ baseUrl: 'https://cms.example.com',
499
+ endpoints: {
500
+ pages: {
501
+ id: 'pages',
502
+ kind: 'http',
503
+ path: '/pages/home',
504
+ operations: {
505
+ 'pages.getHome': {
506
+ id: 'pages.getHome',
507
+ endpointId: 'pages',
508
+ protocol: 'http',
509
+ intent: 'read',
510
+ method: 'GET',
511
+ path: '/pages/home',
512
+ },
513
+ },
514
+ },
515
+ },
516
+ },
517
+ ],
493
518
  modules: [],
494
519
  },
495
520
  navigator: {
@@ -517,30 +542,6 @@ describe('component data-binding contracts', () => {
517
542
  },
518
543
  },
519
544
  },
520
- dataSources: {
521
- cms: {
522
- id: 'cms',
523
- kind: 'rest',
524
- baseUrl: 'https://cms.example.com',
525
- endpoints: {
526
- pages: {
527
- id: 'pages',
528
- kind: 'http',
529
- path: '/pages/home',
530
- operations: {
531
- 'pages.getHome': {
532
- id: 'pages.getHome',
533
- endpointId: 'pages',
534
- protocol: 'http',
535
- intent: 'read',
536
- method: 'GET',
537
- path: '/pages/home',
538
- },
539
- },
540
- },
541
- },
542
- },
543
- },
544
545
  dataBindings: {
545
546
  'hero-title': {
546
547
  componentId: 'hero-title',
@@ -555,19 +556,11 @@ describe('component data-binding contracts', () => {
555
556
  defaultLocale: 'en',
556
557
  locales: ['en'],
557
558
  },
558
- authFlow: {
559
- signInRoute: '/sign-in',
560
- signUpRoute: '/sign-up',
561
- signOutRoute: '/sign-out',
562
- forgotPasswordRoute: '/forgot-password',
563
- postSignInRoute: '/',
564
- unauthorizedRoute: '/sign-in',
565
- },
566
559
  },
567
560
  };
568
561
 
569
562
  assertSerializable(manifest);
570
563
  expect(manifest.dataBindings?.['hero-title']?.props?.children?.source.kind).toBe('operation');
571
- expect(manifest.dataSources?.cms?.kind).toBe('rest');
564
+ expect(manifest.infra.apis?.[0]?.id).toBe('cms');
572
565
  });
573
566
  });
package/src/bindings.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { DataSourceId, EndpointId, OperationId } from './data';
1
+ import type { ApiId, EndpointId, OperationId } from './data';
2
2
 
3
3
  export type ComponentInstanceId = string;
4
4
  export type ComponentTypeId = string;
@@ -18,7 +18,7 @@ export type BindingDataPath = string;
18
18
  export type BindingValueTransform = 'lowercase' | 'trim' | 'uppercase';
19
19
 
20
20
  export interface BindingOperationRef {
21
- readonly dataSourceId: DataSourceId;
21
+ readonly apiId: ApiId;
22
22
  readonly operationId: OperationId;
23
23
  readonly endpointId?: EndpointId;
24
24
  }
@@ -1,69 +1,79 @@
1
1
  import { describe, expect, it } from 'bun:test';
2
2
 
3
- import type { GeneratedApiDefinition, GeneratedApiRegistry } from './apis';
4
- import { GENERATED_API_CRUD_OPERATIONS } from './apis';
3
+ import type {
4
+ ApiDefinitionList,
5
+ ExternalRestApiDefinition,
6
+ InternalRestApiDefinition,
7
+ } from './apis';
5
8
 
6
9
  function assertSerializable<TValue>(value: TValue): void {
7
10
  expect(JSON.parse(JSON.stringify(value))).toEqual(value);
8
11
  }
9
12
 
10
- function createGeneratedApi(): GeneratedApiDefinition {
13
+ function createNutritionApi(): ExternalRestApiDefinition {
11
14
  return {
12
- id: 'catalog-api',
15
+ id: 'nutrition',
16
+ origin: 'external',
13
17
  protocol: 'rest',
14
- name: 'Catalog API',
15
- description: 'Generated CRUD API for catalog resources.',
16
- basePath: '/api/catalog',
17
- database: {
18
- id: 'primary-db',
19
- kind: 'database',
20
- packageName: '@ankhorage/supabase-db',
21
- },
22
- auth: {
23
- required: true,
24
- roles: ['editor'],
18
+ name: 'Nutrition API',
19
+ baseUrl: 'https://api.ankhorage.com/v1/nutrition',
20
+ schemas: {
21
+ Product: {
22
+ type: 'object',
23
+ required: ['id', 'name'],
24
+ properties: {
25
+ id: { type: 'string', format: 'uuid' },
26
+ name: { type: 'string' },
27
+ },
28
+ },
25
29
  },
26
- resources: [
27
- {
30
+ endpoints: {
31
+ products: {
28
32
  id: 'products',
29
- name: 'Products',
33
+ kind: 'http',
30
34
  path: '/products',
31
- collection: {
32
- name: 'products',
33
- schema: 'public',
34
- primaryKey: 'id',
35
- fields: [
36
- { name: 'id', type: 'uuid', required: true, unique: true },
37
- { name: 'name', type: 'text', required: true },
38
- { name: 'price', type: 'number', required: true },
39
- ],
35
+ operations: {
36
+ 'products.list': {
37
+ id: 'products.list',
38
+ endpointId: 'products',
39
+ protocol: 'http',
40
+ intent: 'read',
41
+ method: 'GET',
42
+ path: '/products',
43
+ },
40
44
  },
41
- operations: GENERATED_API_CRUD_OPERATIONS,
42
- seed: [{ id: 'product-1', name: 'Keyboard', price: 120 }],
43
- policies: [
44
- { id: 'catalog.read', operation: 'list' },
45
- { id: 'catalog.write', operation: 'create' },
46
- ],
47
45
  },
48
- ],
46
+ },
49
47
  };
50
48
  }
51
49
 
52
- describe('generated API desired-state contracts', () => {
53
- it('serializes generated REST/CRUD resources independently of runtime operations', () => {
54
- const api = createGeneratedApi();
50
+ describe('canonical API contracts', () => {
51
+ it('serializes external REST schemas, endpoints, and operations', () => {
52
+ const api = createNutritionApi();
55
53
 
56
54
  assertSerializable(api);
57
- expect(api.database.kind).toBe('database');
58
- expect(api.resources[0]?.operations).toEqual(GENERATED_API_CRUD_OPERATIONS);
59
- expect(api.resources[0]?.seed?.[0]?.name).toBe('Keyboard');
55
+ expect(api.baseUrl).toBe('https://api.ankhorage.com/v1/nutrition');
56
+ expect(api.endpoints.products?.operations['products.list']?.method).toBe('GET');
60
57
  });
61
58
 
62
- it('stores generated APIs in a dedicated canonical registry', () => {
63
- const api = createGeneratedApi();
64
- const registry: GeneratedApiRegistry = { [api.id]: api };
59
+ it('stores canonical APIs as an ordered infra list', () => {
60
+ const apis: ApiDefinitionList = [createNutritionApi()];
65
61
 
66
- assertSerializable(registry);
67
- expect(registry['catalog-api']?.protocol).toBe('rest');
62
+ assertSerializable(apis);
63
+ expect(apis[0]?.id).toBe('nutrition');
64
+ });
65
+
66
+ it('reserves an internal REST API identity without persistence implementation', () => {
67
+ const api: InternalRestApiDefinition = {
68
+ id: 'future-internal-api',
69
+ origin: 'internal',
70
+ protocol: 'rest',
71
+ basePath: '/v1/internal',
72
+ endpoints: {},
73
+ };
74
+
75
+ assertSerializable(api);
76
+ expect('database' in api).toBe(false);
77
+ expect('adapter' in api).toBe(false);
68
78
  });
69
79
  });
package/src/data/apis.ts CHANGED
@@ -1,62 +1,65 @@
1
- import type { DbCollectionDefinition } from '../db';
2
- import type { DatabaseAdapterRef } from './refs';
1
+ import type { DataEndpointRegistry } from './endpoints';
2
+ import type { ApiId } from './ids';
3
+ import type { CredentialRef } from './refs';
4
+ import type { DataSchemaRegistry } from './schemas';
3
5
  import type { DataContractValue } from './values';
4
6
 
5
- export const GENERATED_API_CRUD_OPERATIONS = [
6
- 'list',
7
- 'read',
8
- 'create',
9
- 'update',
10
- 'delete',
11
- ] as const;
12
- export type GeneratedApiCrudOperation = (typeof GENERATED_API_CRUD_OPERATIONS)[number];
13
-
14
- export type GeneratedApiId = string;
15
- export type GeneratedApiResourceId = string;
16
- export type GeneratedApiSeedRecord = Readonly<Record<string, DataContractValue>>;
17
-
18
- export interface GeneratedApiAuthRequirement {
19
- readonly required?: boolean;
20
- readonly roles?: readonly string[];
21
- readonly permissions?: readonly string[];
22
- readonly policy?: string;
7
+ export type ApiOrigin = 'external' | 'internal';
8
+ export type ApiProtocol = 'graphql' | 'rest';
9
+
10
+ export interface ApiBaseDefinition {
11
+ readonly id: ApiId;
12
+ readonly origin: ApiOrigin;
13
+ readonly protocol: ApiProtocol;
14
+ readonly name?: string;
15
+ readonly description?: string;
16
+ readonly credential?: CredentialRef;
17
+ readonly endpoints: DataEndpointRegistry;
18
+ readonly schemas?: DataSchemaRegistry;
23
19
  readonly metadata?: DataContractValue;
24
20
  }
25
21
 
26
- export interface GeneratedApiOperationPolicyRef {
27
- readonly id: string;
28
- readonly operation?: GeneratedApiCrudOperation;
22
+ export interface OpenApiDocumentRef {
23
+ readonly url?: string;
24
+ readonly documentId?: string;
25
+ readonly version?: string;
29
26
  }
30
27
 
31
- export interface GeneratedApiResourceDefinition {
32
- readonly id: GeneratedApiResourceId;
33
- readonly name?: string;
34
- readonly description?: string;
35
- readonly path: string;
36
- readonly collection: DbCollectionDefinition;
37
- readonly operations: readonly GeneratedApiCrudOperation[];
38
- readonly seed?: readonly GeneratedApiSeedRecord[];
39
- readonly policies?: readonly GeneratedApiOperationPolicyRef[];
40
- readonly metadata?: DataContractValue;
28
+ export interface ExternalRestApiDefinition extends ApiBaseDefinition {
29
+ readonly origin: 'external';
30
+ readonly protocol: 'rest';
31
+ readonly baseUrl: string;
32
+ readonly openApi?: OpenApiDocumentRef;
33
+ }
34
+
35
+ export interface GraphQlIntrospectionConfig {
36
+ readonly enabled: boolean;
37
+ readonly schemaVersion?: string;
38
+ }
39
+
40
+ export interface ExternalGraphQlApiDefinition extends ApiBaseDefinition {
41
+ readonly origin: 'external';
42
+ readonly protocol: 'graphql';
43
+ readonly endpointUrl: string;
44
+ readonly introspection?: GraphQlIntrospectionConfig;
41
45
  }
42
46
 
43
47
  /**
44
- * Canonical desired state for an API authored and generated by Ankhorage.
48
+ * Consumer-facing identity for an Ankhorage-owned API.
45
49
  *
46
- * The initial supported generated implementation is REST/CRUD backed by an
47
- * existing database adapter. Runtime and binding consumers use the normalized
48
- * generated API data-source projection instead of reading this definition.
50
+ * Internal API generation/execution is intentionally not part of the current
51
+ * contract. `basePath` reserves the portable API-facing location only; later
52
+ * infrastructure/runtime packages may implement it without changing bindings.
49
53
  */
50
- export interface GeneratedApiDefinition {
51
- readonly id: GeneratedApiId;
54
+ export interface InternalRestApiDefinition extends ApiBaseDefinition {
55
+ readonly origin: 'internal';
52
56
  readonly protocol: 'rest';
53
- readonly name?: string;
54
- readonly description?: string;
55
57
  readonly basePath: string;
56
- readonly database: DatabaseAdapterRef;
57
- readonly resources: readonly GeneratedApiResourceDefinition[];
58
- readonly auth?: GeneratedApiAuthRequirement;
59
- readonly metadata?: DataContractValue;
60
58
  }
61
59
 
62
- export type GeneratedApiRegistry = Readonly<Record<GeneratedApiId, GeneratedApiDefinition>>;
60
+ export type ApiDefinition =
61
+ | ExternalGraphQlApiDefinition
62
+ | ExternalRestApiDefinition
63
+ | InternalRestApiDefinition;
64
+
65
+ export type ApiDefinitionList = readonly ApiDefinition[];