Skip to content

Commit a124e5c

Browse files
author
Volodymyr Malyhin
committed
feat: add domainAlias field
1 parent ee5d73e commit a124e5c

File tree

6 files changed

+282
-8
lines changed

6 files changed

+282
-8
lines changed

docs/multi-domains.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,20 @@ When creating or updating a route via the API, you can supply `domainAlias` inst
213213

214214
The Registry resolves the alias to the corresponding `domainId` at write time. If no router domain with that alias exists, the request is rejected with a validation error.
215215

216+
### Using `domainAlias` in apps
217+
218+
When creating or updating an app via the API, or when upserting config via `/api/v1/config`, you can supply `domainAlias` instead of `enforceDomain`. The two fields are mutually exclusive — provide exactly one or neither.
219+
220+
```json
221+
{
222+
"name": "@portal/checkout-embedded",
223+
"domainAlias": "main-shop",
224+
"spaBundle": "https://cdn.example.com/checkout.js"
225+
}
226+
```
227+
228+
The Registry resolves the alias to the corresponding router domain at write time and stores it in the existing `enforceDomain` relation. This is especially useful for embedded applications that do not have their own route: the app is included only in the matching domain config, so it cannot be loaded from another domain through the ILC client APIs.
229+
216230
!!! note ""
217231
The alias must be unique across all router domains within an ILC instance.
218232

registry/server/apps/interfaces/index.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export interface App {
2222
discoveryMetadata?: string | null; // JSON({ [propName: string]: any })
2323
adminNotes?: string | null;
2424
enforceDomain?: number | null;
25+
domainAlias?: string | null;
2526
l10nManifest?: string | null;
2627
namespace?: string | null;
2728
}
@@ -83,17 +84,30 @@ const commonApp = {
8384
discoveryMetadata: Joi.object().default({}),
8485
adminNotes: Joi.string().trim(),
8586
enforceDomain: Joi.number().default(null),
87+
domainAlias: Joi.string()
88+
.lowercase()
89+
.pattern(/^[a-z0-9-]+$/)
90+
.max(64)
91+
.trim(),
8692
l10nManifest: Joi.string().max(255),
8793
versionId: Joi.string().strip(),
8894
namespace: Joi.string().default(null),
8995
};
9096

97+
const validateDomainReference = (value: App, helpers: JoiDefault.CustomHelpers<App>) => {
98+
if (value.enforceDomain != null && value.domainAlias != null) {
99+
return helpers.error('object.oxor', { peers: ['enforceDomain', 'domainAlias'] });
100+
}
101+
102+
return value;
103+
};
104+
91105
export const partialAppSchema = Joi.object<App>({
92106
...commonApp,
93107
name: appNameSchema.forbidden(),
94-
});
108+
}).custom(validateDomainReference);
95109

96110
export const appSchema = Joi.object<App>({
97111
...commonApp,
98112
name: appNameSchema.required(),
99-
});
113+
}).custom(validateDomainReference);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import db from '../../db';
2+
import { getJoiErr } from '../../util/helpers';
3+
4+
export async function resolveDomainAlias<T extends { enforceDomain?: number | null; domainAlias?: string | null }>(
5+
app: T,
6+
): Promise<T> {
7+
const { domainAlias, enforceDomain, ...rest } = app;
8+
9+
if (!domainAlias) {
10+
if (enforceDomain === undefined) {
11+
return rest as T;
12+
}
13+
14+
return { ...rest, enforceDomain } as T;
15+
}
16+
17+
const domain = await db('router_domains').first('id').where({ alias: domainAlias });
18+
if (!domain) {
19+
throw getJoiErr('domainAlias', `Router domain with alias "${domainAlias}" does not exist`);
20+
}
21+
22+
return { ...rest, enforceDomain: domain.id } as T;
23+
}

registry/server/common/services/entries/ApplicationEntry.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Knex } from 'knex';
22
import { User } from '../../../../typings/User';
33
import { App, appSchema, partialAppSchema } from '../../../apps/interfaces';
4+
import { resolveDomainAlias } from '../../../apps/services/resolveDomainAlias';
45
import { VersionedKnex } from '../../../db';
56
import { Tables } from '../../../db/structure';
67
import { EntityTypes } from '../../../versioning/interfaces';
@@ -39,10 +40,11 @@ export class ApplicationEntry implements Entry {
3940
throw new ValidationFqrnError('Patch does not contain any items to update');
4041
}
4142

42-
const appManifest = await this.getManifest(partialAppDTO.assetsDiscoveryUrl);
43+
const resolvedAppDTO = await resolveDomainAlias(partialAppDTO);
44+
const appManifest = await this.getManifest(resolvedAppDTO.assetsDiscoveryUrl);
4345

4446
const appEntity = {
45-
...partialAppDTO,
47+
...resolvedAppDTO,
4648
...appManifest,
4749
};
4850

