@ankhorage/contracts 7.8.0 → 8.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 (67) 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/cli/index.d.ts +6 -1
  26. package/dist/cli/index.d.ts.map +1 -1
  27. package/dist/cli/index.js.map +1 -1
  28. package/dist/data/apis.d.ts +42 -38
  29. package/dist/data/apis.d.ts.map +1 -1
  30. package/dist/data/apis.js +1 -7
  31. package/dist/data/apis.js.map +1 -1
  32. package/dist/data/diagnostics.d.ts +3 -2
  33. package/dist/data/diagnostics.d.ts.map +1 -1
  34. package/dist/data/diagnostics.js.map +1 -1
  35. package/dist/data/ids.d.ts +1 -0
  36. package/dist/data/ids.d.ts.map +1 -1
  37. package/dist/data/ids.js.map +1 -1
  38. package/dist/data/sources.d.ts +5 -42
  39. package/dist/data/sources.d.ts.map +1 -1
  40. package/dist/data/sources.js.map +1 -1
  41. package/dist/types.d.ts +2 -3
  42. package/dist/types.d.ts.map +1 -1
  43. package/dist/types.js.map +1 -1
  44. package/package.json +1 -1
  45. package/src/appManifest/apis.ts +98 -0
  46. package/src/appManifest/bindings.ts +1 -1
  47. package/src/appManifest/data.ts +179 -0
  48. package/src/appManifest/dataSources.ts +30 -157
  49. package/src/appManifest/infra.ts +2 -0
  50. package/src/appManifest.test.ts +95 -39
  51. package/src/appManifest.ts +4 -6
  52. package/src/bindings.test.ts +36 -43
  53. package/src/bindings.ts +2 -2
  54. package/src/cli/index.ts +6 -1
  55. package/src/cli.test.ts +20 -0
  56. package/src/data/apis.test.ts +55 -45
  57. package/src/data/apis.ts +49 -46
  58. package/src/data/data.test.ts +19 -160
  59. package/src/data/diagnostics.ts +3 -1
  60. package/src/data/ids.ts +1 -0
  61. package/src/data/sources.ts +5 -53
  62. package/src/types.ts +2 -3
  63. package/dist/appManifest/generatedApis.d.ts +0 -2
  64. package/dist/appManifest/generatedApis.d.ts.map +0 -1
  65. package/dist/appManifest/generatedApis.js +0 -87
  66. package/dist/appManifest/generatedApis.js.map +0 -1
  67. 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,8 +111,8 @@ 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: {
@@ -86,42 +121,12 @@ function createManifest(): Record<string, unknown> {
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
  }
package/src/cli/index.ts CHANGED
@@ -5,7 +5,12 @@ export type AnkhProviderReference = `./${string}`;
5
5
  export type AnkhCapabilityId = `${string}.${string}`;
6
6
 
7
7
  export interface AnkhCommandDescriptor {
8
- readonly path: readonly [string, ...string[]];
8
+ /**
9
+ * Command path relative to the provider category.
10
+ *
11
+ * An empty path declares the command at the category root.
12
+ */
13
+ readonly path: readonly string[];
9
14
  readonly summary: string;
10
15
  readonly capability: AnkhCapabilityId;
11
16
  readonly aliases?: readonly string[];
package/src/cli.test.ts CHANGED
@@ -38,6 +38,26 @@ describe('cli contracts', () => {
38
38
  expect(manifest.commands[0].path).toEqual(['up']);
39
39
  });
40
40
 
41
+ it('accepts an empty path as a category-root command', () => {
42
+ const deployCommand = {
43
+ path: [],
44
+ summary: 'Deploy the authored release',
45
+ capability: 'deploy.release',
46
+ examples: ['ankh deploy'],
47
+ } as const satisfies AnkhCommandDescriptor;
48
+
49
+ const manifest = {
50
+ id: '@ankhorage/deploy',
51
+ category: 'deploy',
52
+ version: '1.0.0',
53
+ capabilities: ['deploy.release'],
54
+ commands: [deployCommand],
55
+ } as const satisfies AnkhCommandProviderManifest;
56
+
57
+ expect(manifest.commands[0].path).toEqual([]);
58
+ expect(JSON.parse(JSON.stringify(manifest))).toEqual(manifest);
59
+ });
60
+
41
61
  it('accepts metadata-only packages without a provider module', () => {
42
62
  const packageMetadata = {
43
63
  category: 'contracts',
@@ -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
  });