@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/CHANGELOG.md +6 -0
- package/README.md +1 -1
- package/dist/bindings.d.ts +1 -8
- package/dist/bindings.d.ts.map +1 -1
- package/dist/bindings.js.map +1 -1
- package/dist/data/apis.d.ts +32 -58
- package/dist/data/apis.d.ts.map +1 -1
- package/dist/data/apis.js +3 -15
- package/dist/data/apis.js.map +1 -1
- package/dist/data/refs.d.ts +3 -0
- package/dist/data/refs.d.ts.map +1 -1
- package/dist/data/refs.js.map +1 -1
- package/dist/data/sources.d.ts +26 -26
- package/dist/data/sources.d.ts.map +1 -1
- package/dist/data/sources.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
- package/src/bindings.test.ts +19 -29
- package/src/bindings.ts +1 -11
- package/src/data/apis.test.ts +52 -129
- package/src/data/apis.ts +34 -77
- package/src/data/data.test.ts +75 -190
- package/src/data/refs.ts +4 -0
- package/src/data/sources.ts +30 -32
- package/src/types.ts +2 -2
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
|
|
package/src/data/apis.test.ts
CHANGED
|
@@ -1,146 +1,69 @@
|
|
|
1
|
-
import { describe, expect,
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
2
|
|
|
3
|
-
import type {
|
|
4
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
'
|
|
53
|
-
'
|
|
54
|
-
'
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
'
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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: '
|
|
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: '
|
|
38
|
+
{ name: 'price', type: 'number', required: true },
|
|
82
39
|
],
|
|
83
40
|
},
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
expect(
|
|
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
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
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
|
-
|
|
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 {
|
|
2
|
+
import type { DatabaseAdapterRef } from './refs';
|
|
3
3
|
import type { DataContractValue } from './values';
|
|
4
4
|
|
|
5
|
-
export 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
|
|
12
|
+
export type GeneratedApiCrudOperation = (typeof GENERATED_API_CRUD_OPERATIONS)[number];
|
|
31
13
|
|
|
32
|
-
export type
|
|
33
|
-
export type
|
|
34
|
-
export type
|
|
14
|
+
export type GeneratedApiId = string;
|
|
15
|
+
export type GeneratedApiResourceId = string;
|
|
16
|
+
export type GeneratedApiSeedRecord = Readonly<Record<string, DataContractValue>>;
|
|
35
17
|
|
|
36
|
-
export interface
|
|
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
|
|
45
|
-
readonly
|
|
46
|
-
readonly
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
63
|
-
readonly
|
|
64
|
-
readonly
|
|
65
|
-
readonly
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
76
|
-
readonly
|
|
56
|
+
readonly database: DatabaseAdapterRef;
|
|
57
|
+
readonly resources: readonly GeneratedApiResourceDefinition[];
|
|
58
|
+
readonly auth?: GeneratedApiAuthRequirement;
|
|
77
59
|
readonly metadata?: DataContractValue;
|
|
78
60
|
}
|
|
79
61
|
|
|
80
|
-
export
|
|
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>>;
|