@pwrdrvr/microapps-router-lib 0.4.0-alpha.9 → 1.0.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.
Files changed (44) hide show
  1. package/dist/get-app-info.d.ts +9 -0
  2. package/dist/get-app-info.d.ts.map +1 -0
  3. package/dist/get-app-info.js +25 -0
  4. package/dist/get-app-info.js.map +1 -0
  5. package/dist/get-route.d.ts +83 -0
  6. package/dist/get-route.d.ts.map +1 -0
  7. package/dist/get-route.js +154 -0
  8. package/dist/get-route.js.map +1 -0
  9. package/dist/index.d.ts +5 -96
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +11 -372
  12. package/dist/index.js.map +1 -1
  13. package/dist/load-app-frame.d.ts +8 -0
  14. package/dist/load-app-frame.d.ts.map +1 -0
  15. package/dist/load-app-frame.js +38 -0
  16. package/dist/load-app-frame.js.map +1 -0
  17. package/dist/normalize-path-prefix.d.ts +8 -0
  18. package/dist/normalize-path-prefix.d.ts.map +1 -0
  19. package/dist/normalize-path-prefix.js +21 -0
  20. package/dist/normalize-path-prefix.js.map +1 -0
  21. package/dist/redirect-default-file.d.ts +18 -0
  22. package/dist/redirect-default-file.d.ts.map +1 -0
  23. package/dist/redirect-default-file.js +54 -0
  24. package/dist/redirect-default-file.js.map +1 -0
  25. package/dist/route-app.d.ts +23 -0
  26. package/dist/route-app.d.ts.map +1 -0
  27. package/dist/route-app.js +169 -0
  28. package/dist/route-app.js.map +1 -0
  29. package/package.json +4 -2
  30. package/src/get-app-info.spec.ts +77 -0
  31. package/src/get-app-info.ts +31 -0
  32. package/src/get-route.spec.ts +585 -0
  33. package/src/get-route.ts +267 -0
  34. package/src/index.ts +5 -537
  35. package/src/load-app-frame.spec.ts +51 -0
  36. package/src/load-app-frame.ts +36 -0
  37. package/src/normalize-path-prefix.spec.ts +27 -0
  38. package/src/normalize-path-prefix.ts +18 -0
  39. package/src/redirect-default-file.spec.ts +98 -0
  40. package/src/redirect-default-file.ts +79 -0
  41. package/src/route-app.spec.ts +128 -0
  42. package/src/route-app.ts +202 -0
  43. package/src/index.spec.ts +0 -322
  44. /package/src/{index.prefix.spec.ts → get-route.prefix.spec.ts} +0 -0
