@ankhorage/contracts 1.15.0 → 1.16.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @ankhorage/contracts
2
2
 
3
+ ## 1.16.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 5105c41: Add API authoring contracts for generated and external APIs with explicit endpoints, generated CRUD presets, and collection-backed resources.
8
+
3
9
  ## 1.15.0
4
10
 
5
11
  ### Minor Changes
@@ -0,0 +1,76 @@
1
+ import type { DbCollectionDefinition } from '../db';
2
+ import type { DataOperationRequest, DataOperationResponse } from './operations';
3
+ import type { DataContractValue } from './values';
4
+ export declare const APP_API_KINDS: readonly ["external", "generated"];
5
+ export type AppApiKind = (typeof APP_API_KINDS)[number];
6
+ export declare const APP_API_GENERATED_PRESETS: readonly ["crud"];
7
+ export type AppApiGeneratedPreset = (typeof APP_API_GENERATED_PRESETS)[number];
8
+ export declare const APP_API_ENDPOINT_METHODS: readonly ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"];
9
+ export type AppApiEndpointMethod = (typeof APP_API_ENDPOINT_METHODS)[number];
10
+ export declare const APP_API_ENDPOINT_INTENTS: readonly ["create", "custom", "delete", "list", "read", "update"];
11
+ export type AppApiEndpointIntent = (typeof APP_API_ENDPOINT_INTENTS)[number];
12
+ export type AppApiId = string;
13
+ export type AppApiEndpointId = string;
14
+ export type AppApiSeedRecord = Readonly<Record<string, DataContractValue>>;
15
+ export interface AppApiAuthRequirement {
16
+ readonly required?: boolean;
17
+ readonly roles?: readonly string[];
18
+ readonly permissions?: readonly string[];
19
+ readonly policy?: string;
20
+ readonly metadata?: DataContractValue;
21
+ }
22
+ export interface AppApiCollectionResourceDefinition {
23
+ readonly kind: 'collection';
24
+ readonly collection: DbCollectionDefinition;
25
+ readonly seed?: readonly AppApiSeedRecord[];
26
+ readonly metadata?: DataContractValue;
27
+ }
28
+ export type AppApiResourceDefinition = AppApiCollectionResourceDefinition;
29
+ export interface AppApiEndpointDefinition {
30
+ readonly id: AppApiEndpointId;
31
+ readonly label?: string;
32
+ readonly description?: string;
33
+ readonly method: AppApiEndpointMethod;
34
+ /**
35
+ * Path relative to the API base path. Use `/` for the base collection endpoint.
36
+ */
37
+ readonly path: string;
38
+ readonly intent?: AppApiEndpointIntent;
39
+ readonly request?: DataOperationRequest;
40
+ readonly response?: DataOperationResponse;
41
+ readonly auth?: AppApiAuthRequirement;
42
+ readonly metadata?: DataContractValue;
43
+ }
44
+ interface AppApiDefinitionBase {
45
+ readonly id: AppApiId;
46
+ readonly kind: AppApiKind;
47
+ readonly label?: string;
48
+ readonly description?: string;
49
+ readonly basePath: string;
50
+ readonly endpoints: readonly AppApiEndpointDefinition[];
51
+ readonly auth?: AppApiAuthRequirement;
52
+ readonly metadata?: DataContractValue;
53
+ }
54
+ export interface AppGeneratedApiDefinition extends AppApiDefinitionBase {
55
+ readonly kind: 'generated';
56
+ /**
57
+ * Preset used by authoring tools to seed standard endpoints. The resulting
58
+ * serialized API still stores concrete `endpoints[]` so custom endpoints can
59
+ * live next to generated CRUD operations.
60
+ */
61
+ readonly preset?: AppApiGeneratedPreset;
62
+ readonly resource?: AppApiResourceDefinition;
63
+ }
64
+ export interface AppExternalApiDefinition extends AppApiDefinitionBase {
65
+ readonly kind: 'external';
66
+ readonly baseUrl?: string;
67
+ readonly openApiUrl?: string;
68
+ }
69
+ export type AppApiDefinition = AppExternalApiDefinition | AppGeneratedApiDefinition;
70
+ export type AppApiRegistry = Readonly<Record<AppApiId, AppApiDefinition>>;
71
+ export interface AppDataManifest {
72
+ readonly apis?: AppApiRegistry;
73
+ }
74
+ export type AppApiManifest = AppDataManifest;
75
+ export {};
76
+ //# sourceMappingURL=apis.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apis.d.ts","sourceRoot":"","sources":["../../src/data/apis.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ export const APP_API_KINDS = ['external', 'generated'];
2
+ export const APP_API_GENERATED_PRESETS = ['crud'];
3
+ export const APP_API_ENDPOINT_METHODS = [
4
+ 'DELETE',
5
+ 'GET',
6
+ 'HEAD',
7
+ 'OPTIONS',
8
+ 'PATCH',
9
+ 'POST',
10
+ 'PUT',
11
+ ];
12
+ export const APP_API_ENDPOINT_INTENTS = [
13
+ 'create',
14
+ 'custom',
15
+ 'delete',
16
+ 'list',
17
+ 'read',
18
+ 'update',
19
+ ];
20
+ //# sourceMappingURL=apis.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apis.js","sourceRoot":"","sources":["../../src/data/apis.ts"],"names":[],"mappings":"AAIA,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,UAAU,EAAE,WAAW,CAAU,CAAC;AAGhE,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,MAAM,CAAU,CAAC;AAG3D,MAAM,CAAC,MAAM,wBAAwB,GAAG;IACtC,QAAQ;IACR,KAAK;IACL,MAAM;IACN,SAAS;IACT,OAAO;IACP,MAAM;IACN,KAAK;CACG,CAAC;AAGX,MAAM,CAAC,MAAM,wBAAwB,GAAG;IACtC,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,MAAM;IACN,MAAM;IACN,QAAQ;CACA,CAAC","sourcesContent":["import type { DbCollectionDefinition } from '../db';\nimport type { DataOperationRequest, DataOperationResponse } from './operations';\nimport type { DataContractValue } from './values';\n\nexport const APP_API_KINDS = ['external', 'generated'] as const;\nexport type AppApiKind = (typeof APP_API_KINDS)[number];\n\nexport const APP_API_GENERATED_PRESETS = ['crud'] as const;\nexport type AppApiGeneratedPreset = (typeof APP_API_GENERATED_PRESETS)[number];\n\nexport const APP_API_ENDPOINT_METHODS = [\n 'DELETE',\n 'GET',\n 'HEAD',\n 'OPTIONS',\n 'PATCH',\n 'POST',\n 'PUT',\n] as const;\nexport type AppApiEndpointMethod = (typeof APP_API_ENDPOINT_METHODS)[number];\n\nexport const APP_API_ENDPOINT_INTENTS = [\n 'create',\n 'custom',\n 'delete',\n 'list',\n 'read',\n 'update',\n] as const;\nexport type AppApiEndpointIntent = (typeof APP_API_ENDPOINT_INTENTS)[number];\n\nexport type AppApiId = string;\nexport type AppApiEndpointId = string;\nexport type AppApiSeedRecord = Readonly<Record<string, DataContractValue>>;\n\nexport interface AppApiAuthRequirement {\n readonly required?: boolean;\n readonly roles?: readonly string[];\n readonly permissions?: readonly string[];\n readonly policy?: string;\n readonly metadata?: DataContractValue;\n}\n\nexport interface AppApiCollectionResourceDefinition {\n readonly kind: 'collection';\n readonly collection: DbCollectionDefinition;\n readonly seed?: readonly AppApiSeedRecord[];\n readonly metadata?: DataContractValue;\n}\n\nexport type AppApiResourceDefinition = AppApiCollectionResourceDefinition;\n\nexport interface AppApiEndpointDefinition {\n readonly id: AppApiEndpointId;\n readonly label?: string;\n readonly description?: string;\n readonly method: AppApiEndpointMethod;\n /**\n * Path relative to the API base path. Use `/` for the base collection endpoint.\n */\n readonly path: string;\n readonly intent?: AppApiEndpointIntent;\n readonly request?: DataOperationRequest;\n readonly response?: DataOperationResponse;\n readonly auth?: AppApiAuthRequirement;\n readonly metadata?: DataContractValue;\n}\n\ninterface AppApiDefinitionBase {\n readonly id: AppApiId;\n readonly kind: AppApiKind;\n readonly label?: string;\n readonly description?: string;\n readonly basePath: string;\n readonly endpoints: readonly AppApiEndpointDefinition[];\n readonly auth?: AppApiAuthRequirement;\n readonly metadata?: DataContractValue;\n}\n\nexport interface AppGeneratedApiDefinition extends AppApiDefinitionBase {\n readonly kind: 'generated';\n /**\n * Preset used by authoring tools to seed standard endpoints. The resulting\n * serialized API still stores concrete `endpoints[]` so custom endpoints can\n * live next to generated CRUD operations.\n */\n readonly preset?: AppApiGeneratedPreset;\n readonly resource?: AppApiResourceDefinition;\n}\n\nexport interface AppExternalApiDefinition extends AppApiDefinitionBase {\n readonly kind: 'external';\n readonly baseUrl?: string;\n readonly openApiUrl?: string;\n}\n\nexport type AppApiDefinition = AppExternalApiDefinition | AppGeneratedApiDefinition;\n\nexport type AppApiRegistry = Readonly<Record<AppApiId, AppApiDefinition>>;\n\nexport interface AppDataManifest {\n readonly apis?: AppApiRegistry;\n}\n\nexport type AppApiManifest = AppDataManifest;\n"]}
@@ -1,4 +1,4 @@
1
- export * from './datasets';
1
+ export * from './apis';
2
2
  export * from './diagnostics';
