Skip to content

Commit 16c11bb

Browse files
committed
feat(core): add AmplifyContext infrastructure (#14805)
* feat(core): add AmplifyContext infrastructure Add context module with AmplifyContext interface, context brand for runtime type checking, global context management, and resolveCtxArgs utility for optional context-first parameter pattern. Also adds configurationBuilder, AmplifyServerContextError, and setGlobalContext call in Amplify.configure(). * fix(core): reintroduce oAuthListener * tests(core): add tests for context * chore(core): rename configurationBuilder * chore(core): remove AmplifyServerContextError alias * fix(core): remove context exports from singleton * types(core): remove unrelated Storage typings * fix(chore): address smaller issues * fix(core): remove unneeded exports * chore(core): extract deepMerge * chore(core): add license to deepMerge
1 parent 67a015a commit 16c11bb

22 files changed

Lines changed: 1402 additions & 20 deletions
Lines changed: 376 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { createConfigurationBuilder } from '../../src';
5+
6+
describe('createConfigurationBuilder', () => {
7+
describe('basic builder', () => {
8+
it('returns a builder object with from/add/patch/build methods', () => {
9+
const builder = createConfigurationBuilder();
10+
expect(builder.from).toBeDefined();
11+
expect(builder.add).toBeDefined();
12+
expect(builder.patch).toBeDefined();
13+
expect(builder.build).toBeDefined();
14+
});
15+
16+
it('build() returns empty frozen config when nothing added', () => {
17+
const config = createConfigurationBuilder().build();
18+
expect(config).toEqual({});
19+
expect(Object.isFrozen(config)).toBe(true);
20+
});
21+
});
22+
23+
describe('.add(category, value)', () => {
24+
it('sets a category config', () => {
25+
const config = createConfigurationBuilder()
26+
.add('Auth', {
27+
Cognito: { userPoolId: 'pool', userPoolClientId: 'client' },
28+
})
29+
.build();
30+
expect(config.Auth).toEqual({
31+
Cognito: { userPoolId: 'pool', userPoolClientId: 'client' },
32+
});
33+
});
34+
35+
it('replaces existing category config entirely', () => {
36+
const config = createConfigurationBuilder()
37+
.add('Auth', {
38+
Cognito: { userPoolId: 'a', userPoolClientId: 'b' },
39+
})
40+
.add('Auth', {
41+
Cognito: { userPoolId: 'c', userPoolClientId: 'd' },
42+
})
43+
.build();
44+
expect(config.Auth?.Cognito?.userPoolId).toBe('c');
45+
});
46+
47+
it('returns the builder for chaining', () => {
48+
const builder = createConfigurationBuilder();
49+
expect(
50+
builder.add('Auth', {
51+
Cognito: { userPoolId: 'pool', userPoolClientId: 'client' },
52+
}),
53+
).toBe(builder);
54+
});
55+
});
56+
57+
describe('.patch(category, value)', () => {
58+
it('deep-merges partial config into existing category', () => {
59+
const config = createConfigurationBuilder()
60+
.add('Auth', {
61+
Cognito: {
62+
userPoolId: 'pool',
63+
userPoolClientId: 'client',
64+
identityPoolId: 'id',
65+
},
66+
})
67+
.patch('Auth', { Cognito: { userPoolId: 'new-pool' } })
68+
.build();
69+
expect(config.Auth?.Cognito?.userPoolId).toBe('new-pool');
70+
expect(config.Auth?.Cognito?.userPoolClientId).toBe('client');
71+
});
72+
73+
it('creates category if it does not exist', () => {
74+
const config = createConfigurationBuilder()
75+
.patch('Auth', {
76+
Cognito: { userPoolId: 'pool', userPoolClientId: 'client' },
77+
})
78+
.build();
79+
expect(config.Auth).toBeDefined();
80+
});
81+
82+
it('handles category configuration by replacing (not merging)', () => {
83+
const config = createConfigurationBuilder()
84+
.add('API', {
85+
REST: {
86+
myApi: {
87+
endpoint: 'https://api.example.com',
88+
region: 'us-east-1',
89+
},
90+
},
91+
})
92+
.patch('API', {
93+
REST: {
94+
myApi: {
95+
endpoint: 'https://api2.example.com',
96+
},
97+
},
98+
})
99+
.build();
100+
expect(config.API?.REST?.myApi?.endpoint).toBe(
101+
'https://api2.example.com',
102+
);
103+
expect(config.API?.REST?.myApi.region).toBe('us-east-1');
104+
});
105+
106+
it('returns the builder for chaining', () => {
107+
const builder = createConfigurationBuilder();
108+
expect(
109+
builder.patch('Auth', {
110+
Cognito: { userPoolId: 'pool', userPoolClientId: 'client' },
111+
}),
112+
).toBe(builder);
113+
});
114+
});
115+
116+
describe('.from(seed)', () => {
117+
it('merges a ResourcesConfig seed', () => {
118+
const seed = {
119+
Auth: { Cognito: { userPoolId: 'x', userPoolClientId: 'y' } },
120+
};
121+
const config = createConfigurationBuilder().from(seed).build();
122+
expect(config.Auth).toEqual(seed.Auth);
123+
});
124+
125+
it('merges a legacy config seed (parsed via parseAmplifyConfig)', () => {
126+
const legacy = {
127+
aws_project_region: 'us-east-1',
128+
aws_user_pools_id: 'pool',
129+
aws_user_pools_web_client_id: 'client',
130+
};
131+
const config = createConfigurationBuilder()
132+
.from(legacy as any)
133+
.build();
134+
expect(config.Auth?.Cognito?.userPoolId).toBe('pool');
135+
});
136+
137+
it('merges another ConfigurationBuilder as seed', () => {
138+
const base = createConfigurationBuilder().add('Auth', {
139+
Cognito: { userPoolId: 'pool', userPoolClientId: 'client' },
140+
});
141+
const config = createConfigurationBuilder().from(base).build();
142+
expect(config.Auth).toBeDefined();
143+
});
144+
145+
it('can be called multiple times (accumulates)', () => {
146+
const config = createConfigurationBuilder()
147+
.from({
148+
Auth: {
149+
Cognito: { userPoolId: 'pool', userPoolClientId: 'client' },
150+
},
151+
})
152+
.from({ Storage: { S3: { bucket: 'my-bucket', region: 'us-east-1' } } })
153+
.build();
154+
expect(config.Auth).toBeDefined();
155+
expect(config.Storage).toBeDefined();
156+
});
157+
158+
it('returns the builder for chaining', () => {
159+
const builder = createConfigurationBuilder();
160+
expect(
161+
builder.from({
162+
Auth: {
163+
Cognito: { userPoolId: 'pool', userPoolClientId: 'client' },
164+
},
165+
}),
166+
).toBe(builder);
167+
});
168+
});
169+
170+
describe('createConfigurationBuilder({ from })', () => {
171+
it('accepts initial seed via options', () => {
172+
const config = createConfigurationBuilder({
173+
from: {
174+
Auth: {
175+
Cognito: { userPoolId: 'pool', userPoolClientId: 'client' },
176+
},
177+
},
178+
}).build();
179+
expect(config.Auth).toBeDefined();
180+
});
181+
182+
it('accepts a ConfigurationBuilder as initial seed', () => {
183+
const base = createConfigurationBuilder().add('Auth', {
184+
Cognito: { userPoolId: 'pool', userPoolClientId: 'client' },
185+
});
186+
const config = createConfigurationBuilder({ from: base }).build();
187+
expect(config.Auth).toBeDefined();
188+
});
189+
});
190+
191+
describe('category shorthands', () => {
192+
it('.auth() is equivalent to .add("Auth", ...)', () => {
193+
const a = createConfigurationBuilder()
194+
.auth({ Cognito: { userPoolId: 'pool', userPoolClientId: 'client' } })
195+
.build();
196+
const b = createConfigurationBuilder()
197+
.add('Auth', {
198+
Cognito: { userPoolId: 'pool', userPoolClientId: 'client' },
199+
})
200+
.build();
201+
expect(a).toEqual(b);
202+
});
203+
204+
it('.storage() is equivalent to .add("Storage", ...)', () => {
205+
const a = createConfigurationBuilder()
206+
.storage({ S3: { bucket: 'my-bucket', region: 'us-east-1' } })
207+
.build();
208+
const b = createConfigurationBuilder()
209+
.add('Storage', { S3: { bucket: 'my-bucket', region: 'us-east-1' } })
210+
.build();
211+
expect(a).toEqual(b);
212+
});
213+
214+
it('.api() is equivalent to .add("API", ...)', () => {
215+
const a = createConfigurationBuilder()
216+
.api({
217+
REST: {
218+
myApi: { endpoint: 'https://api.example.com', region: 'us-east-1' },
219+
},
220+
})
221+
.build();
222+
const b = createConfigurationBuilder()
223+
.add('API', {
224+
REST: {
225+
myApi: { endpoint: 'https://api.example.com', region: 'us-east-1' },
226+
},
227+
})
228+
.build();
229+
expect(a).toEqual(b);
230+
});
231+
232+
it('.analytics() is equivalent to .add("Analytics", ...)', () => {
233+
const a = createConfigurationBuilder()
234+
.analytics({
235+
Pinpoint: {
236+
appId: 'app-id',
237+
region: 'us-east-1',
238+
},
239+
})
240+
.build();
241+
const b = createConfigurationBuilder()
242+
.add('Analytics', {
243+
Pinpoint: {
244+
appId: 'app-id',
245+
region: 'us-east-1',
246+
},
247+
})
248+
.build();
249+
expect(a).toEqual(b);
250+
});
251+
252+
it('.geo() is equivalent to .add("Geo", ...)', () => {
253+
const a = createConfigurationBuilder()
254+
.geo({
255+
LocationService: {
256+
maps: { items: {}, default: 'map1' },
257+
region: 'us-east-1',
258+
},
259+
})
260+
.build();
261+
const b = createConfigurationBuilder()
262+
.add('Geo', {
263+
LocationService: {
264+
maps: { items: {}, default: 'map1' },
265+
region: 'us-east-1',
266+
},
267+
})
268+
.build();
269+
expect(a).toEqual(b);
270+
});
271+
272+
it('.notifications() is equivalent to .add("Notifications", ...)', () => {
273+
const a = createConfigurationBuilder()
274+
.notifications({
275+
PushNotification: {
276+
Pinpoint: { appId: 'app-id', region: 'us-east-1' },
277+
},
278+
})
279+
.build();
280+
const b = createConfigurationBuilder()
281+
.add('Notifications', {
282+
PushNotification: {
283+
Pinpoint: { appId: 'app-id', region: 'us-east-1' },
284+
},
285+
})
286+
.build();
287+
expect(a).toEqual(b);
288+
});
289+
290+
it('.interactions() is equivalent to .add("Interactions", ...)', () => {
291+
const a = createConfigurationBuilder()
292+
.interactions({
293+
LexV2: {
294+
'bot-alias': {
295+
aliasId: 'alias-id',
296+
botId: 'bot-id',
297+
localeId: 'en_US',
298+
region: 'us-east-1',
299+
},
300+
},
301+
})
302+
.build();
303+
const b = createConfigurationBuilder()
304+
.add('Interactions', {
305+
LexV2: {
306+
'bot-alias': {
307+
aliasId: 'alias-id',
308+
botId: 'bot-id',
309+
localeId: 'en_US',
310+
region: 'us-east-1',
311+
},
312+
},
313+
})
314+
.build();
315+
expect(a).toEqual(b);
316+
});
317+
318+
it('.predictions() is equivalent to .add("Predictions", ...)', () => {
319+
const a = createConfigurationBuilder()
320+
.predictions({
321+
convert: {
322+
translateText: {
323+
region: 'us-east-1',
324+
defaults: { sourceLanguage: 'en', targetLanguage: 'es' },
325+
},
326+
},
327+
})
328+
.build();
329+
const b = createConfigurationBuilder()
330+
.add('Predictions', {
331+
convert: {
332+
translateText: {
333+
region: 'us-east-1',
334+
defaults: { sourceLanguage: 'en', targetLanguage: 'es' },
335+
},
336+
},
337+
})
338+
.build();
339+
expect(a).toEqual(b);
340+
});
341+
});
342+
343+
describe('.build()', () => {
344+
it('returns a frozen object', () => {
345+
const config = createConfigurationBuilder()
346+
.add('Auth', {
347+
Cognito: { userPoolId: 'pool', userPoolClientId: 'client' },
348+
})
349+
.build();
350+
expect(Object.isFrozen(config)).toBe(true);
351+
});
352+
353+
it('returns a shallow copy — mutations to internal state do not affect built config', () => {
354+
const builder = createConfigurationBuilder().add('Auth', {
355+
Cognito: { userPoolId: 'pool', userPoolClientId: 'client' },
356+
});
357+
const config1 = builder.build();
358+
builder.add('Storage', {
359+
S3: { bucket: 'my-bucket', region: 'us-east-1' },
360+
});
361+
const config2 = builder.build();
362+
expect(config1.Storage).toBeUndefined();
363+
expect(config2.Storage).toBeDefined();
364+
});
365+
366+
it('can be called multiple times', () => {
367+
const builder = createConfigurationBuilder().add('Auth', {
368+
Cognito: { userPoolId: 'pool', userPoolClientId: 'client' },
369+
});
370+
const a = builder.build();
371+
const b = builder.build();
372+
expect(a).toEqual(b);
373+
expect(a).not.toBe(b);
374+
});
375+
});
376+
});

0 commit comments

Comments
 (0)