@ankhorage/contracts 4.0.1 → 5.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.
- package/CHANGELOG.md +11 -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 +10 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/bindings.test.ts +19 -29
- package/src/bindings.ts +1 -11
- package/src/contracts.test.ts +24 -1
- 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 +10 -3
package/src/bindings.test.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'bun:test';
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
|
-
ApiScreenDataLoaderDefinition,
|
|
5
4
|
AppManifest,
|
|
6
5
|
BindingInputMap,
|
|
7
6
|
BindingValueSource,
|
|
@@ -161,37 +160,28 @@ describe('component data-binding contracts', () => {
|
|
|
161
160
|
expect(binding.target.kind).toBe('action');
|
|
162
161
|
});
|
|
163
162
|
|
|
164
|
-
it('serializes
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
{
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
operationId: 'nutrition.products.getById',
|
|
180
|
-
},
|
|
181
|
-
input: {
|
|
182
|
-
id: {
|
|
183
|
-
kind: 'source',
|
|
184
|
-
source: {
|
|
185
|
-
kind: 'context',
|
|
186
|
-
path: 'route.params.id',
|
|
187
|
-
},
|
|
163
|
+
it('serializes canonical operation screen data loaders', () => {
|
|
164
|
+
const loader: ScreenDataLoaderDefinition = {
|
|
165
|
+
kind: 'operation',
|
|
166
|
+
id: 'product-detail',
|
|
167
|
+
operation: {
|
|
168
|
+
dataSourceId: 'nutrition-api',
|
|
169
|
+
endpointId: 'products',
|
|
170
|
+
operationId: 'nutrition.products.getById',
|
|
171
|
+
},
|
|
172
|
+
input: {
|
|
173
|
+
id: {
|
|
174
|
+
kind: 'source',
|
|
175
|
+
source: {
|
|
176
|
+
kind: 'context',
|
|
177
|
+
path: 'route.params.id',
|
|
188
178
|
},
|
|
189
179
|
},
|
|
190
|
-
}
|
|
191
|
-
|
|
180
|
+
},
|
|
181
|
+
} satisfies OperationScreenDataLoaderDefinition;
|
|
192
182
|
|
|
193
|
-
assertSerializable(
|
|
194
|
-
expect(
|
|
183
|
+
assertSerializable(loader);
|
|
184
|
+
expect(loader.kind).toBe('operation');
|
|
195
185
|
});
|
|
196
186
|
|
|
197
187
|
it('serializes scanner lookup and conditional navigation intent using existing generic bindings', () => {
|
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/contracts.test.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type FormSubmitEventDto,
|
|
27
27
|
type ImageAssetSource,
|
|
28
28
|
NAVIGATOR_TYPES,
|
|
29
|
+
type RouteDefinition,
|
|
29
30
|
type SplashScreenSpec,
|
|
30
31
|
STATE_PERSISTENCE_MODES,
|
|
31
32
|
STATE_PROVIDERS,
|
|
@@ -115,6 +116,27 @@ describe('contracts', () => {
|
|
|
115
116
|
expect(category).toBe('developer_tools');
|
|
116
117
|
});
|
|
117
118
|
|
|
119
|
+
it('serializes canonical primary-navigation visibility on routes', () => {
|
|
120
|
+
const visibleRoute: RouteDefinition = {
|
|
121
|
+
name: 'home',
|
|
122
|
+
path: '/',
|
|
123
|
+
screenId: 'home-screen',
|
|
124
|
+
};
|
|
125
|
+
const hiddenRoute: RouteDefinition = {
|
|
126
|
+
name: 'settings',
|
|
127
|
+
path: '/settings',
|
|
128
|
+
screenId: 'settings-screen',
|
|
129
|
+
showInPrimaryNavigation: false,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
expect(JSON.parse(JSON.stringify({ visibleRoute, hiddenRoute }))).toEqual({
|
|
133
|
+
visibleRoute,
|
|
134
|
+
hiddenRoute,
|
|
135
|
+
});
|
|
136
|
+
expect(visibleRoute.showInPrimaryNavigation).toBeUndefined();
|
|
137
|
+
expect(hiddenRoute.showInPrimaryNavigation).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
|
|
118
140
|
it('accepts the current serialized theme config shape', () => {
|
|
119
141
|
const theme: ThemeConfig = {
|
|
120
142
|
id: 'theme-default',
|
|
@@ -253,7 +275,7 @@ describe('contracts', () => {
|
|
|
253
275
|
expect(names.includes('color-theory.ts')).toBe(false);
|
|
254
276
|
});
|
|
255
277
|
|
|
256
|
-
it('removes
|
|
278
|
+
it('removes obsolete contract symbols from src recursively', async () => {
|
|
257
279
|
const banned = [
|
|
258
280
|
'Color' + 'Tone',
|
|
259
281
|
'color' + 'Tone',
|
|
@@ -263,6 +285,7 @@ describe('contracts', () => {
|
|
|
263
285
|
'APP_' + 'MOODS',
|
|
264
286
|
'suggested' + 'Color' + 'Tone',
|
|
265
287
|
'APP_CATEGORY_' + 'THEME_RECOMMENDATIONS',
|
|
288
|
+
'hide' + 'InTabBar',
|
|
266
289
|
];
|
|
267
290
|
|
|
268
291
|
const srcFiles = await collectTypeScriptFiles(join(process.cwd(), 'src'));
|
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>>;
|