@ankhorage/contracts 21.0.0 → 22.0.1
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 +12 -0
- package/README.md +3 -1
- package/dist/appManifest/apis.d.ts +3 -3
- package/dist/appManifest/apis.d.ts.map +1 -1
- package/dist/appManifest/apis.js +4 -6
- package/dist/appManifest/apis.js.map +1 -1
- package/dist/appManifest/bindings.d.ts.map +1 -1
- package/dist/appManifest/bindings.js +2 -1
- package/dist/appManifest/bindings.js.map +1 -1
- package/dist/appManifest/data.d.ts.map +1 -1
- package/dist/appManifest/data.js +5 -4
- package/dist/appManifest/data.js.map +1 -1
- package/dist/appManifest/isAppManifest.js +3 -3
- package/dist/appManifest/isAppManifest.js.map +1 -1
- package/dist/appManifest/isManifestValue.d.ts +1 -1
- package/dist/appManifest/isManifestValue.d.ts.map +1 -1
- package/dist/appManifest/isManifestValue.js +3 -12
- package/dist/appManifest/isManifestValue.js.map +1 -1
- package/dist/appManifest/screens.d.ts +3 -2
- package/dist/appManifest/screens.d.ts.map +1 -1
- package/dist/appManifest/screens.js +15 -7
- package/dist/appManifest/screens.js.map +1 -1
- package/dist/appManifest/splashScreen.d.ts +8 -11
- package/dist/appManifest/splashScreen.d.ts.map +1 -1
- package/dist/appManifest/splashScreen.js.map +1 -1
- package/dist/bindings.d.ts +2 -3
- package/dist/bindings.d.ts.map +1 -1
- package/dist/bindings.js.map +1 -1
- package/dist/collections.d.ts +4 -0
- package/dist/collections.d.ts.map +1 -0
- package/dist/collections.js +6 -0
- package/dist/collections.js.map +1 -0
- package/dist/data/apis.d.ts +1 -1
- package/dist/data/apis.d.ts.map +1 -1
- package/dist/data/apis.js.map +1 -1
- package/dist/data/values.d.ts +2 -3
- package/dist/data/values.d.ts.map +1 -1
- package/dist/data/values.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/infra/isInfraEnvironmentSpec.d.ts.map +1 -1
- package/dist/infra/isInfraEnvironmentSpec.js +10 -12
- package/dist/infra/isInfraEnvironmentSpec.js.map +1 -1
- package/dist/infra/isInfraManifest.d.ts.map +1 -1
- package/dist/infra/isInfraManifest.js +14 -5
- package/dist/infra/isInfraManifest.js.map +1 -1
- package/dist/infra/isInfraWorkloadSpec.d.ts.map +1 -1
- package/dist/infra/isInfraWorkloadSpec.js +26 -20
- package/dist/infra/isInfraWorkloadSpec.js.map +1 -1
- package/dist/requirements.d.ts +5 -10
- package/dist/requirements.d.ts.map +1 -1
- package/dist/requirements.js.map +1 -1
- package/dist/serializable.d.ts +6 -0
- package/dist/serializable.d.ts.map +1 -0
- package/dist/serializable.js +14 -0
- package/dist/serializable.js.map +1 -0
- package/dist/state.d.ts +2 -3
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js.map +1 -1
- package/dist/types/infraManifest.d.ts +14 -13
- package/dist/types/infraManifest.d.ts.map +1 -1
- package/dist/types/infraManifest.js.map +1 -1
- package/dist/types/infraWorkload.d.ts +11 -11
- package/dist/types/infraWorkload.d.ts.map +1 -1
- package/dist/types/infraWorkload.js.map +1 -1
- package/dist/types.d.ts +9 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -3
- package/src/appManifest/apis.ts +9 -6
- package/src/appManifest/bindings.ts +7 -1
- package/src/appManifest/data.ts +13 -4
- package/src/appManifest/isAppManifest.ts +3 -3
- package/src/appManifest/isManifestValue.ts +3 -16
- package/src/appManifest/screens.ts +21 -10
- package/src/appManifest/splashScreen.test.ts +3 -3
- package/src/appManifest/splashScreen.ts +8 -18
- package/src/appManifest.test.ts +21 -16
- package/src/bindings.test.ts +11 -8
- package/src/bindings.ts +2 -9
- package/src/collections.ts +8 -0
- package/src/contracts.test.ts +16 -13
- package/src/data/apis.test.ts +6 -4
- package/src/data/apis.ts +1 -1
- package/src/data/values.ts +3 -9
- package/src/deploy.test.ts +4 -4
- package/src/index.ts +2 -0
- package/src/infra/isInfraEnvironmentSpec.ts +16 -16
- package/src/infra/isInfraManifest.ts +17 -6
- package/src/infra/isInfraWorkloadSpec.ts +39 -24
- package/src/infra-published-port.test.ts +8 -8
- package/src/infra-workload-persistence.test.ts +4 -4
- package/src/infra-workload.test.ts +24 -22
- package/src/infra.test.ts +16 -20
- package/src/infraPersistence.test.ts +1 -1
- package/src/infraServices.test.ts +1 -1
- package/src/requirements.test.ts +2 -2
- package/src/requirements.ts +6 -12
- package/src/secrets.test.ts +1 -1
- package/src/serializable.ts +25 -0
- package/src/state.ts +3 -6
- package/src/types/infraManifest.ts +17 -13
- package/src/types/infraWorkload.ts +12 -11
- package/src/types.ts +11 -12
|
@@ -2,8 +2,10 @@ import { COLOR_HARMONIES } from '@ankhorage/color-theory';
|
|
|
2
2
|
import { isRecord } from '@ankhorage/utility/object';
|
|
3
3
|
import { isOptionalString } from '@ankhorage/utility/string';
|
|
4
4
|
|
|
5
|
+
import { isSerializableSet } from '../collections';
|
|
5
6
|
import { isMediaAssetReference } from '../media';
|
|
6
|
-
import {
|
|
7
|
+
import { ANKHORAGE_CAPABILITY_NAMES, ANKHORAGE_PERMISSION_NAMES } from '../requirements';
|
|
8
|
+
import { APP_CATEGORIES, type ThemeRegistry } from '../types';
|
|
7
9
|
import { isBindingValueSource, isScreenDataLoaderDefinition } from './bindings';
|
|
8
10
|
|
|
9
11
|
export { isAppNavigatorManifest } from './navigator';
|
|
@@ -11,6 +13,8 @@ export { isAppNavigatorManifest } from './navigator';
|
|
|
11
13
|
const APP_CATEGORY_SET = new Set<string>(APP_CATEGORIES);
|
|
12
14
|
const COLOR_HARMONY_SET = new Set<string>(COLOR_HARMONIES);
|
|
13
15
|
const SPLASH_SCREEN_RESIZE_MODE_SET = new Set<string>(['contain', 'cover', 'native']);
|
|
16
|
+
const PERMISSION_NAME_SET = new Set<string>(ANKHORAGE_PERMISSION_NAMES);
|
|
17
|
+
const CAPABILITY_NAME_SET = new Set<string>(ANKHORAGE_CAPABILITY_NAMES);
|
|
14
18
|
|
|
15
19
|
/*** Validate application identity and optional authored metadata. */
|
|
16
20
|
export function isManifestMetadata(value: unknown): boolean {
|
|
@@ -28,7 +32,7 @@ export function isManifestMetadata(value: unknown): boolean {
|
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
/*** Validate theme identity and its required light and dark modes. */
|
|
31
|
-
|
|
35
|
+
function isThemeConfig(value: unknown): boolean {
|
|
32
36
|
return (
|
|
33
37
|
isRecord(value) &&
|
|
34
38
|
typeof value.id === 'string' &&
|
|
@@ -38,6 +42,16 @@ export function isThemeConfig(value: unknown): boolean {
|
|
|
38
42
|
);
|
|
39
43
|
}
|
|
40
44
|
|
|
45
|
+
/*** Validate themes in the authored theme registry and require registry keys to match theme identity. */
|
|
46
|
+
export function isThemeRegistry(value: unknown): value is ThemeRegistry {
|
|
47
|
+
return (
|
|
48
|
+
isRecord(value) &&
|
|
49
|
+
Object.entries(value).every(
|
|
50
|
+
([registryKey, theme]) => isThemeConfig(theme) && isRecord(theme) && registryKey === theme.id,
|
|
51
|
+
)
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
41
55
|
/*** Validate screens in the authored screen registry. */
|
|
42
56
|
export function isScreenRegistry(value: unknown): boolean {
|
|
43
57
|
return (
|
|
@@ -127,15 +141,12 @@ function isSplashScreenModeSpec(value: unknown): boolean {
|
|
|
127
141
|
function isScreenRequirements(value: unknown): boolean {
|
|
128
142
|
return (
|
|
129
143
|
isRecord(value) &&
|
|
130
|
-
(value.permissions === undefined ||
|
|
131
|
-
(value.capabilities === undefined ||
|
|
144
|
+
(value.permissions === undefined || isRequirementSet(value.permissions, PERMISSION_NAME_SET)) &&
|
|
145
|
+
(value.capabilities === undefined || isRequirementSet(value.capabilities, CAPABILITY_NAME_SET))
|
|
132
146
|
);
|
|
133
147
|
}
|
|
134
148
|
|
|
135
|
-
/*** Validate
|
|
136
|
-
function
|
|
137
|
-
return (
|
|
138
|
-
Array.isArray(value) &&
|
|
139
|
-
value.every((entry) => isRecord(entry) && typeof entry[key] === 'string')
|
|
140
|
-
);
|
|
149
|
+
/*** Validate unordered requirement membership against the supported capability or permission names. */
|
|
150
|
+
function isRequirementSet(value: unknown, allowedNames: ReadonlySet<string>): boolean {
|
|
151
|
+
return isSerializableSet(value) && Object.keys(value).every((name) => allowedNames.has(name));
|
|
141
152
|
}
|
|
@@ -3,7 +3,7 @@ import { describe, expect, it } from 'bun:test';
|
|
|
3
3
|
import type { AppManifest, SplashScreenSpec } from '../index';
|
|
4
4
|
|
|
5
5
|
describe('AppManifest splash screen contract', () => {
|
|
6
|
-
it('accepts portable media references with
|
|
6
|
+
it('accepts portable media references with the canonical splash option shape', () => {
|
|
7
7
|
const splashScreen: SplashScreenSpec = {
|
|
8
8
|
backgroundColor: '#ffffff',
|
|
9
9
|
image: { mediaId: 'splash-logo' },
|
|
@@ -23,13 +23,13 @@ describe('AppManifest splash screen contract', () => {
|
|
|
23
23
|
it('keeps root-only Expo options out of dark-mode overrides', () => {
|
|
24
24
|
const splashWithDarkImageWidth: SplashScreenSpec = {
|
|
25
25
|
dark: {
|
|
26
|
-
// @ts-expect-error
|
|
26
|
+
// @ts-expect-error Dark-mode overrides do not include image sizing.
|
|
27
27
|
imageWidth: 160,
|
|
28
28
|
},
|
|
29
29
|
};
|
|
30
30
|
const splashWithDarkResizeMode: SplashScreenSpec = {
|
|
31
31
|
dark: {
|
|
32
|
-
// @ts-expect-error
|
|
32
|
+
// @ts-expect-error Dark-mode overrides do not include resize mode.
|
|
33
33
|
resizeMode: 'contain',
|
|
34
34
|
},
|
|
35
35
|
};
|
|
@@ -1,24 +1,14 @@
|
|
|
1
|
-
import type { Props as ExpoSplashScreenPluginProps } from 'expo-splash-screen/plugin';
|
|
2
|
-
|
|
3
1
|
import type { MediaAssetReference } from '../media';
|
|
4
2
|
|
|
5
|
-
type
|
|
6
|
-
ExpoSplashScreenPluginProps,
|
|
7
|
-
'backgroundColor' | 'image' | 'imageWidth' | 'resizeMode'
|
|
8
|
-
>;
|
|
3
|
+
export type SplashScreenResizeMode = 'contain' | 'cover' | 'native';
|
|
9
4
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
> & {
|
|
5
|
+
export interface SplashScreenModeSpec {
|
|
6
|
+
readonly backgroundColor?: string;
|
|
13
7
|
readonly image?: MediaAssetReference;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export type SplashScreenResizeMode = NonNullable<ExpoSplashScreenPluginProps['resizeMode']>;
|
|
17
|
-
|
|
18
|
-
export type SplashScreenModeSpec = PortableSplashScreenProps<
|
|
19
|
-
NonNullable<ExpoSplashScreenPluginProps['dark']>
|
|
20
|
-
>;
|
|
8
|
+
}
|
|
21
9
|
|
|
22
|
-
export
|
|
10
|
+
export interface SplashScreenSpec extends SplashScreenModeSpec {
|
|
11
|
+
readonly imageWidth?: number;
|
|
12
|
+
readonly resizeMode?: SplashScreenResizeMode;
|
|
23
13
|
readonly dark?: SplashScreenModeSpec;
|
|
24
|
-
}
|
|
14
|
+
}
|
package/src/appManifest.test.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readOwnProperty } from '@ankhorage/utility/object';
|
|
1
2
|
import { describe, expect, it } from 'bun:test';
|
|
2
3
|
|
|
3
4
|
import { isAppManifest, parseAppManifest } from './appManifest';
|
|
@@ -11,14 +12,14 @@ function createManifest(): Record<string, unknown> {
|
|
|
11
12
|
category: 'developer_tools',
|
|
12
13
|
themeId: 'default',
|
|
13
14
|
},
|
|
14
|
-
themes:
|
|
15
|
-
{
|
|
15
|
+
themes: {
|
|
16
|
+
default: {
|
|
16
17
|
id: 'default',
|
|
17
18
|
name: 'Default',
|
|
18
19
|
light: { primaryColor: '#3366ff', harmony: 'analogous' },
|
|
19
20
|
dark: { primaryColor: '#6699ff', harmony: 'analogous' },
|
|
20
21
|
},
|
|
21
|
-
|
|
22
|
+
},
|
|
22
23
|
activeThemeId: 'default',
|
|
23
24
|
activeThemeMode: 'light',
|
|
24
25
|
splashScreen: {
|
|
@@ -43,12 +44,12 @@ function createManifest(): Record<string, unknown> {
|
|
|
43
44
|
local: {
|
|
44
45
|
deployment: { compute: { provider: 'local' }, runtime: { provider: 'minikube' } },
|
|
45
46
|
database: { provider: 'supabase', tier: 'dev' },
|
|
46
|
-
objectStorage: { provider: 'supabase', buckets:
|
|
47
|
+
objectStorage: { provider: 'supabase', buckets: { media: true } },
|
|
47
48
|
networking: { domain: 'example.test' },
|
|
48
49
|
},
|
|
49
50
|
},
|
|
50
|
-
apis:
|
|
51
|
-
{
|
|
51
|
+
apis: {
|
|
52
|
+
nutrition: {
|
|
52
53
|
id: 'nutrition',
|
|
53
54
|
origin: 'external',
|
|
54
55
|
protocol: 'rest',
|
|
@@ -81,9 +82,10 @@ function createManifest(): Record<string, unknown> {
|
|
|
81
82
|
},
|
|
82
83
|
},
|
|
83
84
|
},
|
|
84
|
-
|
|
85
|
-
modules:
|
|
86
|
-
|
|
85
|
+
},
|
|
86
|
+
modules: {
|
|
87
|
+
'expo-localization': { config: { defaultLocale: 'en' } },
|
|
88
|
+
},
|
|
87
89
|
},
|
|
88
90
|
navigator: {
|
|
89
91
|
type: 'stack',
|
|
@@ -120,8 +122,8 @@ function createManifest(): Record<string, unknown> {
|
|
|
120
122
|
},
|
|
121
123
|
],
|
|
122
124
|
requires: {
|
|
123
|
-
permissions:
|
|
124
|
-
capabilities:
|
|
125
|
+
permissions: { camera: true },
|
|
126
|
+
capabilities: { barcodeScanner: true, ebookReader: true },
|
|
125
127
|
},
|
|
126
128
|
},
|
|
127
129
|
},
|
|
@@ -199,8 +201,9 @@ describe('AppManifest runtime parsing', () => {
|
|
|
199
201
|
|
|
200
202
|
it('rejects malformed nested canonical structures', () => {
|
|
201
203
|
const manifest = createManifest();
|
|
202
|
-
const themes = manifest.themes as Record<string, unknown
|
|
203
|
-
const
|
|
204
|
+
const themes = manifest.themes as Record<string, unknown>;
|
|
205
|
+
const theme = readOwnProperty<Record<string, unknown>>(themes, 'default');
|
|
206
|
+
const light = theme?.light as Record<string, unknown>;
|
|
204
207
|
light.harmony = 'not-a-harmony';
|
|
205
208
|
|
|
206
209
|
expect(parseAppManifest(manifest)).toEqual({
|
|
@@ -209,11 +212,13 @@ describe('AppManifest runtime parsing', () => {
|
|
|
209
212
|
});
|
|
210
213
|
});
|
|
211
214
|
|
|
212
|
-
it('rejects
|
|
215
|
+
it('rejects API registries whose key disagrees with API id', () => {
|
|
213
216
|
const manifest = createManifest();
|
|
214
217
|
const infra = manifest.infra as Record<string, unknown>;
|
|
215
|
-
const apis = infra.apis as Record<string, unknown
|
|
216
|
-
|
|
218
|
+
const apis = infra.apis as Record<string, unknown>;
|
|
219
|
+
const nutrition = readOwnProperty(apis, 'nutrition');
|
|
220
|
+
apis.catalog = nutrition;
|
|
221
|
+
delete apis.nutrition;
|
|
217
222
|
|
|
218
223
|
expect(isAppManifest(manifest)).toBe(false);
|
|
219
224
|
});
|
package/src/bindings.test.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readOwnProperty } from '@ankhorage/utility/object';
|
|
1
2
|
import { describe, expect, it } from 'bun:test';
|
|
2
3
|
|
|
3
4
|
import type {
|
|
@@ -474,8 +475,8 @@ describe('component data-binding contracts', () => {
|
|
|
474
475
|
category: 'developer_tools',
|
|
475
476
|
themeId: 'default',
|
|
476
477
|
},
|
|
477
|
-
themes:
|
|
478
|
-
{
|
|
478
|
+
themes: {
|
|
479
|
+
default: {
|
|
479
480
|
id: 'default',
|
|
480
481
|
name: 'Default',
|
|
481
482
|
light: {
|
|
@@ -487,7 +488,7 @@ describe('component data-binding contracts', () => {
|
|
|
487
488
|
harmony: 'analogous',
|
|
488
489
|
},
|
|
489
490
|
},
|
|
490
|
-
|
|
491
|
+
},
|
|
491
492
|
activeThemeId: 'default',
|
|
492
493
|
infra: {
|
|
493
494
|
environments: {
|
|
@@ -495,8 +496,8 @@ describe('component data-binding contracts', () => {
|
|
|
495
496
|
deployment: { compute: { provider: 'local' }, runtime: { provider: 'minikube' } },
|
|
496
497
|
},
|
|
497
498
|
},
|
|
498
|
-
apis:
|
|
499
|
-
{
|
|
499
|
+
apis: {
|
|
500
|
+
cms: {
|
|
500
501
|
id: 'cms',
|
|
501
502
|
origin: 'external',
|
|
502
503
|
protocol: 'rest',
|
|
@@ -519,8 +520,8 @@ describe('component data-binding contracts', () => {
|
|
|
519
520
|
},
|
|
520
521
|
},
|
|
521
522
|
},
|
|
522
|
-
|
|
523
|
-
modules:
|
|
523
|
+
},
|
|
524
|
+
modules: {},
|
|
524
525
|
},
|
|
525
526
|
navigator: {
|
|
526
527
|
type: 'stack',
|
|
@@ -566,6 +567,8 @@ describe('component data-binding contracts', () => {
|
|
|
566
567
|
|
|
567
568
|
assertSerializable(manifest);
|
|
568
569
|
expect(manifest.dataBindings?.['hero-title']?.props?.children?.source.kind).toBe('operation');
|
|
569
|
-
expect(manifest.infra.apis?.
|
|
570
|
+
expect(manifest.infra.apis ? readOwnProperty(manifest.infra.apis, 'cms')?.id : undefined).toBe(
|
|
571
|
+
'cms',
|
|
572
|
+
);
|
|
570
573
|
});
|
|
571
574
|
});
|
package/src/bindings.ts
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
import type { ApiId, EndpointId, OperationId } from './data';
|
|
2
|
+
import type { SerializableValue } from './serializable';
|
|
2
3
|
|
|
3
4
|
export type ComponentInstanceId = string;
|
|
4
5
|
export type ComponentTypeId = string;
|
|
5
6
|
|
|
6
|
-
export type BindingValue =
|
|
7
|
-
| string
|
|
8
|
-
| number
|
|
9
|
-
| boolean
|
|
10
|
-
| null
|
|
11
|
-
| readonly BindingValue[]
|
|
12
|
-
| {
|
|
13
|
-
readonly [key: string]: BindingValue;
|
|
14
|
-
};
|
|
7
|
+
export type BindingValue = SerializableValue;
|
|
15
8
|
|
|
16
9
|
export type BindingDataPath = string;
|
|
17
10
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { isRecord } from '@ankhorage/utility/object';
|
|
2
|
+
|
|
3
|
+
export type SerializableSet<T extends string = string> = Readonly<Partial<Record<T, true>>>;
|
|
4
|
+
|
|
5
|
+
/*** Validate the canonical JSON-safe representation for unordered string membership. */
|
|
6
|
+
export function isSerializableSet(value: unknown): value is SerializableSet {
|
|
7
|
+
return isRecord(value) && Object.values(value).every((member) => member === true);
|
|
8
|
+
}
|
package/src/contracts.test.ts
CHANGED
|
@@ -260,10 +260,11 @@ describe('contracts', () => {
|
|
|
260
260
|
deployment: { compute: { provider: 'local' }, runtime: { provider: 'minikube' } },
|
|
261
261
|
},
|
|
262
262
|
},
|
|
263
|
-
modules:
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
263
|
+
modules: {
|
|
264
|
+
'expo-localization': {
|
|
265
|
+
config: {
|
|
266
|
+
defaultLocale: 'en',
|
|
267
|
+
},
|
|
267
268
|
},
|
|
268
269
|
},
|
|
269
270
|
},
|
|
@@ -273,10 +274,11 @@ describe('contracts', () => {
|
|
|
273
274
|
state,
|
|
274
275
|
infra: {
|
|
275
276
|
environments: manifest.infra.environments,
|
|
276
|
-
modules:
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
277
|
+
modules: {
|
|
278
|
+
'expo-localization': {
|
|
279
|
+
config: {
|
|
280
|
+
defaultLocale: 'en',
|
|
281
|
+
},
|
|
280
282
|
},
|
|
281
283
|
},
|
|
282
284
|
},
|
|
@@ -295,11 +297,12 @@ describe('contracts', () => {
|
|
|
295
297
|
deployment: { compute: { provider: 'local' }, runtime: { provider: 'minikube' } },
|
|
296
298
|
},
|
|
297
299
|
},
|
|
298
|
-
modules:
|
|
299
|
-
modulesConfig: {
|
|
300
|
+
modules: {
|
|
300
301
|
'expo-localization': {
|
|
301
|
-
|
|
302
|
-
|
|
302
|
+
config: {
|
|
303
|
+
defaultLocale: 'en',
|
|
304
|
+
locales: ['en', 'de'],
|
|
305
|
+
},
|
|
303
306
|
},
|
|
304
307
|
},
|
|
305
308
|
};
|
|
@@ -311,7 +314,7 @@ describe('contracts', () => {
|
|
|
311
314
|
|
|
312
315
|
expect(hasLegacyModules).toBe(false);
|
|
313
316
|
expect(hasLegacyModulesConfig).toBe(false);
|
|
314
|
-
expect(Object.keys(infra).sort()).toEqual(['environments', 'modules'
|
|
317
|
+
expect(Object.keys(infra).sort()).toEqual(['environments', 'modules']);
|
|
315
318
|
expect(expoConfig.plugins).toEqual(['expo-router']);
|
|
316
319
|
});
|
|
317
320
|
|
package/src/data/apis.test.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { readOwnProperty } from '@ankhorage/utility/object';
|
|
1
2
|
import { describe, expect, it } from 'bun:test';
|
|
2
3
|
|
|
3
4
|
import type {
|
|
4
|
-
|
|
5
|
+
ApiDefinitionRegistry,
|
|
5
6
|
ExternalRestApiDefinition,
|
|
6
7
|
InternalRestApiDefinition,
|
|
7
8
|
} from './apis';
|
|
@@ -56,11 +57,12 @@ describe('canonical API contracts', () => {
|
|
|
56
57
|
expect(api.endpoints.products?.operations['products.list']?.method).toBe('GET');
|
|
57
58
|
});
|
|
58
59
|
|
|
59
|
-
it('stores canonical APIs
|
|
60
|
-
const
|
|
60
|
+
it('stores canonical APIs in an identity-keyed registry', () => {
|
|
61
|
+
const api = createNutritionApi();
|
|
62
|
+
const apis: ApiDefinitionRegistry = { [api.id]: api };
|
|
61
63
|
|
|
62
64
|
assertSerializable(apis);
|
|
63
|
-
expect(
|
|
65
|
+
expect(readOwnProperty<ApiDefinitionRegistry[string]>(apis, 'nutrition')?.id).toBe('nutrition');
|
|
64
66
|
});
|
|
65
67
|
|
|
66
68
|
it('reserves an internal REST API identity without persistence implementation', () => {
|
package/src/data/apis.ts
CHANGED
|
@@ -60,4 +60,4 @@ export interface InternalRestApiDefinition extends ApiBaseDefinition {
|
|
|
60
60
|
export type ApiDefinition =
|
|
61
61
|
ExternalGraphQlApiDefinition | ExternalRestApiDefinition | InternalRestApiDefinition;
|
|
62
62
|
|
|
63
|
-
export type
|
|
63
|
+
export type ApiDefinitionRegistry = Readonly<Record<ApiId, ApiDefinition>>;
|
package/src/data/values.ts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
| boolean
|
|
5
|
-
| null
|
|
6
|
-
| readonly DataContractValue[]
|
|
7
|
-
| {
|
|
8
|
-
readonly [key: string]: DataContractValue;
|
|
9
|
-
};
|
|
1
|
+
import type { SerializableValue } from '../serializable';
|
|
2
|
+
|
|
3
|
+
export type DataContractValue = SerializableValue;
|
|
10
4
|
|
|
11
5
|
export type DataPath = string;
|
package/src/deploy.test.ts
CHANGED
|
@@ -17,14 +17,14 @@ function createManifest(deploy?: unknown): Record<string, unknown> {
|
|
|
17
17
|
category: 'developer_tools',
|
|
18
18
|
themeId: 'default',
|
|
19
19
|
},
|
|
20
|
-
themes:
|
|
21
|
-
{
|
|
20
|
+
themes: {
|
|
21
|
+
default: {
|
|
22
22
|
id: 'default',
|
|
23
23
|
name: 'Default',
|
|
24
24
|
light: { primaryColor: '#3366ff', harmony: 'analogous' },
|
|
25
25
|
dark: { primaryColor: '#6699ff', harmony: 'analogous' },
|
|
26
26
|
},
|
|
27
|
-
|
|
27
|
+
},
|
|
28
28
|
activeThemeId: 'default',
|
|
29
29
|
...(deploy === undefined ? {} : { deploy }),
|
|
30
30
|
infra: {
|
|
@@ -33,7 +33,7 @@ function createManifest(deploy?: unknown): Record<string, unknown> {
|
|
|
33
33
|
deployment: { compute: { provider: 'local' }, runtime: { provider: 'minikube' } },
|
|
34
34
|
},
|
|
35
35
|
},
|
|
36
|
-
modules:
|
|
36
|
+
modules: {},
|
|
37
37
|
},
|
|
38
38
|
navigator: { type: 'stack', routes: [] },
|
|
39
39
|
screens: {},
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ export * from './appManifest';
|
|
|
2
2
|
export * from './auth';
|
|
3
3
|
export * from './bindings';
|
|
4
4
|
export * from './cli';
|
|
5
|
+
export * from './collections';
|
|
5
6
|
export * from './data';
|
|
6
7
|
export * from './db';
|
|
7
8
|
export * from './deploy';
|
|
@@ -14,6 +15,7 @@ export * from './repository';
|
|
|
14
15
|
export * from './requirements';
|
|
15
16
|
export * from './runtimeCallbacks';
|
|
16
17
|
export * from './secrets';
|
|
18
|
+
export * from './serializable';
|
|
17
19
|
export * from './state';
|
|
18
20
|
export * from './storage';
|
|
19
21
|
export * from './theme';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isRecord } from '@ankhorage/utility/object';
|
|
2
2
|
import { isNonEmptyString } from '@ankhorage/utility/string';
|
|
3
3
|
|
|
4
|
+
import { isSerializableSet } from '../collections';
|
|
4
5
|
import type {
|
|
5
6
|
InfraEnvironmentSpec,
|
|
6
7
|
InfraObjectStorageSpec,
|
|
@@ -92,16 +93,14 @@ function hasNetworkingRelationships(value: InfraEnvironmentSpec): boolean {
|
|
|
92
93
|
/*** Validate portable, unique Cerbos policy files without host paths or traversal. */
|
|
93
94
|
function isPolicyFiles(value: unknown): boolean {
|
|
94
95
|
return (
|
|
95
|
-
|
|
96
|
-
value.every(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
)
|
|
103
|
-
new Set(value.map((policy) => (isRecord(policy) ? policy.path : undefined))).size ===
|
|
104
|
-
value.length
|
|
96
|
+
isRecord(value) &&
|
|
97
|
+
Object.entries(value).every(
|
|
98
|
+
([path, content]) =>
|
|
99
|
+
isNonEmptyString(path) &&
|
|
100
|
+
!path.startsWith('/') &&
|
|
101
|
+
!path.split('/').includes('..') &&
|
|
102
|
+
isNonEmptyString(content),
|
|
103
|
+
)
|
|
105
104
|
);
|
|
106
105
|
}
|
|
107
106
|
|
|
@@ -133,7 +132,7 @@ function isObjectStorage(value: unknown): boolean {
|
|
|
133
132
|
if (!isRecord(value)) return false;
|
|
134
133
|
const common = {
|
|
135
134
|
provider: (provider: unknown) => provider === value.provider,
|
|
136
|
-
buckets:
|
|
135
|
+
buckets: (buckets: unknown) => buckets === undefined || isSerializableSet(buckets),
|
|
137
136
|
};
|
|
138
137
|
if (value.provider === 'supabase') {
|
|
139
138
|
return isInfraShape(value, {
|
|
@@ -153,16 +152,17 @@ function isObjectStorage(value: unknown): boolean {
|
|
|
153
152
|
/*** Duplicate desired workload identities cannot be reconciled safely. */
|
|
154
153
|
function isWorkloads(value: unknown): boolean {
|
|
155
154
|
return (
|
|
156
|
-
|
|
157
|
-
value.every(
|
|
158
|
-
|
|
155
|
+
isRecord(value) &&
|
|
156
|
+
Object.entries(value).every(
|
|
157
|
+
([workloadId, workload]) => isInfraWorkloadSpec(workload) && workload.id === workloadId,
|
|
158
|
+
)
|
|
159
159
|
);
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
/*** Prevent two workloads in one environment from claiming the same external listener. */
|
|
163
163
|
function hasUniquePublishedPorts(value: InfraEnvironmentSpec): boolean {
|
|
164
|
-
const ports = (value.workloads ??
|
|
165
|
-
(workload.ports ??
|
|
164
|
+
const ports = Object.values(value.workloads ?? {}).flatMap((workload) =>
|
|
165
|
+
Object.values(workload.ports ?? {}).flatMap(({ publishedPort }) =>
|
|
166
166
|
publishedPort === undefined ? [] : [publishedPort],
|
|
167
167
|
),
|
|
168
168
|
);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { isRecord } from '@ankhorage/utility/object';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { isApiDefinitionRegistry } from '../appManifest/apis';
|
|
4
4
|
import { APP_ENVIRONMENT_IDS } from '../environments';
|
|
5
|
-
import
|
|
5
|
+
import { isSerializableValue } from '../serializable';
|
|
6
|
+
import type { InfraManifest, InfraModuleSpec } from '../types/infraManifest';
|
|
6
7
|
import type { InfraShape } from '../types/infraValidation';
|
|
7
|
-
import { infraFields } from './infraFields';
|
|
8
8
|
import { isInfraEnvironmentSpec } from './isInfraEnvironmentSpec';
|
|
9
9
|
import { isInfraShape } from './isInfraShape';
|
|
10
10
|
|
|
@@ -12,9 +12,8 @@ import { isInfraShape } from './isInfraShape';
|
|
|
12
12
|
export function isInfraManifest(value: unknown): value is InfraManifest {
|
|
13
13
|
return isInfraShape(value, {
|
|
14
14
|
environments: isEnvironments,
|
|
15
|
-
apis: (apis) => apis === undefined ||
|
|
16
|
-
modules:
|
|
17
|
-
modulesConfig: (config) => config === undefined || isRecord(config),
|
|
15
|
+
apis: (apis) => apis === undefined || isApiDefinitionRegistry(apis),
|
|
16
|
+
modules: isModuleRegistry,
|
|
18
17
|
} satisfies InfraShape<InfraManifest>);
|
|
19
18
|
}
|
|
20
19
|
|
|
@@ -27,3 +26,15 @@ function isEnvironments(value: unknown): boolean {
|
|
|
27
26
|
((environment === undefined && key !== 'local') || isInfraEnvironmentSpec(environment)),
|
|
28
27
|
);
|
|
29
28
|
}
|
|
29
|
+
|
|
30
|
+
/*** Validate installed module identity and optional serializable module-owned configuration. */
|
|
31
|
+
function isModuleRegistry(value: unknown): boolean {
|
|
32
|
+
return isRecord(value) && Object.values(value).every(isModuleSpec);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/*** Validate one module registry entry. */
|
|
36
|
+
function isModuleSpec(value: unknown): value is InfraModuleSpec {
|
|
37
|
+
return isInfraShape(value, {
|
|
38
|
+
config: (config) => config === undefined || isSerializableValue(config),
|
|
39
|
+
});
|
|
40
|
+
}
|