@@ -0,0 +1,98 @@
1
+ import { DBManager, Version } from '@pwrdrvr/microapps-datalib';
2
+ import { AppVersionCache } from './app-cache';
3
+ import { RedirectToDefaultFile } from './redirect-default-file';
4
+
5
+ jest.mock('./app-cache');
6
+
7
+ describe('RedirectToDefaultFile', () => {
8
+ const mockDbManager = {} as DBManager;
9
+
10
+ afterEach(() => {
11
+ jest.clearAllMocks();
12
+ });
13
+
14
+ it('should return undefined if GetVersionInfo throws an error', async () => {
15
+ (AppVersionCache.GetInstance as jest.Mock).mockImplementation(() => {
16
+ return {
17
+ GetVersionInfo: () => {
18
+ throw new Error('GetVersionInfo error');
19
+ },
20
+ };
21
+ });
22
+
23
+ const result = await RedirectToDefaultFile({
24
+ dbManager: mockDbManager,
25
+ appName: 'testApp',
26
+ semVer: '1.0.0',
27
+ appNameOrRootTrailingSlash: '',
28
+ });
29
+
30
+ expect(result).toBeUndefined();
31
+ });
32
+
33
+ it('should return undefined if versionInfo is undefined', async () => {
34
+ (AppVersionCache.GetInstance as jest.Mock).mockImplementation(() => {
35
+ return {
36
+ GetVersionInfo: () => undefined,
37
+ };
38
+ });
39
+
40
+ const result = await RedirectToDefaultFile({
41
+ dbManager: mockDbManager,
42
+ appName: 'testApp',
43
+ semVer: '1.0.0',
44
+ appNameOrRootTrailingSlash: '',
45
+ });
46
+
47
+ expect(result).toBeUndefined();
48
+ });
49
+
50
+ it('should return undefined if versionInfo.DefaultFile is not set', async () => {
51
+ const versionInfo: Version = {
52
+ AppName: 'testApp',
53
+ SemVer: '1.0.0',
54
+ // @ts-expect-error this is a test after-all
55
+ DefaultFile: undefined,
56
+ };
57
+ (AppVersionCache.GetInstance as jest.Mock).mockImplementation(() => {
58
+ return {
59
+ GetVersionInfo: () => versionInfo,
60
+ };
61
+ });
62
+
63
+ const result = await RedirectToDefaultFile({
64
+ dbManager: mockDbManager,
65
+ appName: 'testApp',
66
+ semVer: '1.0.0',
67
+ appNameOrRootTrailingSlash: '',
68
+ });
69
+
70
+ expect(result).toBeUndefined();
71
+ });
72
+
73
+ it('should return a redirect with status code 302 when versionInfo.DefaultFile is set', async () => {
74
+ // @ts-expect-error This is ok to be incomplete for the test
75
+ const versionInfo: Version = {
76
+ AppName: 'testApp',
77
+ SemVer: '1.0.0',
78
+ DefaultFile: 'index.html',
79
+ };
80
+ (AppVersionCache.GetInstance as jest.Mock).mockImplementation(() => {
81
+ return {
82
+ GetVersionInfo: () => versionInfo,
83
+ };
84
+ });
85
+
86
+ const result = await RedirectToDefaultFile({
87
+ dbManager: mockDbManager,
88
+ appName: 'testApp',
89
+ semVer: '1.0.0',
90
+ appNameOrRootTrailingSlash: 'testapp/',
91
+ });
92
+
93
+ expect(result).toEqual({
94
+ statusCode: 302,
95
+ redirectLocation: '/testapp/1.0.0/index.html',
96
+ });
97
+ });
98
+ });
@@ -0,0 +1,79 @@
1
+ import { DBManager, Version } from '@pwrdrvr/microapps-datalib';
2
+ import { AppVersionCache } from './app-cache';
3
+ import { IGetRouteResult } from './get-route';
4
+ import Log from './lib/log';
5
+
6
+ const log = Log.Instance;
7
+
8
+ /**
9
+ * Redirect the request to app/x.y.z/? to app/x.y.z/{defaultFile}
10
+ * @param response
11
+ * @param normalizedPathPrefix
12
+ * @param appName
13
+ * @param semVer
14
+ * @returns
15
+ */
16
+
17
+ export async function RedirectToDefaultFile(opts: {
18
+ dbManager: DBManager;
19
+ normalizedPathPrefix?: string;
20
+ appName: string;
21
+ semVer: string;
22
+ appNameOrRootTrailingSlash: string;
23
+ }): Promise<IGetRouteResult | undefined> {
24
+ const {
25
+ dbManager,
26
+ normalizedPathPrefix = '',
27
+ appName,
28
+ appNameOrRootTrailingSlash,
29
+ semVer,
30
+ } = opts;
31
+ let versionInfo: Version | undefined;
32
+
33
+ try {
34
+ // Get the cache
35
+ const appVersionCache = AppVersionCache.GetInstance({ dbManager });
36
+
37
+ versionInfo = await appVersionCache.GetVersionInfo({
38
+ key: { AppName: appName, SemVer: semVer },
39
+ });
40
+ } catch (error) {
41
+ log.info(
42
+ `LoadVersion threw for '${normalizedPathPrefix}/${appNameOrRootTrailingSlash}${semVer}' - falling through to app routing'`,
43
+ {
44
+ appName,
45
+ semVer,
46
+ error,
47
+ },
48
+ );
49
+ return undefined;
50
+ }
51
+
52
+ if (versionInfo === undefined) {
53
+ log.info(
54
+ `LoadVersion returned undefined for '${normalizedPathPrefix}/${appNameOrRootTrailingSlash}${semVer}', assuming not found - falling through to app routing'`,
55
+ {
56
+ appName,
57
+ semVer,
58
+ },
59
+ );
60
+ return undefined;
61
+ }
62
+
63
+ if (!versionInfo.DefaultFile) {
64
+ return undefined;
65
+ }
66
+
67
+ log.info(
68
+ `found '${normalizedPathPrefix}/${appNameOrRootTrailingSlash}${semVer}' - returning 302 to ${normalizedPathPrefix}/${appNameOrRootTrailingSlash}${semVer}/${versionInfo.DefaultFile}`,
69
+ {
70
+ statusCode: 302,
71
+ routedPath: `${normalizedPathPrefix}/${appNameOrRootTrailingSlash}${semVer}/${versionInfo.DefaultFile}`,
72
+ },
73
+ );
74
+
75
+ return {
76
+ statusCode: 302,
77
+ redirectLocation: `${normalizedPathPrefix}/${appNameOrRootTrailingSlash}${semVer}/${versionInfo.DefaultFile}`,
78
+ };
79
+ }
@@ -0,0 +1,128 @@
1
+ import { DBManager } from '@pwrdrvr/microapps-datalib';
2
+ import { AppVersionCache } from './app-cache';
3
+ import { RouteApp } from './route-app';
4
+
5
+ jest.mock('./app-cache');
6
+
7
+ describe('RouteApp', () => {
8
+ const mockDbManager = {} as DBManager;
9
+
10
+ afterEach(() => {
11
+ jest.clearAllMocks();
12
+ });
13
+
14
+ const testCases = [
15
+ {
16
+ caseName: 'should route simple app with iframe startup and no versions specified',
17
+ possibleSemVerPathAfterAppVersionInfo: undefined,
18
+ possibleSemVerPathNextDataVersionInfo: undefined,
19
+ possibleSemVerQueryVersionInfo: undefined,
20
+ possibleSemVerQuery: undefined,
21
+ defaultVersion: '1.0.0',
22
+ expectedStatusCode: 200,
23
+ expectedAppName: 'testApp',
24
+ expectedSemVer: '1.0.0',
25
+ expectedStartupType: 'iframe',
26
+ },
27
+ {
28
+ caseName: 'uses the path specified version instead of the default version',
29
+ possibleSemVerPathAfterAppVersionInfo: {
30
+ SemVer: '1.0.1',
31
+ DefaultFile: 'index.html',
32
+ StartupType: 'iframe',
33
+ },
34
+ possibleSemVerPathNextDataVersionInfo: undefined,
35
+ possibleSemVerQueryVersionInfo: undefined,
36
+ possibleSemVerQuery: undefined,
37
+ defaultVersion: '1.0.0',
38
+ expectedStatusCode: 200,
39
+ expectedAppName: 'testApp',
40
+ expectedSemVer: '1.0.1',
41
+ expectedStartupType: undefined,
42
+ },
43
+ {
44
+ caseName: 'uses the query string specified version instead of the default version',
45
+ possibleSemVerPathAfterAppVersionInfo: undefined,
46
+ possibleSemVerPathNextDataVersionInfo: undefined,
47
+ possibleSemVerQueryVersionInfo: {
48
+ SemVer: '1.0.3',
49
+ DefaultFile: 'index.html',
50
+ StartupType: 'iframe',
51
+ },
52
+ possibleSemVerQuery: '1.0.3',
53
+ defaultVersion: '1.0.0',
54
+ expectedStatusCode: 200,
55
+ expectedAppName: 'testApp',
56
+ expectedSemVer: '1.0.3',
57
+ expectedStartupType: 'iframe',
58
+ },
59
+ {
60
+ caseName: 'gives a 404 when the query string version does not exist',
61
+ possibleSemVerPathAfterAppVersionInfo: undefined,
62
+ possibleSemVerPathNextDataVersionInfo: undefined,
63
+ possibleSemVerQueryVersionInfo: undefined,
64
+ possibleSemVerQuery: '1.0.4',
65
+ defaultVersion: '1.0.0',
66
+ expectedStatusCode: 404,
67
+ expectedAppName: undefined,
68
+ expectedSemVer: undefined,
69
+ expectedStartupType: undefined,
70
+ },
71
+ ];
72
+
73
+ it.each(testCases)(
74
+ '$caseName',
75
+ async ({
76
+ possibleSemVerPathAfterAppVersionInfo,
77
+ possibleSemVerPathNextDataVersionInfo,
78
+ possibleSemVerQueryVersionInfo,
79
+ possibleSemVerQuery,
80
+ defaultVersion,
81
+ expectedStatusCode,
82
+ expectedAppName,
83
+ expectedSemVer,
84
+ expectedStartupType,
85
+ }) => {
86
+ (AppVersionCache.GetInstance as jest.Mock).mockImplementation(() => {
87
+ return {
88
+ GetVersionInfo: (options: { key: { AppName: string; SemVer: string } }) => {
89
+ if (options.key.SemVer === 'pathAfterApp') {
90
+ return possibleSemVerPathAfterAppVersionInfo;
91
+ }
92
+ if (options.key.SemVer === 'pathNextData') {
93
+ return possibleSemVerPathNextDataVersionInfo;
94
+ }
95
+ if (options.key.SemVer === 'query') {
96
+ return possibleSemVerQueryVersionInfo;
97
+ }
98
+ if (options.key.SemVer === defaultVersion) {
99
+ return { ...options.key, DefaultFile: 'index.html', StartupType: 'iframe' };
100
+ }
101
+ return undefined;
102
+ },
103
+ GetRules: () => ({ RuleSet: { default: { SemVer: defaultVersion } } }),
104
+ };
105
+ });
106
+
107
+ const result = await RouteApp({
108
+ dbManager: mockDbManager,
109
+ event: { dbManager: mockDbManager, locales: [], rawPath: '/testApp' },
110
+ appName: 'testApp',
111
+ possibleSemVerPathNextData: possibleSemVerPathNextDataVersionInfo
112
+ ? 'pathNextData'
113
+ : undefined,
114
+ possibleSemVerPathAfterApp: possibleSemVerPathAfterAppVersionInfo
115
+ ? 'pathAfterApp'
116
+ : undefined,
117
+ possibleSemVerQuery: possibleSemVerQueryVersionInfo ? 'query' : possibleSemVerQuery,
118
+ additionalParts: '',
119
+ appNameOrRootTrailingSlash: '',
120
+ });
121
+
122
+ expect(result.statusCode).toBe(expectedStatusCode);
123
+ expect(result.appName).toBe(expectedAppName);
124
+ expect(result.semVer).toBe(expectedSemVer);
125
+ expect(result.startupType).toBe(expectedStartupType);
126
+ },
127
+ );
128
+ });
@@ -0,0 +1,202 @@
1
+ import { DBManager, Version } from '@pwrdrvr/microapps-datalib';
2
+ import { AppVersionCache } from './app-cache';
3
+ import { IGetRouteEvent, IGetRouteResult } from './get-route';
4
+ import Log from './lib/log';
5
+
6
+ const log = Log.Instance;
7
+
8
+ /**
9
+ * Lookup the version of the app to run
10
+ * @param event
11
+ * @param response
12
+ * @param appName
13
+ * @param additionalParts
14
+ * @param log
15
+ * @returns
16
+ */
17
+
18
+ export async function RouteApp(opts: {
19
+ dbManager: DBManager;
20
+ event: IGetRouteEvent;
21
+ appName: string;
22
+ possibleSemVerPathNextData?: string;
23
+ possibleSemVerPathAfterApp?: string;
24
+ possibleSemVerQuery?: string;
25
+ additionalParts: string;
26
+ normalizedPathPrefix?: string;
27
+ appNameOrRootTrailingSlash: string;
28
+ }): Promise<IGetRouteResult> {
29
+ const {
30
+ dbManager,
31
+ event,
32
+ normalizedPathPrefix = '',
33
+ appName,
34
+ possibleSemVerPathNextData,
35
+ possibleSemVerPathAfterApp,
36
+ possibleSemVerQuery,
37
+ additionalParts,
38
+ appNameOrRootTrailingSlash,
39
+ } = opts;
40
+
41
+ let versionInfoToUse: Version | undefined;
42
+
43
+ const appVersionCache = AppVersionCache.GetInstance({ dbManager });
44
+
45
+ // Check if the semver placeholder is actually a defined version
46
+ const possibleSemVerPathAfterAppVersionInfo = possibleSemVerPathAfterApp
47
+ ? await appVersionCache.GetVersionInfo({
48
+ key: { AppName: appName, SemVer: possibleSemVerPathAfterApp },
49
+ })
50
+ : undefined;
51
+ const possibleSemVerPathNextDataVersionInfo = possibleSemVerPathNextData
52
+ ? await appVersionCache.GetVersionInfo({
53
+ key: { AppName: appName, SemVer: possibleSemVerPathNextData },
54
+ })
55
+ : undefined;
56
+ const possibleSemVerQueryVersionInfo = possibleSemVerQuery
57
+ ? await appVersionCache.GetVersionInfo({
58
+ key: { AppName: appName, SemVer: possibleSemVerQuery },
59
+ })
60
+ : undefined;
61
+
62
+ // If there is a version in the path, use it
63
+ const possibleSemVerPathVersionInfo =
64
+ possibleSemVerPathAfterAppVersionInfo || possibleSemVerPathNextDataVersionInfo;
65
+ if (possibleSemVerPathVersionInfo) {
66
+ //
67
+ // If this is an iframe app then it's a startup request
68
+ // If this is a direct app then it's a regular request to send to the app
69
+ //
70
+ // This is a version, and it's in the path already, route the request to it
71
+ // without creating iframe
72
+ return {
73
+ statusCode: 200,
74
+ appName,
75
+ semVer: possibleSemVerPathVersionInfo.SemVer,
76
+ isAPIPath: additionalParts.startsWith('api/'),
77
+ ...(possibleSemVerPathVersionInfo?.URL ? { url: possibleSemVerPathVersionInfo?.URL } : {}),
78
+ ...(possibleSemVerPathVersionInfo?.Type
79
+ ? {
80
+ type:
81
+ possibleSemVerPathVersionInfo?.Type === 'lambda'
82
+ ? 'apigwy'
83
+ : possibleSemVerPathVersionInfo?.Type,
84
+ }
85
+ : {}),
86
+ };
87
+ } else if (possibleSemVerQueryVersionInfo) {
88
+ // We got a version for the query string, but it's not in the path,
89
+ // so fall back to normal routing (create an iframe or direct route)
90
+ versionInfoToUse = possibleSemVerQueryVersionInfo;
91
+ } else if (possibleSemVerQuery) {
92
+ // We got a version in the query string but it does not exist
93
+ // This needs to 404 as this is a very specific request for a specific version
94
+ log.error(`could not find app ${appName}, for path ${event.rawPath} - returning 404`, {
95
+ statusCode: 404,
96
+ });
97
+
98
+ return {
99
+ statusCode: 404,
100
+ errorMessage: `Router - Could not find app: ${event.rawPath}, ${appName}`,
101
+ };
102
+ } else {
103
+ //
104
+ // TODO: Get the incoming attributes of user
105
+ // For logged in users, these can be: department, product type,
106
+ // employee, office, division, etc.
107
+ // For anonymous users this can be: geo region, language,
108
+ // browser, IP, CIDR, ASIN, etc.
109
+ //
110
+ // The Rules can be either a version or a distribution of versions,
111
+ // including default, for example:
112
+ // 80% to 1.1.0, 20% to default (1.0.3)
113
+ //
114
+ const rules = await appVersionCache.GetRules({ key: { AppName: appName } });
115
+ const defaultVersion = rules?.RuleSet.default?.SemVer;
116
+
117
+ if (defaultVersion == null) {
118
+ log.error(`could not find app ${appName}, for path ${event.rawPath} - returning 404`, {
119
+ statusCode: 404,
120
+ });
121
+
122
+ return {
123
+ statusCode: 404,
124
+ errorMessage: `Router - Could not find app: ${event.rawPath}, ${appName}`,
125
+ };
126
+ }
127
+
128
+ const defaultVersionInfo = await appVersionCache.GetVersionInfo({
129
+ key: { AppName: appName, SemVer: defaultVersion },
130
+ });
131
+
132
+ versionInfoToUse = defaultVersionInfo;
133
+ }
134
+
135
+ if (!versionInfoToUse) {
136
+ log.error(
137
+ `could not find version info for app ${appName}, for path ${event.rawPath} - returning 404`,
138
+ {
139
+ statusCode: 404,
140
+ },
141
+ );
142
+
143
+ return {
144
+ statusCode: 404,
145
+ errorMessage: `Router - Could not find version info for app: ${event.rawPath}, ${appName}`,
146
+ };
147
+ }
148
+
149
+ if (versionInfoToUse?.StartupType === 'iframe' || !versionInfoToUse?.StartupType) {
150
+ // Prepare the iframe contents
151
+ let appVersionPath: string;
152
+ if (
153
+ // versionInfoToUse?.Type !== 'static' &&
154
+ versionInfoToUse?.DefaultFile === undefined ||
155
+ versionInfoToUse?.DefaultFile === '' ||
156
+ additionalParts !== ''
157
+ ) {
158
+ // KLUDGE: We're going to take a missing default file to mean that the
159
+ // app type is Next.js (or similar) and that it wants no trailing slash after the version
160
+ // TODO: Move this to an attribute of the version
161
+ appVersionPath = `${normalizedPathPrefix}/${appNameOrRootTrailingSlash}${versionInfoToUse.SemVer}`;
162
+ if (additionalParts !== '') {
163
+ appVersionPath += `/${additionalParts}`;
164
+ }
165
+ } else {
166
+ // Linking to the file directly means this will be peeled off by the S3 route
167
+ // That means we won't have to proxy this from S3
168
+ appVersionPath = `${normalizedPathPrefix}/${appNameOrRootTrailingSlash}${versionInfoToUse.SemVer}/${versionInfoToUse.DefaultFile}`;
169
+ }
170
+
171
+ return {
172
+ statusCode: 200,
173
+ appName,
174
+ semVer: versionInfoToUse.SemVer,
175
+ startupType: 'iframe',
176
+ isAPIPath: additionalParts.startsWith('api/'),
177
+ ...(versionInfoToUse?.URL ? { url: versionInfoToUse?.URL } : {}),
178
+ ...(versionInfoToUse?.Type
179
+ ? { type: versionInfoToUse?.Type === 'lambda' ? 'apigwy' : versionInfoToUse?.Type }
180
+ : {}),
181
+ iFrameAppVersionPath: appVersionPath,
182
+ };
183
+ } else {
184
+ // This is a direct app version, no iframe needed
185
+ if (versionInfoToUse?.Type === 'lambda') {
186
+ throw new Error('Invalid type for direct app version');
187
+ }
188
+ if (['apigwy', 'static'].includes(versionInfoToUse?.Type || '')) {
189
+ throw new Error('Invalid type for direct app version');
190
+ }
191
+
192
+ return {
193
+ statusCode: 200,
194
+ appName,
195
+ semVer: versionInfoToUse.SemVer,
196
+ startupType: 'direct',
197
+ isAPIPath: additionalParts.startsWith('api/'),
198
+ ...(versionInfoToUse?.URL ? { url: versionInfoToUse?.URL } : {}),
199
+ ...(versionInfoToUse?.Type ? { type: versionInfoToUse?.Type } : {}),
200
+ };
201
+ }
202
+ }