@ankhorage/contracts 4.0.0 → 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.
package/src/bindings.ts CHANGED
@@ -94,14 +94,6 @@ export type BindingInputValue =
94
94
 
95
95
  export type BindingInputMap = Readonly<Record<string, BindingInputValue>>;
96
96
 
97
- export interface ApiScreenDataLoaderDefinition {
98
- readonly kind: 'api';
99
- readonly apiId: string;
100
- readonly mode: 'byId' | 'list' | 'one' | 'random';
101
- readonly targetPath: string;
102
- readonly id?: string | number;
103
- }
104
-
105
97
  export interface OperationScreenDataLoaderDefinition {
106
98
  readonly kind: 'operation';
107
99
  readonly id?: string;
@@ -109,9 +101,7 @@ export interface OperationScreenDataLoaderDefinition {
109
101
  readonly input?: BindingInputMap;
110
102
  }
111
103
 
112
- export type ScreenDataLoaderDefinition =
113
- | ApiScreenDataLoaderDefinition
114
- | OperationScreenDataLoaderDefinition;
104
+ export type ScreenDataLoaderDefinition = OperationScreenDataLoaderDefinition;
115
105
 
116
106
  export type BindingConditionOperator = 'eq' | 'exists' | 'neq' | 'notExists';
117
107
 
@@ -1,146 +1,69 @@
1
- import { describe, expect, test } from 'bun:test';
1
+ import { describe, expect, it } from 'bun:test';
2
2
 
3
- import type {
4
- AppApiDefinition,
5
- AppApiEndpointDefinition,
6
- AppApiManifest,
7
- AppGeneratedApiDefinition,
8
- } from './apis';
9
- import { APP_API_ENDPOINT_INTENTS, APP_API_ENDPOINT_METHODS, APP_API_KINDS } from './apis';
3
+ import type { GeneratedApiDefinition, GeneratedApiRegistry } from './apis';
4
+ import { GENERATED_API_CRUD_OPERATIONS } from './apis';
10
5
 
11
- const crudEndpoints = [
12
- {
13
- id: 'players.list',
14
- method: 'GET',
15
- path: '/',
16
- intent: 'list',
17
- },
18
- {
19
- id: 'players.create',
20
- method: 'POST',
21
- path: '/',
22
- intent: 'create',
23
- },
24
- {
25
- id: 'players.read',
26
- method: 'GET',
27
- path: '/{id}',
28
- intent: 'read',
29
- },
30
- {
31
- id: 'players.update',
32
- method: 'PATCH',
33
- path: '/{id}',
34
- intent: 'update',
35
- },
36
- {
37
- id: 'players.delete',
38
- method: 'DELETE',
39
- path: '/{id}',
40
- intent: 'delete',
41
- },
42
- ] satisfies readonly AppApiEndpointDefinition[];
6
+ function assertSerializable<TValue>(value: TValue): void {
7
+ expect(JSON.parse(JSON.stringify(value))).toEqual(value);
8
+ }
43
9
 
44
- describe('app API authoring contracts', () => {
45
- test('exports stable API authoring constants', () => {
46
- expect(APP_API_KINDS).toEqual(['external', 'generated']);
47
- expect(APP_API_ENDPOINT_METHODS).toEqual([
48
- 'DELETE',
49
- 'GET',
50
- 'HEAD',
51
- 'OPTIONS',
52
- 'PATCH',
53
- 'POST',
54
- 'PUT',
55
- ]);
56
- expect(APP_API_ENDPOINT_INTENTS).toEqual([
57
- 'create',
58
- 'custom',
59
- 'delete',
60
- 'list',
61
- 'read',
62
- 'update',
63
- ]);
64
- });
65
-
66
- test('serializes generated APIs with a CRUD preset and explicit custom endpoints', () => {
67
- const generatedApi: AppGeneratedApiDefinition = {
68
- id: 'players',
69
- kind: 'generated',
70
- label: 'Players',
71
- basePath: '/api/players',
72
- preset: 'crud',
73
- resource: {
74
- kind: 'collection',
10
+ function createGeneratedApi(): GeneratedApiDefinition {
11
+ return {
12
+ id: 'catalog-api',
13
+ 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'],
25
+ },
26
+ resources: [
27
+ {
28
+ id: 'products',
29
+ name: 'Products',
30
+ path: '/products',
75
31
  collection: {
76
- name: 'players',
32
+ name: 'products',
33
+ schema: 'public',
77
34
  primaryKey: 'id',
78
35
  fields: [
79
- { name: 'id', type: 'uuid', required: true },
36
+ { name: 'id', type: 'uuid', required: true, unique: true },
80
37
  { name: 'name', type: 'text', required: true },
81
- { name: 'score', type: 'number' },
38
+ { name: 'price', type: 'number', required: true },
82
39
  ],
83
40
  },
84
- seed: [
85
- {
86
- id: 'player-1',
87
- name: 'Ada',
88
- score: 42,
89
- },
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' },
90
46
  ],
91
47
  },
92
- endpoints: [
93
- ...crudEndpoints,
94
- {
95
- id: 'players.leaderboard',
96
- method: 'GET',
97
- path: '/leaderboard',
98
- intent: 'custom',
99
- },
100
- ],
101
- };
102
- const manifest: AppApiManifest = {
103
- apis: {
104
- players: generatedApi,
105
- },
106
- };
48
+ ],
49
+ };
50
+ }
107
51
 
108
- expect(JSON.parse(JSON.stringify(manifest))).toEqual({
109
- apis: {
110
- players: generatedApi,
111
- },
112
- });
113
- expect(manifest.apis?.players?.endpoints.map((endpoint) => endpoint.path)).toEqual([
114
- '/',
115
- '/',
116
- '/{id}',
117
- '/{id}',
118
- '/{id}',
119
- '/leaderboard',
120
- ]);
52
+ describe('generated API desired-state contracts', () => {
53
+ it('serializes generated REST/CRUD resources independently of runtime operations', () => {
54
+ const api = createGeneratedApi();
55
+
56
+ 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');
121
60
  });
122
61
 
123
- test('serializes external APIs without generated resource definitions', () => {
124
- const externalApi: AppApiDefinition = {
125
- id: 'stripe',
126
- kind: 'external',
127
- label: 'Stripe',
128
- basePath: '/api/stripe',
129
- baseUrl: 'https://api.stripe.com',
130
- endpoints: [
131
- {
132
- id: 'stripe.createCheckoutSession',
133
- method: 'POST',
134
- path: '/checkout/sessions',
135
- intent: 'custom',
136
- auth: {
137
- required: true,
138
- permissions: ['payments:create'],
139
- },
140
- },
141
- ],
142
- };
62
+ it('stores generated APIs in a dedicated canonical registry', () => {
63
+ const api = createGeneratedApi();
64
+ const registry: GeneratedApiRegistry = { [api.id]: api };
143
65
 
144
- expect(JSON.parse(JSON.stringify(externalApi))).toEqual(externalApi);
66
+ assertSerializable(registry);
67
+ expect(registry['catalog-api']?.protocol).toBe('rest');
145
68
  });
146
69
  });
package/src/data/apis.ts CHANGED
@@ -1,39 +1,21 @@
1
1
  import type { DbCollectionDefinition } from '../db';
2
- import type { DataOperationRequest, DataOperationResponse } from './operations';
2
+ import type { DatabaseAdapterRef } from './refs';
3
3
  import type { DataContractValue } from './values';
4
4
 
5
- export const APP_API_KINDS = ['external', 'generated'] as const;
6
- export type AppApiKind = (typeof APP_API_KINDS)[number];
7
-
8
- export const APP_API_GENERATED_PRESETS = ['crud'] as const;
9
- export type AppApiGeneratedPreset = (typeof APP_API_GENERATED_PRESETS)[number];
10
-
11
- export const APP_API_ENDPOINT_METHODS = [
12
- 'DELETE',
13
- 'GET',
14
- 'HEAD',
15
- 'OPTIONS',
16
- 'PATCH',
17
- 'POST',
18
- 'PUT',
19
- ] as const;
20
- export type AppApiEndpointMethod = (typeof APP_API_ENDPOINT_METHODS)[number];
21
-
22
- export const APP_API_ENDPOINT_INTENTS = [
23
- 'create',
24
- 'custom',
25
- 'delete',
5
+ export const GENERATED_API_CRUD_OPERATIONS = [
26
6
  'list',
27
7
  'read',
8
+ 'create',
28
9
  'update',
10
+ 'delete',
29
11
  ] as const;
30
- export type AppApiEndpointIntent = (typeof APP_API_ENDPOINT_INTENTS)[number];
12
+ export type GeneratedApiCrudOperation = (typeof GENERATED_API_CRUD_OPERATIONS)[number];
31
13
 
32
- export type AppApiId = string;
33
- export type AppApiEndpointId = string;
34
- export type AppApiSeedRecord = Readonly<Record<string, DataContractValue>>;
14
+ export type GeneratedApiId = string;
15
+ export type GeneratedApiResourceId = string;
16
+ export type GeneratedApiSeedRecord = Readonly<Record<string, DataContractValue>>;
35
17
 
36
- export interface AppApiAuthRequirement {
18
+ export interface GeneratedApiAuthRequirement {
37
19
  readonly required?: boolean;
38
20
  readonly roles?: readonly string[];
39
21
  readonly permissions?: readonly string[];
@@ -41,65 +23,40 @@ export interface AppApiAuthRequirement {
41
23
  readonly metadata?: DataContractValue;
42
24
  }
43
25
 
44
- export interface AppApiCollectionResourceDefinition {
45
- readonly kind: 'collection';
46
- readonly collection: DbCollectionDefinition;
47
- readonly seed?: readonly AppApiSeedRecord[];
48
- readonly metadata?: DataContractValue;
26
+ export interface GeneratedApiOperationPolicyRef {
27
+ readonly id: string;
28
+ readonly operation?: GeneratedApiCrudOperation;
49
29
  }
50
30
 
51
- export type AppApiResourceDefinition = AppApiCollectionResourceDefinition;
52
-
53
- export interface AppApiEndpointDefinition {
54
- readonly id: AppApiEndpointId;
55
- readonly label?: string;
31
+ export interface GeneratedApiResourceDefinition {
32
+ readonly id: GeneratedApiResourceId;
33
+ readonly name?: string;
56
34
  readonly description?: string;
57
- readonly method: AppApiEndpointMethod;
58
- /**
59
- * Path relative to the API base path. Use `/` for the base collection endpoint.
60
- */
61
35
  readonly path: string;
62
- readonly intent?: AppApiEndpointIntent;
63
- readonly request?: DataOperationRequest;
64
- readonly response?: DataOperationResponse;
65
- readonly auth?: AppApiAuthRequirement;
36
+ readonly collection: DbCollectionDefinition;
37
+ readonly operations: readonly GeneratedApiCrudOperation[];
38
+ readonly seed?: readonly GeneratedApiSeedRecord[];
39
+ readonly policies?: readonly GeneratedApiOperationPolicyRef[];
66
40
  readonly metadata?: DataContractValue;
67
41
  }
68
42
 
69
- interface AppApiDefinitionBase {
70
- readonly id: AppApiId;
71
- readonly kind: AppApiKind;
72
- readonly label?: string;
43
+ /**
44
+ * Canonical desired state for an API authored and generated by Ankhorage.
45
+ *
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.
49
+ */
50
+ export interface GeneratedApiDefinition {
51
+ readonly id: GeneratedApiId;
52
+ readonly protocol: 'rest';
53
+ readonly name?: string;
73
54
  readonly description?: string;
74
55
  readonly basePath: string;
75
- readonly endpoints: readonly AppApiEndpointDefinition[];
76
- readonly auth?: AppApiAuthRequirement;
56
+ readonly database: DatabaseAdapterRef;
57
+ readonly resources: readonly GeneratedApiResourceDefinition[];
58
+ readonly auth?: GeneratedApiAuthRequirement;
77
59
  readonly metadata?: DataContractValue;
78
60
  }
79
61
 
80
- export interface AppGeneratedApiDefinition extends AppApiDefinitionBase {
81
- readonly kind: 'generated';
82
- /**
83
- * Preset used by authoring tools to seed standard endpoints. The resulting
84
- * serialized API still stores concrete `endpoints[]` so custom endpoints can
85
- * live next to generated CRUD operations.
86
- */
87
- readonly preset?: AppApiGeneratedPreset;
88
- readonly resource?: AppApiResourceDefinition;
89
- }
90
-
91
- export interface AppExternalApiDefinition extends AppApiDefinitionBase {
92
- readonly kind: 'external';
93
- readonly baseUrl?: string;
94
- readonly openApiUrl?: string;
95
- }
96
-
97
- export type AppApiDefinition = AppExternalApiDefinition | AppGeneratedApiDefinition;
98
-
99
- export type AppApiRegistry = Readonly<Record<AppApiId, AppApiDefinition>>;
100
-
101
- export interface AppDataManifest {
102
- readonly apis?: AppApiRegistry;
103
- }
104
-
105
- export type AppApiManifest = AppDataManifest;
62
+ export type GeneratedApiRegistry = Readonly<Record<GeneratedApiId, GeneratedApiDefinition>>;