3
3
  export * from './endpoints';
4
4
  export * from './ids';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/data/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,OAAO,CAAC;AACtB,cAAc,cAAc,CAAC;AAC7B,cAAc,QAAQ,CAAC;AACvB,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/data/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,OAAO,CAAC;AACtB,cAAc,cAAc,CAAC;AAC7B,cAAc,QAAQ,CAAC;AACvB,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC"}
@@ -1,4 +1,4 @@
1
- export * from './datasets';
1
+ export * from './apis';
2
2
  export * from './diagnostics';
3
3
  export * from './endpoints';
4
4
  export * from './ids';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/data/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,OAAO,CAAC;AACtB,cAAc,cAAc,CAAC;AAC7B,cAAc,QAAQ,CAAC;AACvB,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC","sourcesContent":["export * from './datasets';\nexport * from './diagnostics';\nexport * from './endpoints';\nexport * from './ids';\nexport * from './operations';\nexport * from './refs';\nexport * from './schemas';\nexport * from './sources';\nexport * from './values';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/data/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,OAAO,CAAC;AACtB,cAAc,cAAc,CAAC;AAC7B,cAAc,QAAQ,CAAC;AACvB,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC","sourcesContent":["export * from './apis';\nexport * from './diagnostics';\nexport * from './endpoints';\nexport * from './ids';\nexport * from './operations';\nexport * from './refs';\nexport * from './schemas';\nexport * from './sources';\nexport * from './values';\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ankhorage/contracts",
