@foxtware/mineral 0.1.3 → 0.1.4
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/.hosting.yml.sample +6 -2
- package/_deploy_scripts/deployFromHostingYml.js +23 -24
- package/_deploy_scripts/generateHosted.js +17 -4
- package/bin/mineral.js +11 -0
- package/hosting.utils.js +55 -19
- package/package.json +1 -1
- package/server.utils.js +15 -0
package/.hosting.yml.sample
CHANGED
|
@@ -4,19 +4,23 @@ google_cloud_info:
|
|
|
4
4
|
|
|
5
5
|
# TODO: consider credsPayload in google_cloud_info instead of workspace .creds.yml
|
|
6
6
|
|
|
7
|
+
env:
|
|
8
|
+
- HOSTED_API_KEY
|
|
9
|
+
|
|
7
10
|
functions:
|
|
8
11
|
exampleFunction:
|
|
9
12
|
max_instances: 1
|
|
10
13
|
timeout: 300s
|
|
11
14
|
wrappers:
|
|
12
|
-
- requireHostedApiKey
|
|
15
|
+
- '@foxtware/mineral/server.utils#requireHostedApiKey'
|
|
16
|
+
- '@foxtware/mineral/server.utils#allowCrossOriginCallsAndHandleOptions'
|
|
13
17
|
# entry_point: otherExportName
|
|
14
18
|
|
|
15
19
|
packagedFunction:
|
|
16
20
|
source: '@foxtware/mineral/api/pokemon/pokemonPokeballThrow.js'
|
|
17
21
|
max_instances: 1
|
|
18
22
|
wrappers:
|
|
19
|
-
- checkTrainer
|
|
23
|
+
- '@foxtware/mineral/server.utils#checkTrainer'
|
|
20
24
|
|
|
21
25
|
groups:
|
|
22
26
|
example_group:
|
|
@@ -2,7 +2,13 @@ const readline = require('readline');
|
|
|
2
2
|
const { toAbsolutePath, setWorkspace } = require('../api/workspace');
|
|
3
3
|
const { getApiDirs, readCliFlag } = require('../cli');
|
|
4
4
|
const { loadHandlers } = require('../server');
|
|
5
|
-
const {
|
|
5
|
+
const {
|
|
6
|
+
readHostingYml,
|
|
7
|
+
getCredsJsonForDeploy,
|
|
8
|
+
validateEnvVarsForDeploy,
|
|
9
|
+
resolveHostedHandlersForDeploy,
|
|
10
|
+
functionUsesWrapper,
|
|
11
|
+
} = require('../hosting.utils');
|
|
6
12
|
const { writeHostedJs } = require('./generateHosted');
|
|
7
13
|
const { execCommand } = require('./execCommand');
|
|
8
14
|
const { formatSetEnvVarsForGcloud, shellQuoteSingle } = require('./setEnvVarsGcloud');
|
|
@@ -44,10 +50,6 @@ const chooseOption = async (prompt, options) => new Promise((resolve) => {
|
|
|
44
50
|
});
|
|
45
51
|
});
|
|
46
52
|
|
|
47
|
-
const anyHostedEntryUsesWrapper = (hostedEntries, wrapperName) => (
|
|
48
|
-
hostedEntries.some((hostedEntry) => hostedEntry.wrappers?.includes(wrapperName))
|
|
49
|
-
);
|
|
50
|
-
|
|
51
53
|
const getDeployArgs = () => {
|
|
52
54
|
const args = process.argv.slice(2);
|
|
53
55
|
const deployArgs = [];
|
|
@@ -55,6 +57,10 @@ const getDeployArgs = () => {
|
|
|
55
57
|
for (let index = 0; index < args.length; index++) {
|
|
56
58
|
const arg = args[index];
|
|
57
59
|
|
|
60
|
+
if (arg === 'host') {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
58
64
|
if (arg === '--workspace' || arg === '--api_dirs') {
|
|
59
65
|
index++;
|
|
60
66
|
continue;
|
|
@@ -86,7 +92,7 @@ const deployFunction = async ({
|
|
|
86
92
|
googleCloudInfo,
|
|
87
93
|
workspace,
|
|
88
94
|
credsJson,
|
|
89
|
-
|
|
95
|
+
deployEnvVars = {},
|
|
90
96
|
}) => {
|
|
91
97
|
const config = {
|
|
92
98
|
...googleCloudInfo,
|
|
@@ -113,12 +119,9 @@ const deployFunction = async ({
|
|
|
113
119
|
const envParts = [
|
|
114
120
|
'HOSTED=true',
|
|
115
121
|
`CREDS=${ credsJson }`,
|
|
122
|
+
...Object.entries(deployEnvVars).map(([envName, envValue]) => `${ envName }=${ envValue }`),
|
|
116
123
|
];
|
|
117
124
|
|
|
118
|
-
if (hostedApiKey) {
|
|
119
|
-
envParts.push(`HOSTED_API_KEY=${ hostedApiKey }`);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
125
|
if (extraSetEnvVars) {
|
|
123
126
|
envParts.push(extraSetEnvVars);
|
|
124
127
|
}
|
|
@@ -154,6 +157,7 @@ const deployFunction = async ({
|
|
|
154
157
|
}
|
|
155
158
|
|
|
156
159
|
const requiresHostedApiKey = functionUsesWrapper(functionConfig, 'requireHostedApiKey');
|
|
160
|
+
const hostedApiKey = deployEnvVars.HOSTED_API_KEY;
|
|
157
161
|
|
|
158
162
|
for (const schedule of schedules) {
|
|
159
163
|
const {
|
|
@@ -203,6 +207,7 @@ const deployFromHostingYml = async (options = {}) => {
|
|
|
203
207
|
const hostingConfig = readHostingYml(config.workspace);
|
|
204
208
|
const {
|
|
205
209
|
google_cloud_info: googleCloudInfo,
|
|
210
|
+
env: envNames = [],
|
|
206
211
|
functions = {},
|
|
207
212
|
groups = {},
|
|
208
213
|
} = hostingConfig;
|
|
@@ -222,12 +227,7 @@ const deployFromHostingYml = async (options = {}) => {
|
|
|
222
227
|
handlersByName,
|
|
223
228
|
});
|
|
224
229
|
|
|
225
|
-
|
|
226
|
-
const hostedApiKey = getHostedApiKeyForDeploy(config.workspace);
|
|
227
|
-
if (!hostedApiKey) {
|
|
228
|
-
throw new Error('HOSTED_API_KEY is required in workspace .env when using requireHostedApiKey wrapper');
|
|
229
|
-
}
|
|
230
|
-
}
|
|
230
|
+
const deployEnvVars = validateEnvVarsForDeploy(config.workspace, envNames);
|
|
231
231
|
|
|
232
232
|
writeHostedJs({
|
|
233
233
|
workspace: config.workspace,
|
|
@@ -235,7 +235,6 @@ const deployFromHostingYml = async (options = {}) => {
|
|
|
235
235
|
});
|
|
236
236
|
|
|
237
237
|
const credsJson = getCredsJsonForDeploy(config.workspace);
|
|
238
|
-
const hostedApiKey = getHostedApiKeyForDeploy(config.workspace);
|
|
239
238
|
const deployArgs = getDeployArgs();
|
|
240
239
|
|
|
241
240
|
const deployOne = async (functionName) => {
|
|
@@ -250,7 +249,7 @@ const deployFromHostingYml = async (options = {}) => {
|
|
|
250
249
|
googleCloudInfo,
|
|
251
250
|
workspace: config.workspace,
|
|
252
251
|
credsJson,
|
|
253
|
-
|
|
252
|
+
deployEnvVars,
|
|
254
253
|
});
|
|
255
254
|
};
|
|
256
255
|
|
|
@@ -289,9 +288,9 @@ const deployFromHostingYml = async (options = {}) => {
|
|
|
289
288
|
|
|
290
289
|
console.log(`
|
|
291
290
|
Usage (from workspace repo):
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
291
|
+
mineral host --workspace . --api_dirs api all
|
|
292
|
+
mineral host --workspace . --api_dirs api function
|
|
293
|
+
mineral host --workspace . --api_dirs api group
|
|
295
294
|
`);
|
|
296
295
|
};
|
|
297
296
|
|
|
@@ -309,11 +308,11 @@ module.exports = {
|
|
|
309
308
|
|
|
310
309
|
/*
|
|
311
310
|
Deploy everything:
|
|
312
|
-
|
|
311
|
+
mineral host all
|
|
313
312
|
|
|
314
313
|
Deploy a single function:
|
|
315
|
-
|
|
314
|
+
mineral host function
|
|
316
315
|
|
|
317
316
|
Deploy a group of functions:
|
|
318
|
-
|
|
317
|
+
mineral host group
|
|
319
318
|
*/
|
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
|
-
const { wrapHostedFunction } = require('../hosting.utils');
|
|
2
|
+
const { wrapHostedFunction, parseWrapperRef } = require('../hosting.utils');
|
|
3
|
+
|
|
4
|
+
const formatWrapperRefForHostedJs = (wrapperRef) => {
|
|
5
|
+
const { modulePath, exportName } = parseWrapperRef(wrapperRef);
|
|
6
|
+
return `require('${ modulePath }').${ exportName }`;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const formatWrappersArg = (wrappers = []) => {
|
|
10
|
+
if (!wrappers.length) {
|
|
11
|
+
return '';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const wrapperLines = wrappers.map((wrapperRef) => ` ${ formatWrapperRefForHostedJs(wrapperRef) },`);
|
|
15
|
+
return `, [\n${ wrapperLines.join('\n') }\n ]`;
|
|
16
|
+
};
|
|
3
17
|
|
|
4
18
|
const generateHostedJs = ({
|
|
5
19
|
hostedHandlers,
|
|
6
20
|
}) => {
|
|
7
21
|
const exportLines = [];
|
|
8
|
-
const hostingUtilsRequire = '@foxtware/mineral/hosting.utils';
|
|
9
22
|
|
|
10
23
|
for (const hostedHandler of hostedHandlers) {
|
|
11
24
|
const { entryPoint, wrappers = [], requirePath } = hostedHandler;
|
|
12
|
-
const wrappersArg =
|
|
25
|
+
const wrappersArg = formatWrappersArg(wrappers);
|
|
13
26
|
|
|
14
27
|
exportLines.push(
|
|
15
28
|
` ${ entryPoint }: wrapHostedFunction(() => require('${ requirePath }'), '${ entryPoint }'${ wrappersArg }),`,
|
|
@@ -17,7 +30,7 @@ const generateHostedJs = ({
|
|
|
17
30
|
}
|
|
18
31
|
|
|
19
32
|
return `// Generated by mineral — do not edit
|
|
20
|
-
const { wrapHostedFunction } = require('
|
|
33
|
+
const { wrapHostedFunction } = require('@foxtware/mineral/hosting.utils');
|
|
21
34
|
|
|
22
35
|
module.exports = {
|
|
23
36
|
${ exportLines.join('\n') }
|
package/bin/mineral.js
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
const command = process.argv[2];
|
|
4
|
+
|
|
5
|
+
if (command === 'host') {
|
|
6
|
+
const { deployFromHostingYml } = require('../_deploy_scripts/deployFromHostingYml');
|
|
7
|
+
deployFromHostingYml().catch((error) => {
|
|
8
|
+
console.error(error);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
});
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
3
14
|
const { startServer } = require('../server');
|
|
4
15
|
|
|
5
16
|
startServer();
|
package/hosting.utils.js
CHANGED
|
@@ -8,22 +8,34 @@ const {
|
|
|
8
8
|
argsFromBody,
|
|
9
9
|
funcApi,
|
|
10
10
|
wrapFunction,
|
|
11
|
-
requireHostedApiKey,
|
|
12
11
|
statusCodeFromResult,
|
|
13
12
|
} = require('./server.utils');
|
|
14
13
|
|
|
15
|
-
const
|
|
16
|
-
|
|
14
|
+
const parseWrapperRef = (wrapperRef) => {
|
|
15
|
+
const hashIndex = wrapperRef.lastIndexOf('#');
|
|
16
|
+
|
|
17
|
+
if (hashIndex === -1) {
|
|
18
|
+
throw new Error(`Invalid wrapper ref (expected path#export): ${ wrapperRef }`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
modulePath: wrapperRef.slice(0, hashIndex),
|
|
23
|
+
exportName: wrapperRef.slice(hashIndex + 1),
|
|
24
|
+
};
|
|
17
25
|
};
|
|
18
26
|
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
+
const validateWrapperRef = (wrapperRef, workspaceRequire) => {
|
|
28
|
+
const { modulePath, exportName } = parseWrapperRef(wrapperRef);
|
|
29
|
+
workspaceRequire.resolve(modulePath);
|
|
30
|
+
const moduleExports = workspaceRequire(modulePath);
|
|
31
|
+
|
|
32
|
+
if (typeof moduleExports[exportName] !== 'function') {
|
|
33
|
+
throw new Error(`Wrapper export not found: ${ exportName } in ${ modulePath }`);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const wrapperExportName = (wrapperRef) => (
|
|
38
|
+
parseWrapperRef(wrapperRef).exportName
|
|
27
39
|
);
|
|
28
40
|
|
|
29
41
|
const getHostedEntries = (functions = {}) => {
|
|
@@ -51,8 +63,9 @@ const getHostedEntries = (functions = {}) => {
|
|
|
51
63
|
return [...byEntryPoint.values()];
|
|
52
64
|
};
|
|
53
65
|
|
|
54
|
-
const functionUsesWrapper = (functionConfig = {},
|
|
55
|
-
Array.isArray(functionConfig.wrappers)
|
|
66
|
+
const functionUsesWrapper = (functionConfig = {}, exportName) => (
|
|
67
|
+
Array.isArray(functionConfig.wrappers)
|
|
68
|
+
&& functionConfig.wrappers.some((wrapperRef) => wrapperExportName(wrapperRef) === exportName)
|
|
56
69
|
);
|
|
57
70
|
|
|
58
71
|
const getFuncApiConfig = ({
|
|
@@ -76,10 +89,9 @@ const getFuncApiConfig = ({
|
|
|
76
89
|
}
|
|
77
90
|
};
|
|
78
91
|
|
|
79
|
-
const wrapHostedFunction = (loader, exportName,
|
|
92
|
+
const wrapHostedFunction = (loader, exportName, wrappers = []) => {
|
|
80
93
|
let handler = null;
|
|
81
94
|
let usesFuncApi = false;
|
|
82
|
-
const wrappers = resolveWrappers(wrapperNames);
|
|
83
95
|
|
|
84
96
|
const coreHandler = async (req, res) => {
|
|
85
97
|
if (!handler) {
|
|
@@ -168,7 +180,7 @@ const getCredsJsonForDeploy = (workspace) => {
|
|
|
168
180
|
return JSON.stringify(yaml.parse(credsText));
|
|
169
181
|
};
|
|
170
182
|
|
|
171
|
-
const
|
|
183
|
+
const getEnvValueForDeploy = (workspace, envName) => {
|
|
172
184
|
const envPath = `${ workspace }/.env`;
|
|
173
185
|
|
|
174
186
|
if (!fs.existsSync(envPath)) {
|
|
@@ -176,10 +188,27 @@ const getHostedApiKeyForDeploy = (workspace) => {
|
|
|
176
188
|
}
|
|
177
189
|
|
|
178
190
|
const envText = fs.readFileSync(envPath, 'utf8');
|
|
179
|
-
const match = envText.match(
|
|
191
|
+
const match = envText.match(new RegExp(`^${ envName }=(.*)$`, 'm'));
|
|
180
192
|
return match ? match[1].trim() : '';
|
|
181
193
|
};
|
|
182
194
|
|
|
195
|
+
const getEnvVarsForDeploy = (workspace, envNames = []) => (
|
|
196
|
+
Object.fromEntries(
|
|
197
|
+
envNames.map((envName) => [envName, getEnvValueForDeploy(workspace, envName)]),
|
|
198
|
+
)
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const validateEnvVarsForDeploy = (workspace, envNames = []) => {
|
|
202
|
+
const envVars = getEnvVarsForDeploy(workspace, envNames);
|
|
203
|
+
const missing = envNames.filter((envName) => !envVars[envName]);
|
|
204
|
+
|
|
205
|
+
if (missing.length) {
|
|
206
|
+
throw new Error(`Missing required .env values: ${ missing.join(', ') }`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return envVars;
|
|
210
|
+
};
|
|
211
|
+
|
|
183
212
|
const resolveHostedHandlersForDeploy = ({
|
|
184
213
|
functions = {},
|
|
185
214
|
workspace,
|
|
@@ -189,7 +218,11 @@ const resolveHostedHandlersForDeploy = ({
|
|
|
189
218
|
const hostedEntries = getHostedEntries(functions);
|
|
190
219
|
|
|
191
220
|
return hostedEntries.map((hostedEntry) => {
|
|
192
|
-
const { entryPoint, source } = hostedEntry;
|
|
221
|
+
const { entryPoint, source, wrappers = [] } = hostedEntry;
|
|
222
|
+
|
|
223
|
+
for (const wrapperRef of wrappers) {
|
|
224
|
+
validateWrapperRef(wrapperRef, workspaceRequire);
|
|
225
|
+
}
|
|
193
226
|
|
|
194
227
|
if (source) {
|
|
195
228
|
workspaceRequire.resolve(source);
|
|
@@ -221,12 +254,15 @@ const resolveHostedHandlersForDeploy = ({
|
|
|
221
254
|
// TODO: support credsPayload in google_cloud_info instead of full workspace .creds.yml
|
|
222
255
|
|
|
223
256
|
module.exports = {
|
|
257
|
+
parseWrapperRef,
|
|
224
258
|
getFuncApiConfig,
|
|
225
259
|
wrapHostedFunction,
|
|
226
260
|
readHostingYml,
|
|
227
261
|
getCredsJsonForDeploy,
|
|
228
|
-
|
|
262
|
+
getEnvVarsForDeploy,
|
|
263
|
+
validateEnvVarsForDeploy,
|
|
229
264
|
getHostedEntries,
|
|
230
265
|
resolveHostedHandlersForDeploy,
|
|
231
266
|
functionUsesWrapper,
|
|
267
|
+
wrapperExportName,
|
|
232
268
|
};
|
package/package.json
CHANGED
package/server.utils.js
CHANGED
|
@@ -149,6 +149,20 @@ const requireHostedApiKey = async (req) => {
|
|
|
149
149
|
}
|
|
150
150
|
};
|
|
151
151
|
|
|
152
|
+
const allowCrossOriginCallsAndHandleOptions = async (req, res) => {
|
|
153
|
+
const { origin } = req.headers;
|
|
154
|
+
|
|
155
|
+
res.setHeader('Access-Control-Allow-Origin', origin || '*');
|
|
156
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
157
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, x-api-key, x-wf-token, x-wf-value');
|
|
158
|
+
|
|
159
|
+
if (req.method === 'OPTIONS') {
|
|
160
|
+
res.writeHead(204);
|
|
161
|
+
res.end();
|
|
162
|
+
return { handled: true };
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
152
166
|
const statusCodeFromResult = (result) => {
|
|
153
167
|
if (result?.ok === false) {
|
|
154
168
|
return result?.error?.statusCode ?? 400;
|
|
@@ -267,6 +281,7 @@ module.exports = {
|
|
|
267
281
|
argsFromBody,
|
|
268
282
|
wrapFunction,
|
|
269
283
|
requireHostedApiKey,
|
|
284
|
+
allowCrossOriginCallsAndHandleOptions,
|
|
270
285
|
statusCodeFromResult,
|
|
271
286
|
funcApi,
|
|
272
287
|
};
|