@ankhorage/contracts 1.11.0 → 1.13.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 +12 -0
- package/dist/auth.d.ts +38 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +21 -0
- package/dist/auth.js.map +1 -1
- package/dist/bindings.d.ts +80 -25
- package/dist/bindings.d.ts.map +1 -1
- package/dist/bindings.js.map +1 -1
- package/dist/types.d.ts +6 -80
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/auth-oauth.test.ts +103 -0
- package/src/auth.ts +66 -0
- package/src/bindings.test.ts +290 -70
- package/src/bindings.ts +101 -25
- package/src/contracts.test.ts +0 -172
- package/src/types.ts +6 -119
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AUTH_OAUTH_PROVIDER_IDS,
|
|
5
|
+
type AuthAdapter,
|
|
6
|
+
type AuthOAuthConfig,
|
|
7
|
+
type AuthSpec,
|
|
8
|
+
} from './index';
|
|
9
|
+
|
|
10
|
+
describe('OAuth auth contracts', () => {
|
|
11
|
+
it('accepts provider-neutral OAuth provider config on auth specs', () => {
|
|
12
|
+
const oauth: AuthOAuthConfig = {
|
|
13
|
+
enabled: true,
|
|
14
|
+
callbackRoute: '/auth/callback',
|
|
15
|
+
providers: [
|
|
16
|
+
{
|
|
17
|
+
id: 'google',
|
|
18
|
+
label: 'Google',
|
|
19
|
+
enabled: true,
|
|
20
|
+
scopes: ['openid', 'email', 'profile'],
|
|
21
|
+
icon: {
|
|
22
|
+
provider: 'FontAwesome',
|
|
23
|
+
name: 'google',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'custom-sso',
|
|
28
|
+
label: 'Custom SSO',
|
|
29
|
+
enabled: false,
|
|
30
|
+
redirectTo: '/auth/custom/callback',
|
|
31
|
+
queryParams: {
|
|
32
|
+
prompt: 'select_account',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const auth: AuthSpec = {
|
|
39
|
+
scope: 'global',
|
|
40
|
+
provider: 'supabase',
|
|
41
|
+
authorization: { kind: 'RBAC', engine: 'native' },
|
|
42
|
+
oauth,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
expect(AUTH_OAUTH_PROVIDER_IDS.includes('google')).toBe(true);
|
|
46
|
+
expect(auth.oauth?.providers[0]?.icon?.name).toBe('google');
|
|
47
|
+
expect(auth.oauth?.providers[1]?.id).toBe('custom-sso');
|
|
48
|
+
expect(auth.oauth?.providers[1]?.enabled).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('accepts optional OAuth adapter capabilities and redirect flow methods', async () => {
|
|
52
|
+
const adapter: AuthAdapter = {
|
|
53
|
+
capabilities: {
|
|
54
|
+
signInIdentifiers: ['email'],
|
|
55
|
+
supportsSignUp: true,
|
|
56
|
+
supportsPasswordReset: true,
|
|
57
|
+
supportsOtp: true,
|
|
58
|
+
supportsSessionRefresh: true,
|
|
59
|
+
supportsOAuth: true,
|
|
60
|
+
oauthProviders: ['google', 'custom-sso'],
|
|
61
|
+
},
|
|
62
|
+
signIn() {
|
|
63
|
+
return Promise.resolve({
|
|
64
|
+
ok: false,
|
|
65
|
+
error: { code: 'unsupported', message: 'Password sign-in is not implemented.' },
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
signUp() {
|
|
69
|
+
return Promise.resolve({
|
|
70
|
+
ok: false,
|
|
71
|
+
error: { code: 'unsupported', message: 'Sign-up is not implemented.' },
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
signOut() {
|
|
75
|
+
return Promise.resolve({ ok: true });
|
|
76
|
+
},
|
|
77
|
+
getSession() {
|
|
78
|
+
return Promise.resolve({ ok: true, data: null });
|
|
79
|
+
},
|
|
80
|
+
signInWithOAuth(input) {
|
|
81
|
+
return Promise.resolve({
|
|
82
|
+
ok: true,
|
|
83
|
+
data: {
|
|
84
|
+
provider: input.provider,
|
|
85
|
+
url: `https://auth.example.com/oauth/${input.provider}`,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const result = await adapter.signInWithOAuth?.({
|
|
92
|
+
provider: 'google',
|
|
93
|
+
redirectTo: '/auth/callback',
|
|
94
|
+
scopes: ['openid', 'email', 'profile'],
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
expect(adapter.capabilities?.supportsOAuth).toBe(true);
|
|
98
|
+
expect(adapter.capabilities?.oauthProviders).toEqual(['google', 'custom-sso']);
|
|
99
|
+
expect(result?.ok).toBe(true);
|
|
100
|
+
expect(result?.ok === true ? result.data?.provider : undefined).toBe('google');
|
|
101
|
+
expect(result?.ok === true ? result.data?.url : undefined).toContain('/oauth/google');
|
|
102
|
+
});
|
|
103
|
+
});
|
package/src/auth.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { IconSpec } from './types';
|
|
2
|
+
|
|
1
3
|
export const AUTH_IDENTIFIER_KINDS = ['email', 'phone', 'username'] as const;
|
|
2
4
|
export type AuthIdentifierKind = (typeof AUTH_IDENTIFIER_KINDS)[number];
|
|
3
5
|
|
|
@@ -11,6 +13,30 @@ export const AUTH_SIGN_UP_FIELDS = [
|
|
|
11
13
|
export type KnownAuthSignUpField = (typeof AUTH_SIGN_UP_FIELDS)[number];
|
|
12
14
|
export type AuthSignUpField = KnownAuthSignUpField | (string & {});
|
|
13
15
|
|
|
16
|
+
export const AUTH_OAUTH_PROVIDER_IDS = [
|
|
17
|
+
'apple',
|
|
18
|
+
'azure',
|
|
19
|
+
'bitbucket',
|
|
20
|
+
'discord',
|
|
21
|
+
'facebook',
|
|
22
|
+
'figma',
|
|
23
|
+
'github',
|
|
24
|
+
'gitlab',
|
|
25
|
+
'google',
|
|
26
|
+
'kakao',
|
|
27
|
+
'keycloak',
|
|
28
|
+
'linkedin',
|
|
29
|
+
'notion',
|
|
30
|
+
'slack',
|
|
31
|
+
'spotify',
|
|
32
|
+
'twitch',
|
|
33
|
+
'twitter',
|
|
34
|
+
'workos',
|
|
35
|
+
'zoom',
|
|
36
|
+
] as const;
|
|
37
|
+
export type KnownAuthOAuthProviderId = (typeof AUTH_OAUTH_PROVIDER_IDS)[number];
|
|
38
|
+
export type AuthOAuthProviderId = KnownAuthOAuthProviderId | (string & {});
|
|
39
|
+
|
|
14
40
|
export interface AuthIdentifier {
|
|
15
41
|
kind: AuthIdentifierKind;
|
|
16
42
|
value: string;
|
|
@@ -35,11 +61,28 @@ export interface AuthSignUpConfig {
|
|
|
35
61
|
optionalFields?: AuthSignUpField[];
|
|
36
62
|
}
|
|
37
63
|
|
|
64
|
+
export interface AuthOAuthProviderConfig {
|
|
65
|
+
id: AuthOAuthProviderId;
|
|
66
|
+
label?: string;
|
|
67
|
+
enabled?: boolean;
|
|
68
|
+
scopes?: string[];
|
|
69
|
+
redirectTo?: string;
|
|
70
|
+
queryParams?: Record<string, string>;
|
|
71
|
+
icon?: IconSpec;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface AuthOAuthConfig {
|
|
75
|
+
enabled: boolean;
|
|
76
|
+
callbackRoute: string;
|
|
77
|
+
providers: AuthOAuthProviderConfig[];
|
|
78
|
+
}
|
|
79
|
+
|
|
38
80
|
export interface AuthProviderConfig {
|
|
39
81
|
provider: string;
|
|
40
82
|
flow: AuthFlowConfig;
|
|
41
83
|
signIn: AuthSignInConfig;
|
|
42
84
|
signUp?: AuthSignUpConfig;
|
|
85
|
+
oauth?: AuthOAuthConfig;
|
|
43
86
|
passwordReset?: {
|
|
44
87
|
enabled: boolean;
|
|
45
88
|
};
|
|
@@ -114,12 +157,32 @@ export interface VerifyOtpInput {
|
|
|
114
157
|
metadata?: Record<string, unknown>;
|
|
115
158
|
}
|
|
116
159
|
|
|
160
|
+
export interface SignInWithOAuthInput {
|
|
161
|
+
provider: AuthOAuthProviderId;
|
|
162
|
+
redirectTo?: string;
|
|
163
|
+
scopes?: string[];
|
|
164
|
+
queryParams?: Record<string, string>;
|
|
165
|
+
metadata?: Record<string, unknown>;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface AuthOAuthRedirect {
|
|
169
|
+
provider: AuthOAuthProviderId;
|
|
170
|
+
url: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface CompleteOAuthSignInInput {
|
|
174
|
+
url: string;
|
|
175
|
+
redirectTo?: string;
|
|
176
|
+
}
|
|
177
|
+
|
|
117
178
|
export interface AuthAdapterCapabilities {
|
|
118
179
|
signInIdentifiers: AuthIdentifierKind[];
|
|
119
180
|
supportsSignUp: boolean;
|
|
120
181
|
supportsPasswordReset: boolean;
|
|
121
182
|
supportsOtp: boolean;
|
|
122
183
|
supportsSessionRefresh: boolean;
|
|
184
|
+
supportsOAuth?: boolean;
|
|
185
|
+
oauthProviders?: AuthOAuthProviderId[];
|
|
123
186
|
}
|
|
124
187
|
|
|
125
188
|
export interface AuthAdapter {
|
|
@@ -134,4 +197,7 @@ export interface AuthAdapter {
|
|
|
134
197
|
|
|
135
198
|
requestPasswordReset?(input: PasswordResetInput): Promise<AuthResult>;
|
|
136
199
|
verifyOtp?(input: VerifyOtpInput): Promise<AuthResult<AuthSession>>;
|
|
200
|
+
|
|
201
|
+
signInWithOAuth?(input: SignInWithOAuthInput): Promise<AuthResult<AuthOAuthRedirect>>;
|
|
202
|
+
completeOAuthSignIn?(input: CompleteOAuthSignInInput): Promise<AuthResult<AuthSession>>;
|
|
137
203
|
}
|
package/src/bindings.test.ts
CHANGED
|
@@ -1,96 +1,316 @@
|
|
|
1
1
|
import { describe, expect, it } from 'bun:test';
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
|
+
AppManifest,
|
|
5
|
+
BindingInputMap,
|
|
4
6
|
BindingValueSource,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
ComponentDataBinding,
|
|
8
|
+
ComponentDataBindingRegistry,
|
|
9
|
+
EventBinding,
|
|
10
|
+
PropBinding,
|
|
8
11
|
} from './index';
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
};
|
|
20
|
-
const node: UiNode = {
|
|
21
|
-
id: 'firstname-input',
|
|
22
|
-
type: 'Input',
|
|
13
|
+
function assertSerializable<TValue>(value: TValue): void {
|
|
14
|
+
expect(JSON.parse(JSON.stringify(value))).toEqual(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('component data-binding contracts', () => {
|
|
18
|
+
it('serializes a component prop bound to an operation response path', () => {
|
|
19
|
+
const binding: ComponentDataBinding = {
|
|
20
|
+
componentId: 'hero-title',
|
|
21
|
+
componentType: 'Text',
|
|
23
22
|
props: {
|
|
24
|
-
|
|
23
|
+
children: {
|
|
24
|
+
source: {
|
|
25
|
+
kind: 'operation',
|
|
26
|
+
operation: {
|
|
27
|
+
dataSourceId: 'cms',
|
|
28
|
+
endpointId: 'pages',
|
|
29
|
+
operationId: 'pages.getHome',
|
|
30
|
+
},
|
|
31
|
+
path: '$.data.title',
|
|
32
|
+
},
|
|
33
|
+
fallback: {
|
|
34
|
+
value: 'Untitled',
|
|
35
|
+
},
|
|
36
|
+
loading: {
|
|
37
|
+
state: 'loading',
|
|
38
|
+
fallback: {
|
|
39
|
+
value: 'Loading…',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
error: {
|
|
43
|
+
state: 'error',
|
|
44
|
+
message: 'Could not load title.',
|
|
45
|
+
fallback: {
|
|
46
|
+
value: 'Untitled',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
transforms: ['trim'],
|
|
50
|
+
},
|
|
25
51
|
},
|
|
26
|
-
bindings: [binding],
|
|
27
52
|
};
|
|
28
53
|
|
|
29
|
-
|
|
30
|
-
expect(
|
|
54
|
+
assertSerializable(binding);
|
|
55
|
+
expect(binding.props?.children?.source.kind).toBe('operation');
|
|
31
56
|
});
|
|
32
57
|
|
|
33
|
-
it('serializes
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
58
|
+
it('serializes literal, context, event, state, and operation value sources', () => {
|
|
59
|
+
const sources: readonly BindingValueSource[] = [
|
|
60
|
+
{ kind: 'literal', value: { fallback: 'Untitled' } },
|
|
61
|
+
{ kind: 'context', path: 'auth.user.displayName' },
|
|
62
|
+
{ kind: 'event', path: 'payload.values.search' },
|
|
63
|
+
{ kind: 'state', path: 'forms.search.query' },
|
|
64
|
+
{
|
|
65
|
+
kind: 'operation',
|
|
66
|
+
operation: {
|
|
67
|
+
dataSourceId: 'search-api',
|
|
68
|
+
operationId: 'search.query',
|
|
69
|
+
},
|
|
70
|
+
path: '$.results',
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
assertSerializable(sources);
|
|
75
|
+
expect(sources.map((source) => source.kind)).toEqual([
|
|
76
|
+
'literal',
|
|
77
|
+
'context',
|
|
78
|
+
'event',
|
|
79
|
+
'state',
|
|
80
|
+
'operation',
|
|
81
|
+
]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('serializes an event binding that executes a data-source operation', () => {
|
|
85
|
+
const input: BindingInputMap = {
|
|
86
|
+
email: {
|
|
87
|
+
kind: 'source',
|
|
88
|
+
source: {
|
|
89
|
+
kind: 'event',
|
|
90
|
+
path: 'payload.values.email',
|
|
91
|
+
},
|
|
92
|
+
transforms: ['trim', 'lowercase'],
|
|
93
|
+
},
|
|
94
|
+
message: {
|
|
95
|
+
kind: 'source',
|
|
96
|
+
source: {
|
|
97
|
+
kind: 'event',
|
|
98
|
+
path: 'payload.values.message',
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
source: {
|
|
102
|
+
kind: 'literal',
|
|
103
|
+
value: 'contact-form',
|
|
104
|
+
},
|
|
105
|
+
metadata: {
|
|
106
|
+
kind: 'object',
|
|
107
|
+
fields: {
|
|
108
|
+
userId: {
|
|
109
|
+
kind: 'source',
|
|
110
|
+
source: {
|
|
111
|
+
kind: 'context',
|
|
112
|
+
path: 'auth.user.id',
|
|
113
|
+
},
|
|
52
114
|
},
|
|
53
115
|
},
|
|
54
|
-
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const binding: EventBinding = {
|
|
120
|
+
target: {
|
|
121
|
+
kind: 'operation',
|
|
122
|
+
operation: {
|
|
123
|
+
dataSourceId: 'contact-api',
|
|
124
|
+
endpointId: 'messages',
|
|
125
|
+
operationId: 'messages.create',
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
input,
|
|
129
|
+
when: {
|
|
130
|
+
source: {
|
|
131
|
+
kind: 'event',
|
|
132
|
+
path: 'payload.values.email',
|
|
133
|
+
},
|
|
134
|
+
operator: 'exists',
|
|
135
|
+
},
|
|
55
136
|
};
|
|
56
137
|
|
|
57
|
-
|
|
58
|
-
expect(
|
|
138
|
+
assertSerializable(binding);
|
|
139
|
+
expect(binding.target.kind).toBe('operation');
|
|
140
|
+
expect(binding.input?.email?.kind).toBe('source');
|
|
59
141
|
});
|
|
60
142
|
|
|
61
|
-
it('
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
143
|
+
it('serializes an event binding that targets a named action', () => {
|
|
144
|
+
const binding: EventBinding = {
|
|
145
|
+
target: {
|
|
146
|
+
kind: 'action',
|
|
147
|
+
type: 'toast.show',
|
|
148
|
+
},
|
|
149
|
+
input: {
|
|
150
|
+
message: {
|
|
151
|
+
kind: 'literal',
|
|
152
|
+
value: 'Saved successfully.',
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
};
|
|
67
156
|
|
|
68
|
-
|
|
69
|
-
expect(
|
|
157
|
+
assertSerializable(binding);
|
|
158
|
+
expect(binding.target.kind).toBe('action');
|
|
70
159
|
});
|
|
71
160
|
|
|
72
|
-
it('
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
161
|
+
it('serializes component data-binding registries', () => {
|
|
162
|
+
const bindings: ComponentDataBindingRegistry = {
|
|
163
|
+
'submit-button': {
|
|
164
|
+
componentId: 'submit-button',
|
|
165
|
+
componentType: 'Button',
|
|
166
|
+
props: {
|
|
167
|
+
children: {
|
|
168
|
+
source: {
|
|
169
|
+
kind: 'literal',
|
|
170
|
+
value: 'Send',
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
disabled: {
|
|
174
|
+
source: {
|
|
175
|
+
kind: 'state',
|
|
176
|
+
path: 'forms.contact.isSubmitting',
|
|
177
|
+
},
|
|
83
178
|
},
|
|
84
179
|
},
|
|
85
|
-
|
|
86
|
-
|
|
180
|
+
events: {
|
|
181
|
+
press: [
|
|
182
|
+
{
|
|
183
|
+
target: {
|
|
184
|
+
kind: 'operation',
|
|
185
|
+
operation: {
|
|
186
|
+
dataSourceId: 'contact-api',
|
|
187
|
+
operationId: 'messages.create',
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
};
|
|
87
195
|
|
|
88
|
-
|
|
89
|
-
expect(bindings.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
196
|
+
assertSerializable(bindings);
|
|
197
|
+
expect(bindings['submit-button']?.events?.press?.[0]?.target.kind).toBe('operation');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('serializes app-level dataSources and dataBindings on the manifest', () => {
|
|
201
|
+
const propBinding: PropBinding = {
|
|
202
|
+
source: {
|
|
203
|
+
kind: 'operation',
|
|
204
|
+
operation: {
|
|
205
|
+
dataSourceId: 'cms',
|
|
206
|
+
endpointId: 'pages',
|
|
207
|
+
operationId: 'pages.getHome',
|
|
208
|
+
},
|
|
209
|
+
path: '$.data.heroTitle',
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const manifest: AppManifest = {
|
|
214
|
+
metadata: {
|
|
215
|
+
name: 'Demo',
|
|
216
|
+
slug: 'demo',
|
|
217
|
+
version: '1.0.0',
|
|
218
|
+
themeId: 'default',
|
|
219
|
+
},
|
|
220
|
+
themes: [
|
|
221
|
+
{
|
|
222
|
+
id: 'default',
|
|
223
|
+
name: 'Default',
|
|
224
|
+
light: {
|
|
225
|
+
primaryColor: '#3366ff',
|
|
226
|
+
harmony: 'analogous',
|
|
227
|
+
},
|
|
228
|
+
dark: {
|
|
229
|
+
primaryColor: '#3366ff',
|
|
230
|
+
harmony: 'analogous',
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
activeThemeId: 'default',
|
|
235
|
+
infra: {
|
|
236
|
+
plugins: [],
|
|
237
|
+
},
|
|
238
|
+
navigator: {
|
|
239
|
+
type: 'stack',
|
|
240
|
+
routes: [
|
|
241
|
+
{
|
|
242
|
+
name: 'home',
|
|
243
|
+
screenId: 'home',
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
},
|
|
247
|
+
screens: {
|
|
248
|
+
home: {
|
|
249
|
+
id: 'home',
|
|
250
|
+
name: 'Home',
|
|
251
|
+
root: {
|
|
252
|
+
id: 'screen-root',
|
|
253
|
+
type: 'Screen',
|
|
254
|
+
children: [
|
|
255
|
+
{
|
|
256
|
+
id: 'hero-title',
|
|
257
|
+
type: 'Text',
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
dataSources: {
|
|
264
|
+
cms: {
|
|
265
|
+
id: 'cms',
|
|
266
|
+
kind: 'rest',
|
|
267
|
+
baseUrl: 'https://cms.example.com',
|
|
268
|
+
endpoints: {
|
|
269
|
+
pages: {
|
|
270
|
+
id: 'pages',
|
|
271
|
+
kind: 'http',
|
|
272
|
+
path: '/pages/home',
|
|
273
|
+
operations: {
|
|
274
|
+
'pages.getHome': {
|
|
275
|
+
id: 'pages.getHome',
|
|
276
|
+
endpointId: 'pages',
|
|
277
|
+
protocol: 'http',
|
|
278
|
+
intent: 'read',
|
|
279
|
+
method: 'GET',
|
|
280
|
+
path: '/pages/home',
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
dataBindings: {
|
|
288
|
+
'hero-title': {
|
|
289
|
+
componentId: 'hero-title',
|
|
290
|
+
componentType: 'Text',
|
|
291
|
+
props: {
|
|
292
|
+
children: propBinding,
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
settings: {
|
|
297
|
+
localization: {
|
|
298
|
+
defaultLocale: 'en',
|
|
299
|
+
locales: ['en'],
|
|
300
|
+
},
|
|
301
|
+
authFlow: {
|
|
302
|
+
signInRoute: '/sign-in',
|
|
303
|
+
signUpRoute: '/sign-up',
|
|
304
|
+
signOutRoute: '/sign-out',
|
|
305
|
+
forgotPasswordRoute: '/forgot-password',
|
|
306
|
+
postSignInRoute: '/',
|
|
307
|
+
unauthorizedRoute: '/sign-in',
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
assertSerializable(manifest);
|
|
313
|
+
expect(manifest.dataBindings?.['hero-title']?.props?.children?.source.kind).toBe('operation');
|
|
314
|
+
expect(manifest.dataSources?.cms?.kind).toBe('rest');
|
|
95
315
|
});
|
|
96
316
|
});
|