@@ -66,10 +68,11 @@ export class ApplicationEntry implements Entry {
6668
}
6769

6870
public async create(appDTO: App, { user }: CommonOptions) {
69-
const appManifest = await this.getManifest(appDTO.assetsDiscoveryUrl);
71+
const resolvedAppDTO = await resolveDomainAlias(appDTO);
72+
const appManifest = await this.getManifest(resolvedAppDTO.assetsDiscoveryUrl);
7073

7174
const appEntity = {
72-
...appDTO,
75+
...resolvedAppDTO,
7376
...appManifest,
7477
};
7578

@@ -84,11 +87,12 @@ export class ApplicationEntry implements Entry {
8487

8588
public async upsert(params: unknown, { user, trxProvider, fetchManifest = true }: UpsertOptions): Promise<App> {
8689
const appDto = await appSchema.validateAsync(params, { noDefaults: false, externals: true });
90+
const resolvedAppDTO = await resolveDomainAlias(appDto);
8791

88-
const appManifest = fetchManifest ? await this.getManifest(appDto.assetsDiscoveryUrl) : {};
92+
const appManifest = fetchManifest ? await this.getManifest(resolvedAppDTO.assetsDiscoveryUrl) : {};
8993

9094
const appEntity = {
91-
...appDto,
95+
...resolvedAppDTO,
9296
...appManifest,
9397
};
9498

registry/tests/apps.spec.ts

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,64 @@ describe(`Tests ${example.url}`, () => {
217217
}
218218
});
219219

220+
it('should create record with domainAlias', async () => {
221+
let domainId;
222+
const templateName = 'templateName';
223+
224+
try {
225+
await req
226+
.post('/api/v1/template/')
227+
.send({ name: templateName, content: '<html><head></head><body>foo bar</body></html>' })
228+
.expect(200);
229+
230+
const responseRouterDomains = await req
231+
.post('/api/v1/router_domains/')
232+
.send({ domainName: 'foo.com', template500: templateName, alias: 'app-domain' })
233+
.expect(200);
234+
domainId = responseRouterDomains.body.id;
235+
236+
const response = await req
237+
.post(example.url)
238+
.send({ ...example.correct, domainAlias: 'app-domain' })
239+
.expect(200);
240+
241+
expect(response.body).to.deep.equal({
242+
...example.correct,
243+
enforceDomain: domainId,
244+
});
245+
} finally {
246+
await req.delete(example.url + example.encodedName);
247+
domainId && (await req.delete('/api/v1/router_domains/' + domainId));
248+
await req.delete('/api/v1/template/' + templateName);
249+
}
250+
});
251+
252+
it('should not create record with non-existing domainAlias', async () => {
253+
try {
254+
await req
255+
.post(example.url)
256+
.send({ ...example.correct, domainAlias: 'non-existing' })
257+
.expect(422);
258+
259+
await req.get(example.url + example.encodedName).expect(404);
260+
} finally {
261+
await req.delete(example.url + example.encodedName);
262+
}
263+
});
264+
265+
it('should not create record with both enforceDomain and domainAlias', async () => {
266+
try {
267+
await req
268+
.post(example.url)
269+
.send({ ...example.correct, enforceDomain: 1, domainAlias: 'some-alias' })
270+
.expect(422);
271+
272+
await req.get(example.url + example.encodedName).expect(404);
273+
} finally {
274+
await req.delete(example.url + example.encodedName);
275+
}
276+
});
277+
220278
it('should not create record with non-existed enforceDomain', async () => {
221279
try {
222280
await req
@@ -859,6 +917,113 @@ describe(`Tests ${example.url}`, () => {
859917
}
860918
});
861919

920+
it('should successfully update record with domainAlias', async () => {
921+
let domainId;
922+
const templateName = 'templateName';
923+
924+
try {
925+
await req
926+
.post('/api/v1/template/')
927+
.send({ name: templateName, content: '<html><head></head><body>foo bar</body></html>' })
928+
.expect(200);
929+
930+
const responseRouterDomains = await req
931+
.post('/api/v1/router_domains/')
932+
.send({ domainName: 'foo.com', template500: templateName, alias: 'app-domain-update' })
933+
.expect(200);
934+
domainId = responseRouterDomains.body.id;
935+
936+
await req.post(example.url).send(example.correct).expect(200);
937+
938+
const response = await req
939+
.put(example.url + example.encodedName)
940+
.send({
941+
..._.omit(example.updated, 'name'),
942+
domainAlias: 'app-domain-update',
943+
})
944+
.expect(200);
945+
946+
expect(response.body).to.deep.equal({
947+
...example.updated,
948+
enforceDomain: domainId,
949+
});
950+
} finally {
951+
await req.delete(example.url + example.encodedName);
952+
domainId && (await req.delete('/api/v1/router_domains/' + domainId));
953+
await req.delete('/api/v1/template/' + templateName);
954+
}
955+
});
956+
957+
it('should preserve enforceDomain on unrelated partial updates', async () => {
958+
let domainId;
959+
const templateName = 'templateName';
960+
961+
try {
962+
await req
963+
.post('/api/v1/template/')
964+
.send({ name: templateName, content: '<html><head></head><body>foo bar</body></html>' })
965+
.expect(200);
966+
967+
const responseRouterDomains = await req
968+
.post('/api/v1/router_domains/')
969+
.send({ domainName: 'foo.com', template500: templateName, alias: 'partial-update-domain' })
970+
.expect(200);
971+
domainId = responseRouterDomains.body.id;
972+
973+
await req
974+
.post(example.url)
975+
.send({ ...example.correct, domainAlias: 'partial-update-domain' })
976+
.expect(200);
977+
978+
const response = await req
979+
.put(example.url + example.encodedName)
980+
.send({ adminNotes: 'Updated notes only' })
981+
.expect(200);
982+
983+
expect(response.body).to.deep.include({
984+
adminNotes: 'Updated notes only',
985+
enforceDomain: domainId,
986+
});
987+
} finally {
988+
await req.delete(example.url + example.encodedName);
989+
domainId && (await req.delete('/api/v1/router_domains/' + domainId));
990+
await req.delete('/api/v1/template/' + templateName);
991+
}
992+
});
993+
994+
it('should not update record with non-existing domainAlias', async () => {
995+
try {
996+
await req.post(example.url).send(example.correct).expect(200);
997+
998+
await req
999+
.put(example.url + example.encodedName)
1000+
.send({
1001+
..._.omit(example.updated, 'name'),
1002+
domainAlias: 'non-existing',
1003+
})
1004+
.expect(422);
1005+
} finally {
1006+
await req.delete(example.url + example.encodedName);
1007+
}
1008+
});
1009+
1010+
it('should not update record with both enforceDomain and domainAlias', async () => {
1011+
try {
1012+
await req.post(example.url).send(example.correct).expect(200);
1013+
1014+
await req
1015+
.put(example.url + example.encodedName)
1016+
.send({
1017+
..._.omit(example.updated, 'name'),
1018+
enforceDomain: 1,
1019+
domainAlias: 'some-alias',
1020+
})
1021+
.expect(422);
1022+
} finally {
1023+
await req.delete(example.url + example.encodedName);
1024+
}
1025+
});
1026+
8621027
it('should be possible to remove\reset fields valued during update', async () => {
8631028
let domainId;
8641029
const templateName = 'templateName';

registry/tests/config.spec.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1104,6 +1104,60 @@ describe('Tests /api/v1/config', () => {
11041104
await req.delete('/api/v1/template/' + example.templates.name);
11051105
}
11061106
});
1107+
it('should upsert app with domainAlias', async () => {
1108+
let domainId: number | undefined;
1109+
try {
1110+
await req.post('/api/v1/template/').send(example.templates).expect(200);
1111+
const domainResponse = await req
1112+
.post('/api/v1/router_domains/')
1113+
.send({ ...example.routerDomains, alias: 'config-app-domain' })
1114+
.expect(200);
1115+
domainId = domainResponse.body.id;
1116+
1117+
await req
1118+
.put('/api/v1/config')
1119+
.send({
1120+
apps: [
1121+
{
1122+
...app,
1123+
name: 'app-domain-alias',
1124+
domainAlias: 'config-app-domain',
1125+
},
1126+
],
1127+
})
1128+
.expect(204);
1129+
1130+
const { body: config } = await req
1131+
.get('/api/v1/config')
1132+
.query({ domainName: example.routerDomains.domainName })
1133+
.expect(200);
1134+
1135+
expect(config.apps['app-domain-alias']).to.deep.include({
1136+
enforceDomain: example.routerDomains.domainName,
1137+
});
1138+
} finally {
1139+
await req.delete('/api/v1/app/app-domain-alias');
1140+
domainId && (await req.delete(`/api/v1/router_domains/${domainId}`));
1141+
await req.delete('/api/v1/template/' + example.templates.name);
1142+
}
1143+
});
1144+
it('should not upsert app with non-existing domainAlias', async () => {
1145+
await req
1146+
.put('/api/v1/config')
1147+
.send({
1148+
apps: [
1149+
{
1150+
...app,
1151+
name: 'app-domain-alias-fail',
1152+
domainAlias: 'non-existing',
1153+
},
1154+
],
1155+
})
1156+
.expect(422);
1157+
1158+
const { body: config } = await req.get('/api/v1/config').expect(200);
1159+
expect(config.apps['app-domain-alias-fail']).to.be.undefined;
1160+
});
11071161
it('should not upsert route with non-existing domainAlias', async () => {
11081162
await req
11091163
.put('/api/v1/config')

0 commit comments

Comments
 (0)