3
- "version": "1.15.0",
3
+ "version": "1.16.0",
4
4
  "main": "./dist/index.js",
5
5
  "dependencies": {
6
6
  "@ankhorage/color-theory": "^0.0.7"
@@ -6,11 +6,7 @@ import { describe, expect, it } from 'bun:test';
6
6
 
7
7
  import {
8
8
  APP_CATEGORIES,
9
- APP_DATASET_OPERATIONS,
10
9
  type AppCategory,
11
- type AppDataManifest,
12
- type AppDatasetDefinition,
13
- type AppDatasetOperation,
14
10
  type AppManifest,
15
11
  AUTH_PROVIDERS,
16
12
  AUTH_SIGN_IN_IDENTIFIERS,
@@ -24,7 +20,6 @@ import {
24
20
  type DbAdapter,
25
21
  type DbAdminAdapter,
26
22
  type DbChangeEvent,
27
- type DbCollectionDefinition,
28
23
  type DbRealtimeAdapter,
29
24
  DEPLOYMENT_TARGETS,
30
25
  type FormSubmitEventDto,
@@ -136,50 +131,6 @@ describe('contracts', () => {
136
131
  expect(JSON.parse(JSON.stringify(manifest))).toEqual({ splashScreen });
137
132
  });
138
133
 
139
- it('accepts app-owned dataset manifests backed by db collection definitions', () => {
140
- const operation: AppDatasetOperation = 'list';
141
- const collection: DbCollectionDefinition = {
142
- name: 'poker_situations',
143
- primaryKey: 'id',
144
- fields: [
145
- { name: 'title', type: 'text', required: true },
146
- { name: 'description', type: 'text', required: true },
147
- { name: 'difficulty', type: 'text' },
148
- { name: 'availableActions', type: 'json' },
149
- { name: 'correctAction', type: 'text' },
150
- ],
151
- };
152
- const dataset: AppDatasetDefinition = {
153
- id: 'poker_situations',
154
- label: 'Poker situations',
155
- description: 'App-owned poker trainer situations.',
156
- collection,
157
- operations: [operation, 'read'],
158
- seed: [
159
- {
160
- title: 'Button faces a raise',
161
- description: 'Choose the best action with position and stack depth in mind.',
162
- difficulty: 'beginner',
163
- availableActions: [
164
- { label: 'Fold', value: 'fold' },
165
- { label: 'Call', value: 'call' },
166
- { label: 'Raise', value: 'raise' },
167
- ],
168
- correctAction: 'raise',
169
- },
170
- ],
171
- };
172
- const data: AppDataManifest = {
173
- datasets: {
174
- [dataset.id]: dataset,
175
- },
176
- };
177
- const manifest: Pick<AppManifest, 'data'> = { data };
178
-
179
- expect(APP_DATASET_OPERATIONS).toEqual(['create', 'delete', 'list', 'read', 'update']);
180
- expect(JSON.parse(JSON.stringify(manifest))).toEqual({ data });
181
- });
182
-
183
134
  it('accepts provider-neutral state infra selection on app manifests', () => {
184
135
  const state: StateSpec = {
185
136
  provider: 'legend',
@@ -0,0 +1,146 @@
1
+ import { describe, expect, test } from 'bun:test';
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';
10
+
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[];
43
+
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',
75
+ collection: {
76
+ name: 'players',
77
+ primaryKey: 'id',
78
+ fields: [
79
+ { name: 'id', type: 'uuid', required: true },
80
+ { name: 'name', type: 'text', required: true },
81
+ { name: 'score', type: 'number' },
82
+ ],
83
+ },
84
+ seed: [
85
+ {
86
+ id: 'player-1',
87
+ name: 'Ada',
88
+ score: 42,
89
+ },
90
+ ],
91
+ },
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
+ };
107
+
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
+ ]);
121
+ });
122
+
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
+ };
143
+
144
+ expect(JSON.parse(JSON.stringify(externalApi))).toEqual(externalApi);
145
+ });
146
+ });
@@ -0,0 +1,105 @@
1
+ import type { DbCollectionDefinition } from '../db';
2
+ import type { DataOperationRequest, DataOperationResponse } from './operations';
3
+ import type { DataContractValue } from './values';
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',
26
+ 'list',
27
+ 'read',
28
+ 'update',
29
+ ] as const;
30
+ export type AppApiEndpointIntent = (typeof APP_API_ENDPOINT_INTENTS)[number];
31
+
32
+ export type AppApiId = string;
33
+ export type AppApiEndpointId = string;
34
+ export type AppApiSeedRecord = Readonly<Record<string, DataContractValue>>;
35
+
36
+ export interface AppApiAuthRequirement {
37
+ readonly required?: boolean;
38
+ readonly roles?: readonly string[];
39
+ readonly permissions?: readonly string[];
40
+ readonly policy?: string;
41
+ readonly metadata?: DataContractValue;
42
+ }
43
+
44
+ export interface AppApiCollectionResourceDefinition {
45
+ readonly kind: 'collection';
46
+ readonly collection: DbCollectionDefinition;
47
+ readonly seed?: readonly AppApiSeedRecord[];
48
+ readonly metadata?: DataContractValue;
49
+ }
50
+
51
+ export type AppApiResourceDefinition = AppApiCollectionResourceDefinition;
52
+
53
+ export interface AppApiEndpointDefinition {
54
+ readonly id: AppApiEndpointId;
55
+ readonly label?: string;
56
+ readonly description?: string;
57
+ readonly method: AppApiEndpointMethod;
58
+ /**
59
+ * Path relative to the API base path. Use `/` for the base collection endpoint.
60
+ */
61
+ readonly path: string;
62
+ readonly intent?: AppApiEndpointIntent;
63
+ readonly request?: DataOperationRequest;
64
+ readonly response?: DataOperationResponse;
65
+ readonly auth?: AppApiAuthRequirement;
66
+ readonly metadata?: DataContractValue;
67
+ }
68
+
69
+ interface AppApiDefinitionBase {
70
+ readonly id: AppApiId;
71
+ readonly kind: AppApiKind;
72
+ readonly label?: string;
73
+ readonly description?: string;
74
+ readonly basePath: string;
75
+ readonly endpoints: readonly AppApiEndpointDefinition[];
76
+ readonly auth?: AppApiAuthRequirement;
77
+ readonly metadata?: DataContractValue;
78
+ }
79
+
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;
package/src/data/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export * from './datasets';
1
+ export * from './apis';
2
2
  export * from './diagnostics';
