@pwrdrvr/microapps-router-lib 0.4.0-alpha.8
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/LICENSE +21 -0
- package/package.json +44 -0
- package/src/index.prefix.spec.ts +93 -0
- package/src/index.spec.ts +318 -0
- package/src/index.ts +536 -0
- package/src/lib/log.ts +8 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 PwrDrvr LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pwrdrvr/microapps-router-lib",
|
|
3
|
+
"version": "0.4.0-alpha.8",
|
|
4
|
+
"description": "Router library for the microapps framework",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/pwrdrvr/microapps-core.git"
|
|
13
|
+
},
|
|
14
|
+
"author": "PwrDrvr LLC",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/pwrdrvr/microapps-core/issues"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/pwrdrvr/microapps-core#readme",
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"@aws-sdk/client-dynamodb": "^3.0.0",
|
|
22
|
+
"fs-extra": "^9.0.0",
|
|
23
|
+
"lambda-log": "^3.0.0",
|
|
24
|
+
"source-map-support": "^0.5.0",
|
|
25
|
+
"tslib": "^2.0.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@aws-sdk/client-dynamodb": "3.78.0",
|
|
29
|
+
"@types/aws-lambda": "8.10.110",
|
|
30
|
+
"@types/lambda-log": "^2.2.0",
|
|
31
|
+
"@types/node": "16.9.2",
|
|
32
|
+
"@types/source-map-support": "0.5.6",
|
|
33
|
+
"fs-extra": "^9.1.0",
|
|
34
|
+
"lambda-log": "^3.1.0",
|
|
35
|
+
"source-map-support": "0.5.21",
|
|
36
|
+
"tslib": "^2.1.0"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"src",
|
|
41
|
+
"package.json",
|
|
42
|
+
"LICENSE"
|
|
43
|
+
]
|
|
44
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/// <reference types="jest" />
|
|
2
|
+
import 'reflect-metadata';
|
|
3
|
+
import 'jest-dynalite/withDb';
|
|
4
|
+
import * as dynamodb from '@aws-sdk/client-dynamodb';
|
|
5
|
+
import { Application, DBManager, Version, Rules } from '@pwrdrvr/microapps-datalib';
|
|
6
|
+
import { GetRoute } from './index';
|
|
7
|
+
|
|
8
|
+
let dynamoClient: dynamodb.DynamoDBClient;
|
|
9
|
+
let dbManager: DBManager;
|
|
10
|
+
|
|
11
|
+
const TEST_TABLE_NAME = 'microapps';
|
|
12
|
+
|
|
13
|
+
describe('router - with prefix', () => {
|
|
14
|
+
beforeAll(() => {
|
|
15
|
+
dynamoClient = new dynamodb.DynamoDBClient({
|
|
16
|
+
endpoint: process.env.MOCK_DYNAMODB_ENDPOINT,
|
|
17
|
+
tls: false,
|
|
18
|
+
region: 'local',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Init the DB manager to point it at the right table
|
|
22
|
+
dbManager = new DBManager({ dynamoClient, tableName: TEST_TABLE_NAME });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should serve appframe with version and default file substitued', async () => {
|
|
26
|
+
const app = new Application({
|
|
27
|
+
AppName: 'Bat',
|
|
28
|
+
DisplayName: 'Bat App',
|
|
29
|
+
});
|
|
30
|
+
await app.Save(dbManager);
|
|
31
|
+
|
|
32
|
+
const version = new Version({
|
|
33
|
+
AppName: 'Bat',
|
|
34
|
+
DefaultFile: 'bat.html',
|
|
35
|
+
IntegrationID: 'abcd',
|
|
36
|
+
SemVer: '3.2.1-beta.1',
|
|
37
|
+
Status: 'deployed',
|
|
38
|
+
Type: 'lambda',
|
|
39
|
+
});
|
|
40
|
+
await version.Save(dbManager);
|
|
41
|
+
|
|
42
|
+
const rules = new Rules({
|
|
43
|
+
AppName: 'Bat',
|
|
44
|
+
Version: 0,
|
|
45
|
+
RuleSet: { default: { SemVer: '3.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
|
|
46
|
+
});
|
|
47
|
+
await rules.Save(dbManager);
|
|
48
|
+
|
|
49
|
+
// Call the handler
|
|
50
|
+
const response = await GetRoute({
|
|
51
|
+
dbManager,
|
|
52
|
+
normalizedPathPrefix: '/qa',
|
|
53
|
+
rawPath: '/qa/bat/',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(response).toHaveProperty('statusCode');
|
|
57
|
+
expect(response.statusCode).toBe(200);
|
|
58
|
+
expect(response).toBeDefined();
|
|
59
|
+
expect(response.iFrameAppVersionPath).toBe('/qa/bat/3.2.1-beta.1/bat.html');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should 404 appframe with version if the prefix is missing', async () => {
|
|
63
|
+
const app = new Application({
|
|
64
|
+
AppName: 'Bat',
|
|
65
|
+
DisplayName: 'Bat App',
|
|
66
|
+
});
|
|
67
|
+
await app.Save(dbManager);
|
|
68
|
+
|
|
69
|
+
const version = new Version({
|
|
70
|
+
AppName: 'Bat',
|
|
71
|
+
DefaultFile: 'bat.html',
|
|
72
|
+
IntegrationID: 'abcd',
|
|
73
|
+
SemVer: '3.2.1-beta.1',
|
|
74
|
+
Status: 'deployed',
|
|
75
|
+
Type: 'lambda',
|
|
76
|
+
});
|
|
77
|
+
await version.Save(dbManager);
|
|
78
|
+
|
|
79
|
+
const rules = new Rules({
|
|
80
|
+
AppName: 'Bat',
|
|
81
|
+
Version: 0,
|
|
82
|
+
RuleSet: { default: { SemVer: '3.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
|
|
83
|
+
});
|
|
84
|
+
await rules.Save(dbManager);
|
|
85
|
+
|
|
86
|
+
// Call the handler
|
|
87
|
+
const response = await GetRoute({ dbManager, normalizedPathPrefix: '/qa', rawPath: '/bat/' });
|
|
88
|
+
|
|
89
|
+
expect(response).toHaveProperty('statusCode');
|
|
90
|
+
expect(response.statusCode).toBe(404);
|
|
91
|
+
expect(response.errorMessage).toBe('Request not routable');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/// <reference types="jest" />
|
|
2
|
+
import 'jest-dynalite/withDb';
|
|
3
|
+
import * as dynamodb from '@aws-sdk/client-dynamodb';
|
|
4
|
+
import { Application, DBManager, Version, Rules } from '@pwrdrvr/microapps-datalib';
|
|
5
|
+
import { GetRoute } from './index';
|
|
6
|
+
|
|
7
|
+
let dynamoClient: dynamodb.DynamoDBClient;
|
|
8
|
+
let dbManager: DBManager;
|
|
9
|
+
|
|
10
|
+
const TEST_TABLE_NAME = 'microapps';
|
|
11
|
+
|
|
12
|
+
describe('router - without prefix', () => {
|
|
13
|
+
beforeAll(() => {
|
|
14
|
+
dynamoClient = new dynamodb.DynamoDBClient({
|
|
15
|
+
endpoint: process.env.MOCK_DYNAMODB_ENDPOINT,
|
|
16
|
+
tls: false,
|
|
17
|
+
region: 'local',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Init the DB manager to point it at the right table
|
|
21
|
+
dbManager = new DBManager({ dynamoClient, tableName: TEST_TABLE_NAME });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should serve appframe with version and default file substitued', async () => {
|
|
25
|
+
const app = new Application({
|
|
26
|
+
AppName: 'Bat',
|
|
27
|
+
DisplayName: 'Bat App',
|
|
28
|
+
});
|
|
29
|
+
await app.Save(dbManager);
|
|
30
|
+
|
|
31
|
+
const version = new Version({
|
|
32
|
+
AppName: 'Bat',
|
|
33
|
+
DefaultFile: 'bat.html',
|
|
34
|
+
IntegrationID: 'abcd',
|
|
35
|
+
SemVer: '3.2.1-beta.1',
|
|
36
|
+
Status: 'deployed',
|
|
37
|
+
Type: 'lambda',
|
|
38
|
+
});
|
|
39
|
+
await version.Save(dbManager);
|
|
40
|
+
|
|
41
|
+
const rules = new Rules({
|
|
42
|
+
AppName: 'Bat',
|
|
43
|
+
Version: 0,
|
|
44
|
+
RuleSet: { default: { SemVer: '3.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
|
|
45
|
+
});
|
|
46
|
+
await rules.Save(dbManager);
|
|
47
|
+
|
|
48
|
+
// Call the handler
|
|
49
|
+
const response = await GetRoute({ dbManager, rawPath: '/bat/' });
|
|
50
|
+
|
|
51
|
+
expect(response).toHaveProperty('statusCode');
|
|
52
|
+
expect(response.statusCode).toBe(200);
|
|
53
|
+
expect(response).toBeDefined();
|
|
54
|
+
expect(response.iFrameAppVersionPath).toBe('/bat/3.2.1-beta.1/bat.html');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('static app - request to app/x.y.z should redirect to defaultFile', async () => {
|
|
58
|
+
const app = new Application({
|
|
59
|
+
AppName: 'Bat',
|
|
60
|
+
DisplayName: 'Bat App',
|
|
61
|
+
});
|
|
62
|
+
await app.Save(dbManager);
|
|
63
|
+
|
|
64
|
+
const version = new Version({
|
|
65
|
+
AppName: 'Bat',
|
|
66
|
+
DefaultFile: 'bat.html',
|
|
67
|
+
IntegrationID: 'abcd',
|
|
68
|
+
SemVer: '3.2.1-beta.1',
|
|
69
|
+
Status: 'deployed',
|
|
70
|
+
Type: 'static',
|
|
71
|
+
});
|
|
72
|
+
await version.Save(dbManager);
|
|
73
|
+
|
|
74
|
+
const rules = new Rules({
|
|
75
|
+
AppName: 'Bat',
|
|
76
|
+
Version: 0,
|
|
77
|
+
RuleSet: { default: { SemVer: '3.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
|
|
78
|
+
});
|
|
79
|
+
await rules.Save(dbManager);
|
|
80
|
+
|
|
81
|
+
// Call the handler
|
|
82
|
+
const response = await GetRoute({ dbManager, rawPath: '/bat/3.2.1-beta.1' });
|
|
83
|
+
|
|
84
|
+
expect(response).toHaveProperty('statusCode');
|
|
85
|
+
expect(response.statusCode).toBe(302);
|
|
86
|
+
expect(response.redirectLocation).toBeDefined();
|
|
87
|
+
expect(response.redirectLocation).toBe('/bat/3.2.1-beta.1/bat.html');
|
|
88
|
+
}, 60000);
|
|
89
|
+
|
|
90
|
+
it('static app - request to app/x.y.z/ should not redirect if no defaultFile', async () => {
|
|
91
|
+
const app = new Application({
|
|
92
|
+
AppName: 'Bat',
|
|
93
|
+
DisplayName: 'Bat App',
|
|
94
|
+
});
|
|
95
|
+
await app.Save(dbManager);
|
|
96
|
+
|
|
97
|
+
const version = new Version({
|
|
98
|
+
AppName: 'Bat',
|
|
99
|
+
IntegrationID: 'abcd',
|
|
100
|
+
SemVer: '3.2.1-beta.1',
|
|
101
|
+
Status: 'deployed',
|
|
102
|
+
Type: 'static',
|
|
103
|
+
});
|
|
104
|
+
await version.Save(dbManager);
|
|
105
|
+
|
|
106
|
+
const rules = new Rules({
|
|
107
|
+
AppName: 'Bat',
|
|
108
|
+
Version: 0,
|
|
109
|
+
RuleSet: { default: { SemVer: '3.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
|
|
110
|
+
});
|
|
111
|
+
await rules.Save(dbManager);
|
|
112
|
+
|
|
113
|
+
// Call the handler
|
|
114
|
+
const response = await GetRoute({ dbManager, rawPath: '/bat/3.2.1-beta.1/' });
|
|
115
|
+
|
|
116
|
+
expect(response).toHaveProperty('appName');
|
|
117
|
+
expect(response.appName).toBe('bat');
|
|
118
|
+
expect(response).toHaveProperty('semVer');
|
|
119
|
+
expect(response.semVer).toBe('3.2.1-beta.1');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('static app - request to app/x.y.z/ should redirect to defaultFile', async () => {
|
|
123
|
+
const app = new Application({
|
|
124
|
+
AppName: 'Bat',
|
|
125
|
+
DisplayName: 'Bat App',
|
|
126
|
+
});
|
|
127
|
+
await app.Save(dbManager);
|
|
128
|
+
|
|
129
|
+
const version = new Version({
|
|
130
|
+
AppName: 'Bat',
|
|
131
|
+
DefaultFile: 'bat.html',
|
|
132
|
+
IntegrationID: 'abcd',
|
|
133
|
+
SemVer: '3.2.1-beta.1',
|
|
134
|
+
Status: 'deployed',
|
|
135
|
+
Type: 'static',
|
|
136
|
+
});
|
|
137
|
+
await version.Save(dbManager);
|
|
138
|
+
|
|
139
|
+
const rules = new Rules({
|
|
140
|
+
AppName: 'Bat',
|
|
141
|
+
Version: 0,
|
|
142
|
+
RuleSet: { default: { SemVer: '3.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
|
|
143
|
+
});
|
|
144
|
+
await rules.Save(dbManager);
|
|
145
|
+
|
|
146
|
+
// Call the handler
|
|
147
|
+
const response = await GetRoute({ dbManager, rawPath: '/bat/3.2.1-beta.1/' });
|
|
148
|
+
|
|
149
|
+
expect(response).toHaveProperty('statusCode');
|
|
150
|
+
expect(response.statusCode).toBe(302);
|
|
151
|
+
expect(response.redirectLocation).toBeDefined();
|
|
152
|
+
expect(response.redirectLocation).toBe('/bat/3.2.1-beta.1/bat.html');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('static app - request to app/notVersion should load app frame with defaultFile', async () => {
|
|
156
|
+
const app = new Application({
|
|
157
|
+
AppName: 'Bat',
|
|
158
|
+
DisplayName: 'Bat App',
|
|
159
|
+
});
|
|
160
|
+
await app.Save(dbManager);
|
|
161
|
+
|
|
162
|
+
const version = new Version({
|
|
163
|
+
AppName: 'Bat',
|
|
164
|
+
DefaultFile: 'bat.html',
|
|
165
|
+
IntegrationID: 'abcd',
|
|
166
|
+
SemVer: '3.2.1-beta.1',
|
|
167
|
+
Status: 'deployed',
|
|
168
|
+
Type: 'static',
|
|
169
|
+
});
|
|
170
|
+
await version.Save(dbManager);
|
|
171
|
+
|
|
172
|
+
const rules = new Rules({
|
|
173
|
+
AppName: 'Bat',
|
|
174
|
+
Version: 0,
|
|
175
|
+
RuleSet: { default: { SemVer: '3.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
|
|
176
|
+
});
|
|
177
|
+
await rules.Save(dbManager);
|
|
178
|
+
|
|
179
|
+
// Call the handler
|
|
180
|
+
const response = await GetRoute({ dbManager, rawPath: '/bat/notVersion' });
|
|
181
|
+
|
|
182
|
+
expect(response).toHaveProperty('statusCode');
|
|
183
|
+
expect(response.statusCode).toBe(200);
|
|
184
|
+
expect(response).toBeDefined();
|
|
185
|
+
expect(response.iFrameAppVersionPath).toBe('/bat/3.2.1-beta.1/bat.html');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should serve appframe with no default file', async () => {
|
|
189
|
+
const app = new Application({
|
|
190
|
+
AppName: 'Bat',
|
|
191
|
+
DisplayName: 'Bat App',
|
|
192
|
+
});
|
|
193
|
+
await app.Save(dbManager);
|
|
194
|
+
|
|
195
|
+
const version = new Version({
|
|
196
|
+
AppName: 'Bat',
|
|
197
|
+
DefaultFile: '',
|
|
198
|
+
IntegrationID: 'abcd',
|
|
199
|
+
SemVer: '3.2.1-beta1',
|
|
200
|
+
Status: 'deployed',
|
|
201
|
+
Type: 'lambda',
|
|
202
|
+
});
|
|
203
|
+
await version.Save(dbManager);
|
|
204
|
+
|
|
205
|
+
const rules = new Rules({
|
|
206
|
+
AppName: 'Bat',
|
|
207
|
+
Version: 0,
|
|
208
|
+
RuleSet: { default: { SemVer: '3.2.1-beta1', AttributeName: '', AttributeValue: '' } },
|
|
209
|
+
});
|
|
210
|
+
await rules.Save(dbManager);
|
|
211
|
+
|
|
212
|
+
// Call the handler
|
|
213
|
+
const response = await GetRoute({ dbManager, rawPath: '/bat/' });
|
|
214
|
+
|
|
215
|
+
expect(response).toBeDefined();
|
|
216
|
+
expect(response).toHaveProperty('statusCode');
|
|
217
|
+
expect(response.statusCode).toBe(200);
|
|
218
|
+
expect(response.iFrameAppVersionPath).toBe('/bat/3.2.1-beta1');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should serve appframe with sub-route', async () => {
|
|
222
|
+
const app = new Application({
|
|
223
|
+
AppName: 'Bat',
|
|
224
|
+
DisplayName: 'Bat App',
|
|
225
|
+
});
|
|
226
|
+
await app.Save(dbManager);
|
|
227
|
+
|
|
228
|
+
const version = new Version({
|
|
229
|
+
AppName: 'Bat',
|
|
230
|
+
DefaultFile: '',
|
|
231
|
+
IntegrationID: 'abcd',
|
|
232
|
+
SemVer: '3.2.1-beta2',
|
|
233
|
+
Status: 'deployed',
|
|
234
|
+
Type: 'lambda',
|
|
235
|
+
});
|
|
236
|
+
await version.Save(dbManager);
|
|
237
|
+
|
|
238
|
+
const rules = new Rules({
|
|
239
|
+
AppName: 'Bat',
|
|
240
|
+
Version: 0,
|
|
241
|
+
RuleSet: { default: { SemVer: '3.2.1-beta2', AttributeName: '', AttributeValue: '' } },
|
|
242
|
+
});
|
|
243
|
+
await rules.Save(dbManager);
|
|
244
|
+
|
|
245
|
+
// Call the handler
|
|
246
|
+
const response = await GetRoute({ dbManager, rawPath: '/bat/demo/grid' });
|
|
247
|
+
|
|
248
|
+
expect(response).toBeDefined();
|
|
249
|
+
expect(response).toHaveProperty('statusCode');
|
|
250
|
+
expect(response.statusCode).toBe(200);
|
|
251
|
+
expect(response.iFrameAppVersionPath).toBe('/bat/3.2.1-beta2/demo/grid');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should serve appframe with sub-route', async () => {
|
|
255
|
+
const app = new Application({
|
|
256
|
+
AppName: 'Bat',
|
|
257
|
+
DisplayName: 'Bat App',
|
|
258
|
+
});
|
|
259
|
+
await app.Save(dbManager);
|
|
260
|
+
|
|
261
|
+
const version = new Version({
|
|
262
|
+
AppName: 'Bat',
|
|
263
|
+
DefaultFile: 'someFile.html',
|
|
264
|
+
IntegrationID: 'abcd',
|
|
265
|
+
SemVer: '3.2.1-beta3',
|
|
266
|
+
Status: 'deployed',
|
|
267
|
+
Type: 'lambda',
|
|
268
|
+
});
|
|
269
|
+
await version.Save(dbManager);
|
|
270
|
+
|
|
271
|
+
const rules = new Rules({
|
|
272
|
+
AppName: 'Bat',
|
|
273
|
+
Version: 0,
|
|
274
|
+
RuleSet: { default: { SemVer: '3.2.1-beta3', AttributeName: '', AttributeValue: '' } },
|
|
275
|
+
});
|
|
276
|
+
await rules.Save(dbManager);
|
|
277
|
+
|
|
278
|
+
// Call the handler
|
|
279
|
+
const response = await GetRoute({ dbManager, rawPath: '/bat/demo' });
|
|
280
|
+
|
|
281
|
+
expect(response).toBeDefined();
|
|
282
|
+
expect(response).toHaveProperty('statusCode');
|
|
283
|
+
expect(response.statusCode).toBe(200);
|
|
284
|
+
expect(response.iFrameAppVersionPath).toBe('/bat/3.2.1-beta3/demo');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('should return 404 for /favicon.ico', async () => {
|
|
288
|
+
const app = new Application({
|
|
289
|
+
AppName: 'Bat',
|
|
290
|
+
DisplayName: 'Bat App',
|
|
291
|
+
});
|
|
292
|
+
await app.Save(dbManager);
|
|
293
|
+
|
|
294
|
+
const version = new Version({
|
|
295
|
+
AppName: 'Bat',
|
|
296
|
+
DefaultFile: 'someFile.html',
|
|
297
|
+
IntegrationID: 'abcd',
|
|
298
|
+
SemVer: '3.2.1-beta3',
|
|
299
|
+
Status: 'deployed',
|
|
300
|
+
Type: 'lambda',
|
|
301
|
+
});
|
|
302
|
+
await version.Save(dbManager);
|
|
303
|
+
|
|
304
|
+
const rules = new Rules({
|
|
305
|
+
AppName: 'Bat',
|
|
306
|
+
Version: 0,
|
|
307
|
+
RuleSet: { default: { SemVer: '3.2.1-beta3', AttributeName: '', AttributeValue: '' } },
|
|
308
|
+
});
|
|
309
|
+
await rules.Save(dbManager);
|
|
310
|
+
|
|
311
|
+
// Call the handler
|
|
312
|
+
const response = await GetRoute({ dbManager, rawPath: '/favicon.ico' });
|
|
313
|
+
|
|
314
|
+
expect(response).toBeDefined();
|
|
315
|
+
expect(response).toHaveProperty('statusCode');
|
|
316
|
+
expect(response.statusCode).toBe(404);
|
|
317
|
+
});
|
|
318
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
// Used by ts-convict
|
|
2
|
+
import 'source-map-support/register';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { pathExistsSync, readFileSync } from 'fs-extra';
|
|
5
|
+
import { Application, DBManager, IVersionsAndRules, Version } from '@pwrdrvr/microapps-datalib';
|
|
6
|
+
import Log from './lib/log';
|
|
7
|
+
|
|
8
|
+
const log = Log.Instance;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Find and load the appFrame file
|
|
12
|
+
* @returns
|
|
13
|
+
*/
|
|
14
|
+
export function loadAppFrame({ basePath = '.' }: { basePath?: string }): string {
|
|
15
|
+
const paths = [
|
|
16
|
+
basePath,
|
|
17
|
+
path.join(basePath, '..'),
|
|
18
|
+
path.join(basePath, 'templates'),
|
|
19
|
+
basePath,
|
|
20
|
+
'/opt',
|
|
21
|
+
'/opt/templates',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
for (const pathRoot of paths) {
|
|
25
|
+
const fullPath = path.join(pathRoot, 'appFrame.html');
|
|
26
|
+
try {
|
|
27
|
+
if (pathExistsSync(fullPath)) {
|
|
28
|
+
log.info('found html file', { fullPath });
|
|
29
|
+
return readFileSync(fullPath, 'utf-8');
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// Don't care - we get here if stat throws because the file does not exist
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
log.error('appFrame.html not found');
|
|
37
|
+
throw new Error('appFrame.html not found');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Ensure that the path starts with a / and does not end with a /
|
|
42
|
+
*
|
|
43
|
+
* @param pathPrefix
|
|
44
|
+
* @returns
|
|
45
|
+
*/
|
|
46
|
+
export function normalizePathPrefix(pathPrefix: string): string {
|
|
47
|
+
let normalizedPathPrefix = pathPrefix;
|
|
48
|
+
if (normalizedPathPrefix !== '' && !normalizedPathPrefix.startsWith('/')) {
|
|
49
|
+
normalizedPathPrefix = '/' + pathPrefix;
|
|
50
|
+
}
|
|
51
|
+
if (normalizedPathPrefix.endsWith('/')) {
|
|
52
|
+
normalizedPathPrefix.substring(0, normalizedPathPrefix.length - 1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return normalizedPathPrefix;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface IGetRouteResult {
|
|
59
|
+
/**
|
|
60
|
+
* HTTP status code for immediate response, immediate redirect, and errors
|
|
61
|
+
*/
|
|
62
|
+
readonly statusCode?: number;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Error message for errors
|
|
66
|
+
*/
|
|
67
|
+
readonly errorMessage?: string;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Location to redirect to
|
|
71
|
+
*/
|
|
72
|
+
readonly redirectLocation?: string;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Optional headers for immediate response, immediate redirect, and errors
|
|
76
|
+
*/
|
|
77
|
+
readonly headers?: Record<string, string>;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
*
|
|
81
|
+
*
|
|
82
|
+
* @example /myapp/1.0.0/index.html
|
|
83
|
+
* @example /myapp/1.0.1
|
|
84
|
+
* @example /myapp/1.0.2/some/path?query=string
|
|
85
|
+
*/
|
|
86
|
+
readonly iFrameAppVersionPath?: string;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Name of the app if resolved
|
|
90
|
+
*/
|
|
91
|
+
readonly appName?: string;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Version of the app if resolved
|
|
95
|
+
*/
|
|
96
|
+
readonly semVer?: string;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Type of the app
|
|
100
|
+
*/
|
|
101
|
+
readonly type?: 'apigwy' | 'lambda-url' | 'url' | 'static';
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Startup type of the app (indirect with iframe or direct)
|
|
105
|
+
*/
|
|
106
|
+
readonly startupType?: 'iframe' | 'direct';
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* URL to the app if resolved
|
|
110
|
+
*/
|
|
111
|
+
readonly url?: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface IGetRouteEvent {
|
|
115
|
+
readonly dbManager: DBManager;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* rawPath from the Lambda event
|
|
119
|
+
*/
|
|
120
|
+
readonly rawPath: string;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Configured prefix of the deployment, must start with a / and not end with a /
|
|
124
|
+
*/
|
|
125
|
+
readonly normalizedPathPrefix?: string;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Query string params, if any
|
|
129
|
+
* Checked for `appver=1.2.3` to override the app version
|
|
130
|
+
*/
|
|
131
|
+
readonly queryStringParameters?: URLSearchParams;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get information about immediate redirect, immediate response,
|
|
136
|
+
* or which host to route the request to.
|
|
137
|
+
*
|
|
138
|
+
* @param event
|
|
139
|
+
*
|
|
140
|
+
* @returns IGetRouteResult
|
|
141
|
+
*/
|
|
142
|
+
export async function GetRoute(event: IGetRouteEvent): Promise<IGetRouteResult> {
|
|
143
|
+
const { dbManager, normalizedPathPrefix = '', queryStringParameters } = event;
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
if (normalizedPathPrefix && !event.rawPath.startsWith(normalizedPathPrefix)) {
|
|
147
|
+
// The prefix is required if configured, if missing we cannot serve this app
|
|
148
|
+
return { statusCode: 404, errorMessage: 'Request not routable' };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const pathAfterPrefix =
|
|
152
|
+
normalizedPathPrefix && event.rawPath.startsWith(normalizedPathPrefix)
|
|
153
|
+
? event.rawPath.slice(normalizedPathPrefix.length - 1)
|
|
154
|
+
: event.rawPath;
|
|
155
|
+
|
|
156
|
+
// /someapp will split into length 2 with ["", "someapp"] as results
|
|
157
|
+
// /someapp/somepath will split into length 3 with ["", "someapp", "somepath"] as results
|
|
158
|
+
// /someapp/somepath/ will split into length 3 with ["", "someapp", "somepath", ""] as results
|
|
159
|
+
// /someapp/somepath/somefile.foo will split into length 4 with ["", "someapp", "somepath", "somefile.foo", ""] as results
|
|
160
|
+
const partsAfterPrefix = pathAfterPrefix.split('/');
|
|
161
|
+
|
|
162
|
+
const { versionsAndRules, appName } = await GetAppInfo({
|
|
163
|
+
dbManager,
|
|
164
|
+
appName: partsAfterPrefix.length >= 2 ? partsAfterPrefix[1] : '[root]',
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const isRootApp = appName === '[root]';
|
|
168
|
+
const appNameOrRootTrailingSlash = isRootApp ? '' : `${appName}/`;
|
|
169
|
+
|
|
170
|
+
// Strip the appName from the start of the path, if there was one
|
|
171
|
+
const pathAfterAppName = isRootApp
|
|
172
|
+
? pathAfterPrefix
|
|
173
|
+
: pathAfterPrefix.slice(appName.length + 1);
|
|
174
|
+
const partsAfterAppName = pathAfterAppName.split('/');
|
|
175
|
+
|
|
176
|
+
// Pass any parts after the appName/Version to the route handler
|
|
177
|
+
let additionalParts = '';
|
|
178
|
+
if (partsAfterAppName.length >= 2 && partsAfterAppName[1] !== '') {
|
|
179
|
+
additionalParts = partsAfterAppName.slice(1).join('/');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Route an app and version (only) to include the defaultFile
|
|
183
|
+
// If the second part is not a version that exists, fall through to
|
|
184
|
+
// routing the app and glomming the rest of the path on to the end
|
|
185
|
+
if (
|
|
186
|
+
partsAfterAppName.length === 2 ||
|
|
187
|
+
(partsAfterAppName.length === 3 && !partsAfterAppName[2])
|
|
188
|
+
) {
|
|
189
|
+
// / semVer /
|
|
190
|
+
// ^ ^^^^^^ ^
|
|
191
|
+
// 0 1 2
|
|
192
|
+
// This is an app and a version only
|
|
193
|
+
// If the request got here it's likely a static app that has no
|
|
194
|
+
// Lambda function (thus the API Gateway route fell through to the Router)
|
|
195
|
+
const response = await RedirectToDefaultFile({
|
|
196
|
+
dbManager,
|
|
197
|
+
appName,
|
|
198
|
+
normalizedPathPrefix,
|
|
199
|
+
semVer: partsAfterAppName[1],
|
|
200
|
+
appNameOrRootTrailingSlash,
|
|
201
|
+
});
|
|
202
|
+
if (response) {
|
|
203
|
+
return response;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Check for a version in the path
|
|
208
|
+
// Examples
|
|
209
|
+
// / semVer / somepath
|
|
210
|
+
// / _next / data / semVer / somepath
|
|
211
|
+
const possibleSemVerPathNextData = partsAfterAppName.length >= 4 ? partsAfterAppName[3] : '';
|
|
212
|
+
const possibleSemVerPathAfterApp = partsAfterAppName.length >= 2 ? partsAfterAppName[1] : '';
|
|
213
|
+
|
|
214
|
+
// (/ something)?
|
|
215
|
+
// ^ ^^^^^^^^^^^^
|
|
216
|
+
// 0 1
|
|
217
|
+
// Got at least an application name, try to route it
|
|
218
|
+
const response = RouteApp({
|
|
219
|
+
dbManager,
|
|
220
|
+
normalizedPathPrefix,
|
|
221
|
+
event,
|
|
222
|
+
appName,
|
|
223
|
+
possibleSemVerPathNextData,
|
|
224
|
+
possibleSemVerPathAfterApp,
|
|
225
|
+
possibleSemVerQuery: queryStringParameters?.get('appver') || '',
|
|
226
|
+
additionalParts,
|
|
227
|
+
versionsAndRules,
|
|
228
|
+
appNameOrRootTrailingSlash,
|
|
229
|
+
});
|
|
230
|
+
if (response) {
|
|
231
|
+
return response;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
statusCode: 599,
|
|
236
|
+
errorMessage: `Router - Could not route: ${event.rawPath}, no matching route`,
|
|
237
|
+
};
|
|
238
|
+
} catch (error: any) {
|
|
239
|
+
log.error('unexpected exception - returning 599', { statusCode: 599, error });
|
|
240
|
+
return {
|
|
241
|
+
statusCode: 599,
|
|
242
|
+
errorMessage: `Router - Could not route: ${event.rawPath}, ${error.message}`,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export interface IAppInfo {
|
|
248
|
+
appName: string;
|
|
249
|
+
versionsAndRules: IVersionsAndRules;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Get info about an app or the root app
|
|
254
|
+
*/
|
|
255
|
+
export async function GetAppInfo(opts: {
|
|
256
|
+
dbManager: DBManager;
|
|
257
|
+
appName: string;
|
|
258
|
+
}): Promise<IAppInfo> {
|
|
259
|
+
const { dbManager, appName } = opts;
|
|
260
|
+
|
|
261
|
+
let versionsAndRules: IVersionsAndRules;
|
|
262
|
+
|
|
263
|
+
versionsAndRules = await Application.GetVersionsAndRules({
|
|
264
|
+
dbManager,
|
|
265
|
+
key: { AppName: appName },
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
if (versionsAndRules.Versions.length > 0) {
|
|
269
|
+
return {
|
|
270
|
+
appName,
|
|
271
|
+
versionsAndRules,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Check if we have a `[root]` app that is a catch all
|
|
276
|
+
versionsAndRules = await Application.GetVersionsAndRules({
|
|
277
|
+
dbManager,
|
|
278
|
+
key: { AppName: '[root]' },
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
appName: '[root]',
|
|
283
|
+
versionsAndRules,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Lookup the version of the app to run
|
|
289
|
+
* @param event
|
|
290
|
+
* @param response
|
|
291
|
+
* @param appName
|
|
292
|
+
* @param additionalParts
|
|
293
|
+
* @param log
|
|
294
|
+
* @returns
|
|
295
|
+
*/
|
|
296
|
+
function RouteApp(opts: {
|
|
297
|
+
dbManager: DBManager;
|
|
298
|
+
event: IGetRouteEvent;
|
|
299
|
+
appName: string;
|
|
300
|
+
possibleSemVerPathNextData?: string;
|
|
301
|
+
possibleSemVerPathAfterApp?: string;
|
|
302
|
+
possibleSemVerQuery?: string;
|
|
303
|
+
additionalParts: string;
|
|
304
|
+
normalizedPathPrefix?: string;
|
|
305
|
+
versionsAndRules: IVersionsAndRules;
|
|
306
|
+
appNameOrRootTrailingSlash: string;
|
|
307
|
+
}): IGetRouteResult {
|
|
308
|
+
const {
|
|
309
|
+
event,
|
|
310
|
+
normalizedPathPrefix = '',
|
|
311
|
+
appName,
|
|
312
|
+
possibleSemVerPathNextData,
|
|
313
|
+
possibleSemVerPathAfterApp,
|
|
314
|
+
possibleSemVerQuery,
|
|
315
|
+
additionalParts,
|
|
316
|
+
versionsAndRules,
|
|
317
|
+
appNameOrRootTrailingSlash,
|
|
318
|
+
} = opts;
|
|
319
|
+
|
|
320
|
+
let versionInfoToUse: Version | undefined;
|
|
321
|
+
|
|
322
|
+
// Check if the semver placeholder is actually a defined version
|
|
323
|
+
const possibleSemVerPathAfterAppVersionInfo = possibleSemVerPathAfterApp
|
|
324
|
+
? versionsAndRules.Versions.find((item) => item.SemVer === possibleSemVerPathAfterApp)
|
|
325
|
+
: undefined;
|
|
326
|
+
const possibleSemVerPathNextDataVersionInfo = possibleSemVerPathNextData
|
|
327
|
+
? versionsAndRules.Versions.find((item) => item.SemVer === possibleSemVerPathNextData)
|
|
328
|
+
: undefined;
|
|
329
|
+
const possibleSemVerQueryVersionInfo = possibleSemVerQuery
|
|
330
|
+
? versionsAndRules.Versions.find((item) => item.SemVer === possibleSemVerQuery)
|
|
331
|
+
: undefined;
|
|
332
|
+
|
|
333
|
+
// If there is a version in the path, use it
|
|
334
|
+
const possibleSemVerPathVersionInfo =
|
|
335
|
+
possibleSemVerPathAfterAppVersionInfo || possibleSemVerPathNextDataVersionInfo;
|
|
336
|
+
if (possibleSemVerPathVersionInfo) {
|
|
337
|
+
// This is a version, and it's in the path already, route the request to it
|
|
338
|
+
// without creating iframe
|
|
339
|
+
return {
|
|
340
|
+
appName,
|
|
341
|
+
semVer: possibleSemVerPathVersionInfo.SemVer,
|
|
342
|
+
...(possibleSemVerPathVersionInfo?.URL ? { url: possibleSemVerPathVersionInfo?.URL } : {}),
|
|
343
|
+
...(possibleSemVerPathVersionInfo?.Type
|
|
344
|
+
? {
|
|
345
|
+
type:
|
|
346
|
+
possibleSemVerPathVersionInfo?.Type === 'lambda'
|
|
347
|
+
? 'apigwy'
|
|
348
|
+
: possibleSemVerPathVersionInfo?.Type,
|
|
349
|
+
}
|
|
350
|
+
: {}),
|
|
351
|
+
};
|
|
352
|
+
} else if (possibleSemVerQueryVersionInfo) {
|
|
353
|
+
// We got a version for the query string, but it's not in the path,
|
|
354
|
+
// so fall back to normal routing (create an iframe or direct route)
|
|
355
|
+
versionInfoToUse = possibleSemVerQueryVersionInfo;
|
|
356
|
+
} else if (possibleSemVerQuery) {
|
|
357
|
+
// We got a version in the query string but it does not exist
|
|
358
|
+
// This needs to 404 as this is a very specific request for a specific version
|
|
359
|
+
log.error(`could not find app ${appName}, for path ${event.rawPath} - returning 404`, {
|
|
360
|
+
statusCode: 404,
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
statusCode: 404,
|
|
365
|
+
errorMessage: `Router - Could not find app: ${event.rawPath}, ${appName}`,
|
|
366
|
+
};
|
|
367
|
+
} else {
|
|
368
|
+
//
|
|
369
|
+
// TODO: Get the incoming attributes of user
|
|
370
|
+
// For logged in users, these can be: department, product type,
|
|
371
|
+
// employee, office, division, etc.
|
|
372
|
+
// For anonymous users this can be: geo region, language,
|
|
373
|
+
// browser, IP, CIDR, ASIN, etc.
|
|
374
|
+
//
|
|
375
|
+
// The Rules can be either a version or a distribution of versions,
|
|
376
|
+
// including default, for example:
|
|
377
|
+
// 80% to 1.1.0, 20% to default (1.0.3)
|
|
378
|
+
//
|
|
379
|
+
|
|
380
|
+
const defaultVersion = versionsAndRules.Rules?.RuleSet.default?.SemVer;
|
|
381
|
+
|
|
382
|
+
if (defaultVersion == null) {
|
|
383
|
+
log.error(`could not find app ${appName}, for path ${event.rawPath} - returning 404`, {
|
|
384
|
+
statusCode: 404,
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
statusCode: 404,
|
|
389
|
+
errorMessage: `Router - Could not find app: ${event.rawPath}, ${appName}`,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// TODO: Yeah, this is lame - We should save these in a dictionary keyed by SemVer
|
|
394
|
+
const defaultVersionInfo = versionsAndRules.Versions.find(
|
|
395
|
+
(item) => item.SemVer === defaultVersion,
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
versionInfoToUse = defaultVersionInfo;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (!versionInfoToUse) {
|
|
402
|
+
log.error(
|
|
403
|
+
`could not find version info for app ${appName}, for path ${event.rawPath} - returning 404`,
|
|
404
|
+
{
|
|
405
|
+
statusCode: 404,
|
|
406
|
+
},
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
statusCode: 404,
|
|
411
|
+
errorMessage: `Router - Could not find version info for app: ${event.rawPath}, ${appName}`,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (versionInfoToUse?.StartupType === 'iframe' || !versionInfoToUse?.StartupType) {
|
|
416
|
+
// Prepare the iframe contents
|
|
417
|
+
let appVersionPath: string;
|
|
418
|
+
if (
|
|
419
|
+
versionInfoToUse?.Type !== 'static' &&
|
|
420
|
+
(versionInfoToUse?.DefaultFile === undefined ||
|
|
421
|
+
versionInfoToUse?.DefaultFile === '' ||
|
|
422
|
+
additionalParts !== '')
|
|
423
|
+
) {
|
|
424
|
+
// KLUDGE: We're going to take a missing default file to mean that the
|
|
425
|
+
// app type is Next.js (or similar) and that it wants no trailing slash after the version
|
|
426
|
+
// TODO: Move this to an attribute of the version
|
|
427
|
+
appVersionPath = `${normalizedPathPrefix}/${appNameOrRootTrailingSlash}${versionInfoToUse.SemVer}`;
|
|
428
|
+
if (additionalParts !== '') {
|
|
429
|
+
appVersionPath += `/${additionalParts}`;
|
|
430
|
+
}
|
|
431
|
+
} else {
|
|
432
|
+
// Linking to the file directly means this will be peeled off by the S3 route
|
|
433
|
+
// That means we won't have to proxy this from S3
|
|
434
|
+
appVersionPath = `${normalizedPathPrefix}/${appNameOrRootTrailingSlash}${versionInfoToUse.SemVer}/${versionInfoToUse.DefaultFile}`;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
statusCode: 200,
|
|
439
|
+
appName,
|
|
440
|
+
semVer: versionInfoToUse.SemVer,
|
|
441
|
+
startupType: 'iframe',
|
|
442
|
+
...(versionInfoToUse?.URL ? { url: versionInfoToUse?.URL } : {}),
|
|
443
|
+
...(versionInfoToUse?.Type
|
|
444
|
+
? { type: versionInfoToUse?.Type === 'lambda' ? 'apigwy' : versionInfoToUse?.Type }
|
|
445
|
+
: {}),
|
|
446
|
+
iFrameAppVersionPath: appVersionPath,
|
|
447
|
+
};
|
|
448
|
+
} else {
|
|
449
|
+
// This is a direct app version, no iframe needed
|
|
450
|
+
|
|
451
|
+
if (versionInfoToUse?.Type === 'lambda') {
|
|
452
|
+
throw new Error('Invalid type for direct app version');
|
|
453
|
+
}
|
|
454
|
+
if (['apigwy', 'static'].includes(versionInfoToUse?.Type || '')) {
|
|
455
|
+
throw new Error('Invalid type for direct app version');
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
appName,
|
|
460
|
+
semVer: versionInfoToUse.SemVer,
|
|
461
|
+
startupType: 'direct',
|
|
462
|
+
...(versionInfoToUse?.URL ? { url: versionInfoToUse?.URL } : {}),
|
|
463
|
+
...(versionInfoToUse?.Type ? { type: versionInfoToUse?.Type } : {}),
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Redirect the request to app/x.y.z/? to app/x.y.z/{defaultFile}
|
|
470
|
+
* @param response
|
|
471
|
+
* @param normalizedPathPrefix
|
|
472
|
+
* @param appName
|
|
473
|
+
* @param semVer
|
|
474
|
+
* @returns
|
|
475
|
+
*/
|
|
476
|
+
async function RedirectToDefaultFile(opts: {
|
|
477
|
+
dbManager: DBManager;
|
|
478
|
+
normalizedPathPrefix?: string;
|
|
479
|
+
appName: string;
|
|
480
|
+
semVer: string;
|
|
481
|
+
appNameOrRootTrailingSlash: string;
|
|
482
|
+
}): Promise<IGetRouteResult | undefined> {
|
|
483
|
+
const {
|
|
484
|
+
dbManager,
|
|
485
|
+
normalizedPathPrefix = '',
|
|
486
|
+
appName,
|
|
487
|
+
appNameOrRootTrailingSlash,
|
|
488
|
+
semVer,
|
|
489
|
+
} = opts;
|
|
490
|
+
let versionInfo: Version;
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
versionInfo = await Version.LoadVersion({
|
|
494
|
+
dbManager,
|
|
495
|
+
key: { AppName: appName, SemVer: semVer },
|
|
496
|
+
});
|
|
497
|
+
} catch (error) {
|
|
498
|
+
log.info(
|
|
499
|
+
`LoadVersion threw for '${normalizedPathPrefix}/${appNameOrRootTrailingSlash}${semVer}' - falling through to app routing'`,
|
|
500
|
+
{
|
|
501
|
+
appName,
|
|
502
|
+
semVer,
|
|
503
|
+
error,
|
|
504
|
+
},
|
|
505
|
+
);
|
|
506
|
+
return undefined;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (versionInfo === undefined) {
|
|
510
|
+
log.info(
|
|
511
|
+
`LoadVersion returned undefined for '${normalizedPathPrefix}/${appNameOrRootTrailingSlash}${semVer}', assuming not found - falling through to app routing'`,
|
|
512
|
+
{
|
|
513
|
+
appName,
|
|
514
|
+
semVer,
|
|
515
|
+
},
|
|
516
|
+
);
|
|
517
|
+
return undefined;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (!versionInfo.DefaultFile) {
|
|
521
|
+
return undefined;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
log.info(
|
|
525
|
+
`found '${normalizedPathPrefix}/${appNameOrRootTrailingSlash}${semVer}' - returning 302 to ${normalizedPathPrefix}/${appNameOrRootTrailingSlash}${semVer}/${versionInfo.DefaultFile}`,
|
|
526
|
+
{
|
|
527
|
+
statusCode: 302,
|
|
528
|
+
routedPath: `${normalizedPathPrefix}/${appNameOrRootTrailingSlash}${semVer}/${versionInfo.DefaultFile}`,
|
|
529
|
+
},
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
statusCode: 302,
|
|
534
|
+
redirectLocation: `${normalizedPathPrefix}/${appNameOrRootTrailingSlash}${semVer}/${versionInfo.DefaultFile}`,
|
|
535
|
+
};
|
|
536
|
+
}
|