@ankhorage/contracts 4.0.1 → 4.0.2

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.
@@ -1,12 +1,12 @@
1
1
  import { describe, expect, it } from 'bun:test';
2
2
 
3
3
  import type {
4
+ DatabaseDataSourceConfig,
4
5
  DataSourceConfig,
5
6
  DataSourceDiagnostic,
6
- GraphQlDataSourceConfig,
7
- ManagedApiDataSourceConfig,
8
- OpenApiDataSourceConfig,
9
- RestDataSourceConfig,
7
+ ExternalGraphQlApiDataSourceConfig,
8
+ ExternalRestApiDataSourceConfig,
9
+ GeneratedRestApiDataSourceConfig,
10
10
  } from './index';
11
11
 
12
12
  function assertSerializable<TValue>(value: TValue): void {
@@ -14,10 +14,12 @@ function assertSerializable<TValue>(value: TValue): void {
14
14
  }
15
15
 
16
16
  describe('data source contracts', () => {
17
- it('serializes a manual REST data source with read and create operations', () => {
18
- const source: RestDataSourceConfig = {
17
+ it('serializes an external REST API with manual operations', () => {
18
+ const source: ExternalRestApiDataSourceConfig = {
19
19
  id: 'cms-rest',
20
- kind: 'rest',
20
+ kind: 'api',
21
+ origin: 'external',
22
+ protocol: 'rest',
21
23
  name: 'CMS REST API',
22
24
  baseUrl: 'https://cms.example.com',
23
25
  credential: {
@@ -25,17 +27,6 @@ describe('data source contracts', () => {
25
27
  kind: 'apiKey',
26
28
  label: 'CMS API key',
27
29
  },
28
- schemas: {
29
- post: {
30
- type: 'object',
31
- required: ['id', 'title'],
32
- properties: {
33
- id: { type: 'string' },
34
- title: { type: 'string' },
35
- published: { type: 'boolean', default: false },
36
- },
37
- },
38
- },
39
30
  endpoints: {
40
31
  posts: {
41
32
  id: 'posts',
@@ -49,43 +40,6 @@ describe('data source contracts', () => {
49
40
  intent: 'read',
50
41
  method: 'GET',
51
42
  path: '/posts',
52
- request: {
53
- parameters: [
54
- {
55
- name: 'limit',
56
- location: 'query',
57
- schema: { type: 'integer', default: 20 },
58
- },
59
- ],
60
- },
61
- response: {
62
- status: 200,
63
- schema: {
64
- type: 'array',
65
- items: { ref: { id: 'post' } },
66
- },
67
- },
68
- pagination: {
69
- kind: 'limit-offset',
70
- limitParameter: 'limit',
71
- offsetParameter: 'offset',
72
- },
73
- },
74
- 'posts.create': {
75
- id: 'posts.create',
76
- endpointId: 'posts',
77
- protocol: 'http',
78
- intent: 'create',
79
- method: 'POST',
80
- path: '/posts',
81
- request: {
82
- contentType: 'application/json',
83
- schema: { ref: { id: 'post' } },
84
- },
85
- response: {
86
- status: 201,
87
- schemaRef: { id: 'post' },
88
- },
89
43
  },
90
44
  },
91
45
  },
@@ -94,74 +48,38 @@ describe('data source contracts', () => {
94
48
 
95
49
  assertSerializable(source);
96
50
  expect(source.endpoints.posts?.operations['posts.list']?.intent).toBe('read');
97
- expect(source.endpoints.posts?.operations['posts.create']?.intent).toBe('create');
98
51
  });
99
52
 
100
- it('serializes an imported OpenAPI data source', () => {
101
- const source: OpenApiDataSourceConfig = {
102
- id: 'shop-openapi',
103
- kind: 'openapi',
104
- name: 'Shop OpenAPI',
53
+ it('models OpenAPI as optional metadata on an external REST API', () => {
54
+ const source: ExternalRestApiDataSourceConfig = {
55
+ id: 'shop-api',
56
+ kind: 'api',
57
+ origin: 'external',
58
+ protocol: 'rest',
59
+ name: 'Shop API',
105
60
  baseUrl: 'https://shop.example.com/api',
106
- import: {
61
+ openApi: {
107
62
  url: 'https://shop.example.com/openapi.json',
108
- version: '2026-05-19',
109
- },
110
- endpoints: {
111
- products: {
112
- id: 'products',
113
- kind: 'http',
114
- path: '/products/{productId}',
115
- operations: {
116
- getProduct: {
117
- id: 'getProduct',
118
- endpointId: 'products',
119
- protocol: 'http',
120
- intent: 'read',
121
- method: 'GET',
122
- path: '/products/{productId}',
123
- request: {
124
- parameters: [
125
- {
126
- name: 'productId',
127
- location: 'path',
128
- required: true,
129
- schema: { type: 'string' },
130
- },
131
- ],
132
- },
133
- response: {
134
- status: 200,
135
- schema: {
136
- type: 'object',
137
- properties: {
138
- id: { type: 'string' },
139
- name: { type: 'string' },
140
- price: { type: 'number' },
141
- },
142
- },
143
- },
144
- },
145
- },
146
- },
63
+ version: '2026-08-06',
147
64
  },
65
+ endpoints: {},
148
66
  };
149
67
 
150
68
  assertSerializable(source);
151
- expect(source.import?.url).toContain('openapi.json');
152
- expect(
153
- source.endpoints.products?.operations.getProduct?.request?.parameters?.[0]?.location,
154
- ).toBe('path');
69
+ expect(source.openApi?.url).toContain('openapi.json');
70
+ expect(source.kind).toBe('api');
155
71
  });
156
72
 
157
- it('serializes a GraphQL data source with query and mutation operations', () => {
158
- const source: GraphQlDataSourceConfig = {
73
+ it('serializes an external GraphQL API independently of origin', () => {
74
+ const source: ExternalGraphQlApiDataSourceConfig = {
159
75
  id: 'content-graphql',
160
- kind: 'graphql',
76
+ kind: 'api',
77
+ origin: 'external',
78
+ protocol: 'graphql',
161
79
  endpointUrl: 'https://content.example.com/graphql',
162
80
  introspection: {
163
81
  enabled: true,
164
- schemaVersion: '2026-05-19',
82
+ schemaVersion: '2026-08-06',
165
83
  },
166
84
  endpoints: {
167
85
  graphql: {
@@ -173,48 +91,6 @@ describe('data source contracts', () => {
173
91
  endpointId: 'graphql',
174
92
  protocol: 'graphql',
175
93
  intent: 'read',
176
- request: {
177
- schema: {
178
- type: 'object',
179
- properties: {
180
- search: { type: 'string' },
181
- },
182
- },
183
- },
184
- response: {
185
- schema: {
186
- type: 'object',
187
- properties: {
188
- posts: {
189
- type: 'array',
190
- items: { type: 'object' },
191
- },
192
- },
193
- },
194
- },
195
- },
196
- CreatePost: {
197
- id: 'CreatePost',
198
- endpointId: 'graphql',
199
- protocol: 'graphql',
200
- intent: 'create',
201
- request: {
202
- schema: {
203
- type: 'object',
204
- required: ['title'],
205
- properties: {
206
- title: { type: 'string' },
207
- },
208
- },
209
- },
210
- response: {
211
- schema: {
212
- type: 'object',
213
- properties: {
214
- createPost: { type: 'object' },
215
- },
216
- },
217
- },
218
94
  },
219
95
  },
220
96
  },
@@ -222,52 +98,50 @@ describe('data source contracts', () => {
222
98
  };
223
99
 
224
100
  assertSerializable(source);
225
- expect(source.endpoints.graphql?.operations.PostsQuery?.protocol).toBe('graphql');
226
- expect(source.endpoints.graphql?.operations.CreatePost?.intent).toBe('create');
101
+ expect(source.protocol).toBe('graphql');
102
+ expect(source.endpoints.graphql?.operations.PostsQuery?.intent).toBe('read');
227
103
  });
228
104
 
229
- it('serializes a managed API backed by a database adapter reference', () => {
230
- const source: ManagedApiDataSourceConfig = {
231
- id: 'app-managed-api',
232
- kind: 'managed-api',
233
- name: 'App managed API',
105
+ it('serializes a normalized generated REST API projection', () => {
106
+ const source: GeneratedRestApiDataSourceConfig = {
107
+ id: 'catalog-api',
108
+ kind: 'api',
109
+ origin: 'generated',
110
+ protocol: 'rest',
111
+ generatedApiId: 'catalog-api',
112
+ name: 'Catalog API',
234
113
  adapter: {
235
114
  id: 'primary-db',
236
115
  kind: 'database',
237
116
  packageName: '@ankhorage/supabase-db',
238
117
  },
239
- resources: [
240
- {
241
- name: 'posts',
242
- collection: {
243
- name: 'posts',
244
- schema: 'public',
245
- primaryKey: 'id',
246
- fields: [
247
- { name: 'id', type: 'uuid', required: true, unique: true },
248
- { name: 'title', type: 'text', required: true },
249
- { name: 'published', type: 'boolean', defaultValue: false },
250
- ],
118
+ schemas: {
119
+ products: {
120
+ type: 'object',
121
+ required: ['id', 'name'],
122
+ properties: {
123
+ id: { type: 'string', format: 'uuid' },
124
+ name: { type: 'string' },
251
125
  },
252
- operations: ['list', 'read', 'create', 'update', 'delete'],
253
126
  },
254
- ],
127
+ },
255
128
  endpoints: {
256
- posts: {
257
- id: 'posts',
129
+ products: {
130
+ id: 'products',
258
131
  kind: 'database',
132
+ path: '/products',
259
133
  operations: {
260
- 'posts.list': {
261
- id: 'posts.list',
262
- endpointId: 'posts',
134
+ 'products.list': {
135
+ id: 'products.list',
136
+ endpointId: 'products',
263
137
  protocol: 'database',
264
138
  intent: 'read',
265
139
  },
266
- 'posts.delete': {
267
- id: 'posts.delete',
268
- endpointId: 'posts',
140
+ 'products.create': {
141
+ id: 'products.create',
142
+ endpointId: 'products',
269
143
  protocol: 'database',
270
- intent: 'delete',
144
+ intent: 'create',
271
145
  },
272
146
  },
273
147
  },
@@ -276,17 +150,28 @@ describe('data source contracts', () => {
276
150
 
277
151
  assertSerializable(source);
278
152
  expect(source.adapter.kind).toBe('database');
279
- expect(source.resources[0]?.collection.fields.map((field) => field.name)).toEqual([
280
- 'id',
281
- 'title',
282
- 'published',
283
- ]);
153
+ expect(source.origin).toBe('generated');
154
+ expect(source.endpoints.products?.operations['products.create']?.intent).toBe('create');
155
+ });
156
+
157
+ it('keeps database sources separate from API origin and protocol', () => {
158
+ const source: DatabaseDataSourceConfig = {
159
+ id: 'primary-db',
160
+ kind: 'database',
161
+ adapter: { id: 'supabase-db', kind: 'database' },
162
+ endpoints: {},
163
+ };
164
+
165
+ assertSerializable(source);
166
+ expect(source.kind).toBe('database');
284
167
  });
285
168
 
286
- it('accepts a provider-neutral data source registry and diagnostics', () => {
169
+ it('accepts a provider-neutral source union and diagnostics', () => {
287
170
  const source: DataSourceConfig = {
288
- id: 'public-rest',
289
- kind: 'rest',
171
+ id: 'public-api',
172
+ kind: 'api',
173
+ origin: 'external',
174
+ protocol: 'rest',
290
175
  baseUrl: 'https://public.example.com',
291
176
  endpoints: {},
292
177
  };
@@ -294,7 +179,7 @@ describe('data source contracts', () => {
294
179
  code: 'missing-operation',
295
180
  severity: 'error',
296
181
  message: 'Operation not found.',
297
- dataSourceId: 'public-rest',
182
+ dataSourceId: 'public-api',
298
183
  endpointId: 'posts',
299
184
  operationId: 'posts.list',
300
185
  };
package/src/data/refs.ts CHANGED
@@ -19,3 +19,7 @@ export interface AdapterRef {
19
19
  readonly exportName?: string;
20
20
  readonly config?: DataContractValue;
21
21
  }
22
+
23
+ export interface DatabaseAdapterRef extends AdapterRef {
24
+ readonly kind: 'database';
25
+ }
@@ -1,11 +1,12 @@
1
- import type { DbCollectionDefinition } from '../db';
2
1
  import type { DataEndpointRegistry } from './endpoints';
3
2
  import type { DataSourceId } from './ids';
4
- import type { AdapterRef, CredentialRef } from './refs';
3
+ import type { CredentialRef, DatabaseAdapterRef } from './refs';
5
4
  import type { DataSchemaRegistry } from './schemas';
6
5
  import type { DataContractValue } from './values';
7
6
 
8
- export type DataSourceKind = 'database' | 'graphql' | 'managed-api' | 'openapi' | 'rest';
7
+ export type DataSourceKind = 'api' | 'database';
8
+ export type ApiOrigin = 'external' | 'generated';
9
+ export type ApiProtocol = 'graphql' | 'rest';
9
10
 
10
11
  export interface DataSourceBaseConfig {
11
12
  readonly id: DataSourceId;
@@ -18,25 +19,28 @@ export interface DataSourceBaseConfig {
18
19
  readonly metadata?: DataContractValue;
19
20
  }
20
21
 
21
- export interface RestDataSourceConfig extends DataSourceBaseConfig {
22
- readonly kind: 'rest';
23
- readonly baseUrl: string;
22
+ export interface ApiDataSourceBaseConfig extends DataSourceBaseConfig {
23
+ readonly kind: 'api';
24
+ readonly origin: ApiOrigin;
25
+ readonly protocol: ApiProtocol;
24
26
  }
25
27
 
26
- export interface OpenApiImportRef {
28
+ export interface OpenApiDocumentRef {
27
29
  readonly url?: string;
28
30
  readonly documentId?: string;
29
31
  readonly version?: string;
30
32
  }
31
33
 
32
- export interface OpenApiDataSourceConfig extends DataSourceBaseConfig {
33
- readonly kind: 'openapi';
34
- readonly baseUrl?: string;
35
- readonly import?: OpenApiImportRef;
34
+ export interface ExternalRestApiDataSourceConfig extends ApiDataSourceBaseConfig {
35
+ readonly origin: 'external';
36
+ readonly protocol: 'rest';
37
+ readonly baseUrl: string;
38
+ readonly openApi?: OpenApiDocumentRef;
36
39
  }
37
40
 
38
- export interface GraphQlDataSourceConfig extends DataSourceBaseConfig {
39
- readonly kind: 'graphql';
41
+ export interface ExternalGraphQlApiDataSourceConfig extends ApiDataSourceBaseConfig {
42
+ readonly origin: 'external';
43
+ readonly protocol: 'graphql';
40
44
  readonly endpointUrl: string;
41
45
  readonly introspection?: {
42
46
  readonly enabled: boolean;
@@ -44,29 +48,23 @@ export interface GraphQlDataSourceConfig extends DataSourceBaseConfig {
44
48
  };
45
49
  }
46
50
 
47
- export interface DatabaseDataSourceConfig extends DataSourceBaseConfig {
48
- readonly kind: 'database';
49
- readonly adapter: AdapterRef;
51
+ export interface GeneratedRestApiDataSourceConfig extends ApiDataSourceBaseConfig {
52
+ readonly origin: 'generated';
53
+ readonly protocol: 'rest';
54
+ readonly generatedApiId: string;
55
+ readonly adapter: DatabaseAdapterRef;
50
56
  }
51
57
 
52
- export interface ManagedApiResourceConfig {
53
- readonly name: string;
54
- readonly collection: DbCollectionDefinition;
55
- readonly operations?: readonly ('create' | 'delete' | 'list' | 'read' | 'update')[];
56
- readonly metadata?: DataContractValue;
57
- }
58
+ export type ApiDataSourceConfig =
59
+ | ExternalGraphQlApiDataSourceConfig
60
+ | ExternalRestApiDataSourceConfig
61
+ | GeneratedRestApiDataSourceConfig;
58
62
 
59
- export interface ManagedApiDataSourceConfig extends DataSourceBaseConfig {
60
- readonly kind: 'managed-api';
61
- readonly adapter: AdapterRef;
62
- readonly resources: readonly ManagedApiResourceConfig[];
63
+ export interface DatabaseDataSourceConfig extends DataSourceBaseConfig {
64
+ readonly kind: 'database';
65
+ readonly adapter: DatabaseAdapterRef;
63
66
  }
64
67
 
65
- export type DataSourceConfig =
66
- | DatabaseDataSourceConfig
67
- | GraphQlDataSourceConfig
68
- | ManagedApiDataSourceConfig
69
- | OpenApiDataSourceConfig
70
- | RestDataSourceConfig;
68
+ export type DataSourceConfig = ApiDataSourceConfig | DatabaseDataSourceConfig;
71
69
 
72
70
  export type DataSourceRegistry = Readonly<Record<DataSourceId, DataSourceConfig>>;
package/src/types.ts CHANGED
@@ -6,7 +6,7 @@ import type {
6
6
  ComponentDataBindingRegistry,
7
7
  ScreenDataLoaderDefinition,
8
8
  } from './bindings';
9
- import type { AppDataManifest, DataSourceRegistry } from './data';
9
+ import type { DataSourceRegistry, GeneratedApiRegistry } from './data';
10
10
  import type { ScreenRequirements } from './requirements';
11
11
 
12
12
  export interface ThemeModeConfig {
@@ -389,7 +389,7 @@ export interface AppManifest {
389
389
  infra: InfraManifest;
390
390
  navigator: NavigatorSpec;
391
391
  screens: Record<string, ScreenSpec>;
392
- data?: AppDataManifest;
392
+ generatedApis?: GeneratedApiRegistry;
393
393
  dataSources?: DataSourceRegistry;
394
394
  dataBindings?: ComponentDataBindingRegistry;
395
395
  settings: AppSettings;