3
3
  export * from './endpoints';
4
4
  export * from './ids';
@@ -1,20 +0,0 @@
1
- import type { DbCollectionDefinition } from '../db';
2
- import type { DataContractValue } from './values';
3
- export declare const APP_DATASET_OPERATIONS: readonly ["create", "delete", "list", "read", "update"];
4
- export type AppDatasetOperation = (typeof APP_DATASET_OPERATIONS)[number];
5
- export type AppDatasetId = string;
6
- export type AppDatasetSeedRecord = Readonly<Record<string, DataContractValue>>;
7
- export interface AppDatasetDefinition {
8
- readonly id: AppDatasetId;
9
- readonly label?: string;
10
- readonly description?: string;
11
- readonly collection: DbCollectionDefinition;
12
- readonly operations?: readonly AppDatasetOperation[];
13
- readonly seed?: readonly AppDatasetSeedRecord[];
14
- readonly metadata?: DataContractValue;
15
- }
16
- export type AppDatasetRegistry = Readonly<Record<AppDatasetId, AppDatasetDefinition>>;
17
- export interface AppDataManifest {
18
- readonly datasets?: AppDatasetRegistry;
19
- }
20
- //# sourceMappingURL=datasets.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"datasets.d.ts","sourceRoot":"","sources":["../../src/data/datasets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,OAAO,CAAC;AACpD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAElD,eAAO,MAAM,sBAAsB,yDAA0D,CAAC;AAC9F,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE1E,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC;AAClC,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAAC;AAE/E,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,EAAE,EAAE,YAAY,CAAC;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,sBAAsB,CAAC;IAC5C,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACrD,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,oBAAoB,EAAE,CAAC;IAChD,QAAQ,CAAC,QAAQ,CAAC,EAAE,iBAAiB,CAAC;CACvC;AAED,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC,MAAM,CAAC,YAAY,EAAE,oBAAoB,CAAC,CAAC,CAAC;AAEtF,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,CAAC,EAAE,kBAAkB,CAAC;CACxC"}
@@ -1,2 +0,0 @@
1
- export const APP_DATASET_OPERATIONS = ['create', 'delete', 'list', 'read', 'update'];
2
- //# sourceMappingURL=datasets.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"datasets.js","sourceRoot":"","sources":["../../src/data/datasets.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAU,CAAC","sourcesContent":["import type { DbCollectionDefinition } from '../db';\nimport type { DataContractValue } from './values';\n\nexport const APP_DATASET_OPERATIONS = ['create', 'delete', 'list', 'read', 'update'] as const;\nexport type AppDatasetOperation = (typeof APP_DATASET_OPERATIONS)[number];\n\nexport type AppDatasetId = string;\nexport type AppDatasetSeedRecord = Readonly<Record<string, DataContractValue>>;\n\nexport interface AppDatasetDefinition {\n readonly id: AppDatasetId;\n readonly label?: string;\n readonly description?: string;\n readonly collection: DbCollectionDefinition;\n readonly operations?: readonly AppDatasetOperation[];\n readonly seed?: readonly AppDatasetSeedRecord[];\n readonly metadata?: DataContractValue;\n}\n\nexport type AppDatasetRegistry = Readonly<Record<AppDatasetId, AppDatasetDefinition>>;\n\nexport interface AppDataManifest {\n readonly datasets?: AppDatasetRegistry;\n}\n"]}
@@ -1,24 +0,0 @@
1
- import type { DbCollectionDefinition } from '../db';
2
- import type { DataContractValue } from './values';
3
-
4
- export const APP_DATASET_OPERATIONS = ['create', 'delete', 'list', 'read', 'update'] as const;
5
- export type AppDatasetOperation = (typeof APP_DATASET_OPERATIONS)[number];
6
-
7
- export type AppDatasetId = string;
8
- export type AppDatasetSeedRecord = Readonly<Record<string, DataContractValue>>;
9
-
10
- export interface AppDatasetDefinition {
11
- readonly id: AppDatasetId;
12
- readonly label?: string;
13
- readonly description?: string;
14
- readonly collection: DbCollectionDefinition;
15
- readonly operations?: readonly AppDatasetOperation[];
16
- readonly seed?: readonly AppDatasetSeedRecord[];
17
- readonly metadata?: DataContractValue;
18
- }
19
-
20
- export type AppDatasetRegistry = Readonly<Record<AppDatasetId, AppDatasetDefinition>>;
21
-
22
- export interface AppDataManifest {
23
- readonly datasets?: AppDatasetRegistry;
24
- }