@foxtware/mineral 0.1.1 → 0.1.3
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/.gcloudignore +17 -0
- package/.hosting.yml.sample +23 -0
- package/README.md +2 -0
- package/_build_scripts/publish.js +26 -6
- package/_deploy_scripts/deployFromHostingYml.js +319 -0
- package/_deploy_scripts/execCommand.js +38 -0
- package/_deploy_scripts/generateHosted.js +44 -0
- package/_deploy_scripts/setEnvVarsGcloud.js +51 -0
- package/cli.js +42 -0
- package/hosting.utils.js +232 -0
- package/package.json +1 -1
- package/server.js +12 -62
- package/server.utils.js +44 -0
package/.gcloudignore
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# This file specifies files that are *not* uploaded to Google Cloud
|
|
2
|
+
# using gcloud. It follows the same syntax as .gitignore, with the addition of
|
|
3
|
+
# "#!include" directives (which insert the entries of the given .gitignore-style
|
|
4
|
+
# file at that point).
|
|
5
|
+
#
|
|
6
|
+
# For more information, run:
|
|
7
|
+
# $ gcloud topic gcloudignore
|
|
8
|
+
#
|
|
9
|
+
.gcloudignore
|
|
10
|
+
# If you would like to upload your .git directory, .gitignore file or files
|
|
11
|
+
# from your .gitignore file, remove the corresponding line
|
|
12
|
+
# below:
|
|
13
|
+
.git
|
|
14
|
+
.gitignore
|
|
15
|
+
|
|
16
|
+
node_modules
|
|
17
|
+
#!include:.gitignore
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
google_cloud_info:
|
|
2
|
+
project:
|
|
3
|
+
region:
|
|
4
|
+
|
|
5
|
+
# TODO: consider credsPayload in google_cloud_info instead of workspace .creds.yml
|
|
6
|
+
|
|
7
|
+
functions:
|
|
8
|
+
exampleFunction:
|
|
9
|
+
max_instances: 1
|
|
10
|
+
timeout: 300s
|
|
11
|
+
wrappers:
|
|
12
|
+
- requireHostedApiKey
|
|
13
|
+
# entry_point: otherExportName
|
|
14
|
+
|
|
15
|
+
packagedFunction:
|
|
16
|
+
source: '@foxtware/mineral/api/pokemon/pokemonPokeballThrow.js'
|
|
17
|
+
max_instances: 1
|
|
18
|
+
wrappers:
|
|
19
|
+
- checkTrainer
|
|
20
|
+
|
|
21
|
+
groups:
|
|
22
|
+
example_group:
|
|
23
|
+
- exampleFunction
|
package/README.md
CHANGED
|
@@ -50,3 +50,5 @@ The [bedrock](https://github.com/GorgonFreeman/bedrock) middleware, refactored f
|
|
|
50
50
|
```
|
|
51
51
|
- **Monorepo structure**
|
|
52
52
|
Mineral gets pushed to from a larger repo that can also contain private functions. Mineral should be strictly useful stuff for the public, and can be used standalone, but needs to be instantiated for serving, setting stuff like which creds file to use. This allows it to be used as part of another repo in the same HTTP/curl way as by itself. Pass `--workspace` to locate `.creds.yml` and `--api_dirs` to serve additional function directories.
|
|
53
|
+
|
|
54
|
+
For cloud deploy, workspaces use `.hosting.yml` and `npm run host` (same `--workspace` / `--api_dirs` flags as dev/serve). See `.hosting.yml.sample`.
|
|
@@ -12,8 +12,25 @@ const writePackageJson = (packageJson) => {
|
|
|
12
12
|
fs.writeFileSync(packageJsonPath, `${ JSON.stringify(packageJson, null, 2) }\n`);
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
const parseVersion = (version) => version.split('.').map((part) => Number(part) || 0);
|
|
16
|
+
|
|
17
|
+
const isVersionGreater = (left, right) => {
|
|
18
|
+
const [leftMajor, leftMinor, leftPatch] = parseVersion(left);
|
|
19
|
+
const [rightMajor, rightMinor, rightPatch] = parseVersion(right);
|
|
20
|
+
|
|
21
|
+
if (leftMajor !== rightMajor) {
|
|
22
|
+
return leftMajor > rightMajor;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (leftMinor !== rightMinor) {
|
|
26
|
+
return leftMinor > rightMinor;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return leftPatch > rightPatch;
|
|
30
|
+
};
|
|
31
|
+
|
|
15
32
|
const bumpPatch = (version) => {
|
|
16
|
-
const [major, minor, patch] = version
|
|
33
|
+
const [major, minor, patch] = parseVersion(version);
|
|
17
34
|
return [major, minor, patch + 1].join('.');
|
|
18
35
|
};
|
|
19
36
|
|
|
@@ -63,14 +80,17 @@ const publish = async () => {
|
|
|
63
80
|
|
|
64
81
|
let version = localVersion;
|
|
65
82
|
|
|
66
|
-
|
|
67
|
-
const suggestedVersion = bumpPatch(localVersion);
|
|
68
|
-
version = await askWithDefault('What version should we use?', suggestedVersion);
|
|
83
|
+
const localIsAhead = publishedVersion && isVersionGreater(localVersion, publishedVersion);
|
|
69
84
|
|
|
70
|
-
|
|
71
|
-
|
|
85
|
+
if (!localIsAhead) {
|
|
86
|
+
const latestVersion = publishedVersion || localVersion;
|
|
87
|
+
const suggestedVersion = bumpPatch(latestVersion);
|
|
88
|
+
version = await askWithDefault('What version should we use?', suggestedVersion);
|
|
72
89
|
}
|
|
73
90
|
|
|
91
|
+
packageJson.version = version;
|
|
92
|
+
writePackageJson(packageJson);
|
|
93
|
+
|
|
74
94
|
execSync('npm publish', {
|
|
75
95
|
cwd: mineralRoot,
|
|
76
96
|
stdio: 'inherit',
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
const readline = require('readline');
|
|
2
|
+
const { toAbsolutePath, setWorkspace } = require('../api/workspace');
|
|
3
|
+
const { getApiDirs, readCliFlag } = require('../cli');
|
|
4
|
+
const { loadHandlers } = require('../server');
|
|
5
|
+
const { readHostingYml, getCredsJsonForDeploy, getHostedApiKeyForDeploy, resolveHostedHandlersForDeploy, functionUsesWrapper } = require('../hosting.utils');
|
|
6
|
+
const { writeHostedJs } = require('./generateHosted');
|
|
7
|
+
const { execCommand } = require('./execCommand');
|
|
8
|
+
const { formatSetEnvVarsForGcloud, shellQuoteSingle } = require('./setEnvVarsGcloud');
|
|
9
|
+
|
|
10
|
+
const GCLOUD_NODEJS_MAX_MAJOR = 24;
|
|
11
|
+
|
|
12
|
+
const gcloudRuntimeFromCurrentNode = () => {
|
|
13
|
+
const localMajor = Number.parseInt(process.versions.node.split('.')[0], 10);
|
|
14
|
+
const effective = Number.isNaN(localMajor) ? GCLOUD_NODEJS_MAX_MAJOR : localMajor;
|
|
15
|
+
const major = Math.min(effective, GCLOUD_NODEJS_MAX_MAJOR);
|
|
16
|
+
return `nodejs${ major }`;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const getHostConfig = (options = {}) => ({
|
|
20
|
+
workspace: toAbsolutePath(
|
|
21
|
+
options.workspace
|
|
22
|
+
?? readCliFlag('--workspace')
|
|
23
|
+
?? process.env.MINERAL_WORKSPACE
|
|
24
|
+
?? process.cwd(),
|
|
25
|
+
),
|
|
26
|
+
api_dirs: getApiDirs(options),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const chooseOption = async (prompt, options) => new Promise((resolve) => {
|
|
30
|
+
const rl = readline.createInterface({
|
|
31
|
+
input: process.stdin,
|
|
32
|
+
output: process.stdout,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
console.log(prompt);
|
|
36
|
+
options.forEach((option, index) => {
|
|
37
|
+
console.log(` ${ index + 1 }. ${ option }`);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
rl.question('> ', (answer) => {
|
|
41
|
+
rl.close();
|
|
42
|
+
const choice = Number(answer) - 1;
|
|
43
|
+
resolve(options[choice] ?? options[0]);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const anyHostedEntryUsesWrapper = (hostedEntries, wrapperName) => (
|
|
48
|
+
hostedEntries.some((hostedEntry) => hostedEntry.wrappers?.includes(wrapperName))
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const getDeployArgs = () => {
|
|
52
|
+
const args = process.argv.slice(2);
|
|
53
|
+
const deployArgs = [];
|
|
54
|
+
|
|
55
|
+
for (let index = 0; index < args.length; index++) {
|
|
56
|
+
const arg = args[index];
|
|
57
|
+
|
|
58
|
+
if (arg === '--workspace' || arg === '--api_dirs') {
|
|
59
|
+
index++;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (arg.startsWith('--workspace=') || arg.startsWith('--api_dirs=')) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
deployArgs.push(arg);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return deployArgs;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const handlerByRouteName = (handlers) => {
|
|
74
|
+
const byName = new Map();
|
|
75
|
+
|
|
76
|
+
for (const handler of handlers.values()) {
|
|
77
|
+
byName.set(handler.routeName, handler);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return byName;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const deployFunction = async ({
|
|
84
|
+
functionName,
|
|
85
|
+
functionConfig,
|
|
86
|
+
googleCloudInfo,
|
|
87
|
+
workspace,
|
|
88
|
+
credsJson,
|
|
89
|
+
hostedApiKey,
|
|
90
|
+
}) => {
|
|
91
|
+
const config = {
|
|
92
|
+
...googleCloudInfo,
|
|
93
|
+
...functionConfig,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
let {
|
|
97
|
+
project,
|
|
98
|
+
region,
|
|
99
|
+
trigger = 'http',
|
|
100
|
+
runtime = gcloudRuntimeFromCurrentNode(),
|
|
101
|
+
allow_unauthenticated: allowUnauthenticated = true,
|
|
102
|
+
gen2 = true,
|
|
103
|
+
set_env_vars: extraSetEnvVars,
|
|
104
|
+
entry_point: entryPoint,
|
|
105
|
+
schedules,
|
|
106
|
+
groups,
|
|
107
|
+
wrappers,
|
|
108
|
+
source,
|
|
109
|
+
...gcloudArgs
|
|
110
|
+
} = config;
|
|
111
|
+
|
|
112
|
+
const resolvedEntryPoint = entryPoint || functionName;
|
|
113
|
+
const envParts = [
|
|
114
|
+
'HOSTED=true',
|
|
115
|
+
`CREDS=${ credsJson }`,
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
if (hostedApiKey) {
|
|
119
|
+
envParts.push(`HOSTED_API_KEY=${ hostedApiKey }`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (extraSetEnvVars) {
|
|
123
|
+
envParts.push(extraSetEnvVars);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const rawSetEnvVars = envParts.join(',');
|
|
127
|
+
const setEnvVarsForGcloud = formatSetEnvVarsForGcloud(rawSetEnvVars);
|
|
128
|
+
|
|
129
|
+
const deployCommand = [
|
|
130
|
+
`gcloud functions deploy ${ functionName }`,
|
|
131
|
+
`--project ${ project }`,
|
|
132
|
+
`--region ${ region }`,
|
|
133
|
+
`--source ${ shellQuoteSingle(workspace) }`,
|
|
134
|
+
`--entry-point ${ resolvedEntryPoint }`,
|
|
135
|
+
`--trigger-${ trigger }`,
|
|
136
|
+
`--runtime ${ runtime }`,
|
|
137
|
+
`--set-env-vars ${ shellQuoteSingle(setEnvVarsForGcloud) }`,
|
|
138
|
+
...(allowUnauthenticated ? ['--allow-unauthenticated'] : []),
|
|
139
|
+
...(gen2 ? ['--gen2'] : []),
|
|
140
|
+
...Object.entries(gcloudArgs).map(([key, value]) => `--${ key.replaceAll('_', '-') } ${ value }`),
|
|
141
|
+
].join(' ');
|
|
142
|
+
|
|
143
|
+
console.log(deployCommand);
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
await execCommand(deployCommand);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.error(`Error deploying function ${ functionName }:`, error);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!schedules?.length) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const requiresHostedApiKey = functionUsesWrapper(functionConfig, 'requireHostedApiKey');
|
|
157
|
+
|
|
158
|
+
for (const schedule of schedules) {
|
|
159
|
+
const {
|
|
160
|
+
name: jobName,
|
|
161
|
+
schedule: jobSchedule,
|
|
162
|
+
http_method: jobHttpMethod = 'POST',
|
|
163
|
+
headers: jobHeaders = '',
|
|
164
|
+
message_body: jobMessageBody,
|
|
165
|
+
...schedulerArgs
|
|
166
|
+
} = schedule;
|
|
167
|
+
|
|
168
|
+
let schedulerHeaders = jobHeaders;
|
|
169
|
+
if (requiresHostedApiKey && hostedApiKey) {
|
|
170
|
+
schedulerHeaders = schedulerHeaders ? `${ schedulerHeaders },` : '';
|
|
171
|
+
schedulerHeaders += `x-api-key=${ hostedApiKey }`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const checkCommand = `gcloud scheduler jobs describe ${ jobName } --project=${ project } --location=${ region } 2>/dev/null || echo "NOT_FOUND"`;
|
|
176
|
+
const checkResult = await execCommand(checkCommand);
|
|
177
|
+
const jobExists = !checkResult.stdout.includes('NOT_FOUND');
|
|
178
|
+
|
|
179
|
+
const schedulerCommand = [
|
|
180
|
+
jobExists ? `gcloud scheduler jobs update http ${ jobName }` : `gcloud scheduler jobs create http ${ jobName }`,
|
|
181
|
+
`--schedule="${ jobSchedule }"`,
|
|
182
|
+
`--uri="https://${ region }-${ project }.cloudfunctions.net/${ functionName }"`,
|
|
183
|
+
`--http-method=${ jobHttpMethod }`,
|
|
184
|
+
`--project=${ project }`,
|
|
185
|
+
`--location=${ region }`,
|
|
186
|
+
jobExists ? `--update-headers ${ schedulerHeaders }` : `--headers ${ schedulerHeaders }`,
|
|
187
|
+
...(jobMessageBody ? [`--message-body '${ jobMessageBody }'`] : []),
|
|
188
|
+
...Object.entries(schedulerArgs).map(([key, value]) => `--${ key.replaceAll('_', '-') } ${ value }`),
|
|
189
|
+
].join(' ');
|
|
190
|
+
|
|
191
|
+
console.log(schedulerCommand);
|
|
192
|
+
await execCommand(schedulerCommand);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
console.error(`Error handling scheduler job ${ jobName }:`, error);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const deployFromHostingYml = async (options = {}) => {
|
|
200
|
+
const config = getHostConfig(options);
|
|
201
|
+
setWorkspace(config.workspace);
|
|
202
|
+
|
|
203
|
+
const hostingConfig = readHostingYml(config.workspace);
|
|
204
|
+
const {
|
|
205
|
+
google_cloud_info: googleCloudInfo,
|
|
206
|
+
functions = {},
|
|
207
|
+
groups = {},
|
|
208
|
+
} = hostingConfig;
|
|
209
|
+
|
|
210
|
+
if (!googleCloudInfo?.project || !googleCloudInfo?.region) {
|
|
211
|
+
throw new Error('.hosting.yml requires google_cloud_info.project and google_cloud_info.region');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const handlers = loadHandlers({
|
|
215
|
+
...config,
|
|
216
|
+
host_mode: true,
|
|
217
|
+
});
|
|
218
|
+
const handlersByName = handlerByRouteName(handlers);
|
|
219
|
+
const hostedHandlers = resolveHostedHandlersForDeploy({
|
|
220
|
+
functions,
|
|
221
|
+
workspace: config.workspace,
|
|
222
|
+
handlersByName,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
if (anyHostedEntryUsesWrapper(hostedHandlers, 'requireHostedApiKey')) {
|
|
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
|
+
}
|
|
231
|
+
|
|
232
|
+
writeHostedJs({
|
|
233
|
+
workspace: config.workspace,
|
|
234
|
+
hostedHandlers,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const credsJson = getCredsJsonForDeploy(config.workspace);
|
|
238
|
+
const hostedApiKey = getHostedApiKeyForDeploy(config.workspace);
|
|
239
|
+
const deployArgs = getDeployArgs();
|
|
240
|
+
|
|
241
|
+
const deployOne = async (functionName) => {
|
|
242
|
+
if (!functions[functionName]) {
|
|
243
|
+
console.log(`Function ${ functionName } not found in .hosting.yml, skipping`);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
await deployFunction({
|
|
248
|
+
functionName,
|
|
249
|
+
functionConfig: functions[functionName],
|
|
250
|
+
googleCloudInfo,
|
|
251
|
+
workspace: config.workspace,
|
|
252
|
+
credsJson,
|
|
253
|
+
hostedApiKey,
|
|
254
|
+
});
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
if (deployArgs.includes('group')) {
|
|
258
|
+
const groupNames = Object.keys(groups);
|
|
259
|
+
if (!groupNames.length) {
|
|
260
|
+
console.log('No groups defined in .hosting.yml');
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const selectedGroup = await chooseOption('Which group would you like to deploy?', groupNames);
|
|
265
|
+
for (const functionName of groups[selectedGroup] || []) {
|
|
266
|
+
await deployOne(functionName);
|
|
267
|
+
}
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (deployArgs.includes('function')) {
|
|
272
|
+
const functionNames = Object.keys(functions);
|
|
273
|
+
if (!functionNames.length) {
|
|
274
|
+
console.log('No functions defined in .hosting.yml');
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const selectedFunction = await chooseOption('Which function would you like to deploy?', functionNames);
|
|
279
|
+
await deployOne(selectedFunction);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (deployArgs.includes('all')) {
|
|
284
|
+
for (const functionName of Object.keys(functions)) {
|
|
285
|
+
await deployOne(functionName);
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
console.log(`
|
|
291
|
+
Usage (from workspace repo):
|
|
292
|
+
npm run host --workspace . --api_dirs api all
|
|
293
|
+
npm run host --workspace . --api_dirs api function
|
|
294
|
+
npm run host --workspace . --api_dirs api group
|
|
295
|
+
`);
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
if (require.main === module) {
|
|
299
|
+
deployFromHostingYml().catch((error) => {
|
|
300
|
+
console.error(error);
|
|
301
|
+
process.exit(1);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
module.exports = {
|
|
306
|
+
deployFromHostingYml,
|
|
307
|
+
getHostConfig,
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
/*
|
|
311
|
+
Deploy everything:
|
|
312
|
+
npm run host all
|
|
313
|
+
|
|
314
|
+
Deploy a single function:
|
|
315
|
+
npm run host function
|
|
316
|
+
|
|
317
|
+
Deploy a group of functions:
|
|
318
|
+
npm run host group
|
|
319
|
+
*/
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
|
|
3
|
+
const execCommand = (command) => new Promise((resolve, reject) => {
|
|
4
|
+
const childProcess = spawn(command, [], {
|
|
5
|
+
stdio: 'pipe',
|
|
6
|
+
shell: true,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
let stdout = '';
|
|
10
|
+
let stderr = '';
|
|
11
|
+
|
|
12
|
+
childProcess.stdout.on('data', (data) => {
|
|
13
|
+
stdout += data.toString();
|
|
14
|
+
process.stdout.write(data);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
childProcess.stderr.on('data', (data) => {
|
|
18
|
+
stderr += data.toString();
|
|
19
|
+
process.stderr.write(data);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
childProcess.on('close', (code) => {
|
|
23
|
+
if (code === 0) {
|
|
24
|
+
resolve({ stdout, stderr, command });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
reject({ stdout, stderr, command, code });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
childProcess.on('error', (error) => {
|
|
32
|
+
reject({ error, command });
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
module.exports = {
|
|
37
|
+
execCommand,
|
|
38
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const { wrapHostedFunction } = require('../hosting.utils');
|
|
3
|
+
|
|
4
|
+
const generateHostedJs = ({
|
|
5
|
+
hostedHandlers,
|
|
6
|
+
}) => {
|
|
7
|
+
const exportLines = [];
|
|
8
|
+
const hostingUtilsRequire = '@foxtware/mineral/hosting.utils';
|
|
9
|
+
|
|
10
|
+
for (const hostedHandler of hostedHandlers) {
|
|
11
|
+
const { entryPoint, wrappers = [], requirePath } = hostedHandler;
|
|
12
|
+
const wrappersArg = wrappers.length ? `, ${ JSON.stringify(wrappers) }` : '';
|
|
13
|
+
|
|
14
|
+
exportLines.push(
|
|
15
|
+
` ${ entryPoint }: wrapHostedFunction(() => require('${ requirePath }'), '${ entryPoint }'${ wrappersArg }),`,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return `// Generated by mineral — do not edit
|
|
20
|
+
const { wrapHostedFunction } = require('${ hostingUtilsRequire }');
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
${ exportLines.join('\n') }
|
|
24
|
+
};
|
|
25
|
+
`;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const writeHostedJs = ({
|
|
29
|
+
workspace,
|
|
30
|
+
hostedHandlers,
|
|
31
|
+
}) => {
|
|
32
|
+
const hostedPath = `${ workspace.replace(/\/$/, '') }/hosted.js`;
|
|
33
|
+
const content = generateHostedJs({
|
|
34
|
+
hostedHandlers,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
fs.writeFileSync(hostedPath, content);
|
|
38
|
+
return hostedPath;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
generateHostedJs,
|
|
43
|
+
writeHostedJs,
|
|
44
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
function splitSetEnvVarsPairs(combined) {
|
|
2
|
+
const segments = combined.split(/,(?=[A-Za-z_][A-Za-z0-9_]*=)/);
|
|
3
|
+
return segments.map((segment) => {
|
|
4
|
+
const eq = segment.indexOf('=');
|
|
5
|
+
if (eq === -1) {
|
|
6
|
+
throw new Error(`Invalid set_env_vars segment (no =): ${ segment }`);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
key: segment.slice(0, eq).trim(),
|
|
11
|
+
value: segment.slice(eq + 1),
|
|
12
|
+
};
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function pickDelimiter(pairs) {
|
|
17
|
+
const blob = pairs.map((pair) => `${ pair.key }=${ pair.value }`).join('');
|
|
18
|
+
|
|
19
|
+
for (let count = 3; count < 64; count++) {
|
|
20
|
+
const delimiter = '#'.repeat(count);
|
|
21
|
+
if (!blob.includes(delimiter)) {
|
|
22
|
+
return delimiter;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
throw new Error('Could not find a delimiter for gcloud --set-env-vars');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function formatSetEnvVarsForGcloud(combined) {
|
|
30
|
+
const pairs = splitSetEnvVarsPairs(combined);
|
|
31
|
+
if (pairs.length === 0) {
|
|
32
|
+
return combined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const needsDelimiter = pairs.length > 1 || pairs.some((pair) => pair.value.includes(','));
|
|
36
|
+
if (!needsDelimiter) {
|
|
37
|
+
return combined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const delimiter = pickDelimiter(pairs);
|
|
41
|
+
return `^${ delimiter }^${ pairs.map((pair) => `${ pair.key }=${ pair.value }`).join(delimiter) }`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function shellQuoteSingle(value) {
|
|
45
|
+
return `'${ String(value).replace(/'/g, `'\\''`) }'`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
formatSetEnvVarsForGcloud,
|
|
50
|
+
shellQuoteSingle,
|
|
51
|
+
};
|
package/cli.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const splitCommaList = (value = '') => value
|
|
2
|
+
.split(',')
|
|
3
|
+
.map((item) => item.trim())
|
|
4
|
+
.filter(Boolean);
|
|
5
|
+
|
|
6
|
+
const readCliFlag = (flag) => {
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const equalsPrefix = `${ flag }=`;
|
|
9
|
+
|
|
10
|
+
for (let index = 0; index < args.length; index++) {
|
|
11
|
+
if (args[index].startsWith(equalsPrefix)) {
|
|
12
|
+
return args[index].slice(equalsPrefix.length);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (args[index] === flag) {
|
|
16
|
+
return args[index + 1];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const getApiDirs = (options = {}) => {
|
|
22
|
+
if (options.api_dirs) {
|
|
23
|
+
return options.api_dirs;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const fromCli = readCliFlag('--api_dirs');
|
|
27
|
+
if (fromCli) {
|
|
28
|
+
return splitCommaList(fromCli);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (process.env.MINERAL_API_DIRS) {
|
|
32
|
+
return splitCommaList(process.env.MINERAL_API_DIRS);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return [];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
module.exports = {
|
|
39
|
+
splitCommaList,
|
|
40
|
+
readCliFlag,
|
|
41
|
+
getApiDirs,
|
|
42
|
+
};
|
package/hosting.utils.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const { createRequire } = require('module');
|
|
3
|
+
const yaml = require('yaml');
|
|
4
|
+
const {
|
|
5
|
+
respondJson,
|
|
6
|
+
errorToReadable,
|
|
7
|
+
getRequestBody,
|
|
8
|
+
argsFromBody,
|
|
9
|
+
funcApi,
|
|
10
|
+
wrapFunction,
|
|
11
|
+
requireHostedApiKey,
|
|
12
|
+
statusCodeFromResult,
|
|
13
|
+
} = require('./server.utils');
|
|
14
|
+
|
|
15
|
+
const wrappersByName = {
|
|
16
|
+
requireHostedApiKey,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const resolveWrappers = (wrapperNames = []) => (
|
|
20
|
+
wrapperNames.map((wrapperName) => {
|
|
21
|
+
const wrapper = wrappersByName[wrapperName];
|
|
22
|
+
if (!wrapper) {
|
|
23
|
+
throw new Error(`Unknown wrapper: ${ wrapperName }`);
|
|
24
|
+
}
|
|
25
|
+
return wrapper;
|
|
26
|
+
})
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const getHostedEntries = (functions = {}) => {
|
|
30
|
+
const byEntryPoint = new Map();
|
|
31
|
+
|
|
32
|
+
for (const [functionName, functionConfig] of Object.entries(functions)) {
|
|
33
|
+
const entryPoint = functionConfig.entry_point || functionConfig.entryPoint || functionName;
|
|
34
|
+
const existing = byEntryPoint.get(entryPoint) || {
|
|
35
|
+
entryPoint,
|
|
36
|
+
wrappers: [],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
if (Array.isArray(functionConfig.wrappers)) {
|
|
40
|
+
existing.wrappers.push(...functionConfig.wrappers);
|
|
41
|
+
existing.wrappers = [...new Set(existing.wrappers)];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (functionConfig.source) {
|
|
45
|
+
existing.source = functionConfig.source;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
byEntryPoint.set(entryPoint, existing);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return [...byEntryPoint.values()];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const functionUsesWrapper = (functionConfig = {}, wrapperName) => (
|
|
55
|
+
Array.isArray(functionConfig.wrappers) && functionConfig.wrappers.includes(wrapperName)
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const getFuncApiConfig = ({
|
|
59
|
+
moduleExports,
|
|
60
|
+
routeName,
|
|
61
|
+
}) => {
|
|
62
|
+
const { funcApiConfig } = moduleExports;
|
|
63
|
+
if (!funcApiConfig || typeof funcApiConfig !== 'object') {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (funcApiConfig[routeName]) {
|
|
68
|
+
return funcApiConfig[routeName];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const exportNames = Object.keys(moduleExports).filter((key) => key !== 'funcApiConfig');
|
|
72
|
+
const configIsShared = !exportNames.some((name) => funcApiConfig[name]);
|
|
73
|
+
|
|
74
|
+
if (configIsShared) {
|
|
75
|
+
return funcApiConfig;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const wrapHostedFunction = (loader, exportName, wrapperNames = []) => {
|
|
80
|
+
let handler = null;
|
|
81
|
+
let usesFuncApi = false;
|
|
82
|
+
const wrappers = resolveWrappers(wrapperNames);
|
|
83
|
+
|
|
84
|
+
const coreHandler = async (req, res) => {
|
|
85
|
+
if (!handler) {
|
|
86
|
+
const moduleExports = loader();
|
|
87
|
+
const handlerFn = moduleExports[exportName];
|
|
88
|
+
|
|
89
|
+
if (typeof handlerFn !== 'function') {
|
|
90
|
+
throw new Error(`Hosted export not found: ${ exportName }`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const funcApiConfig = getFuncApiConfig({
|
|
94
|
+
moduleExports,
|
|
95
|
+
routeName: exportName,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
usesFuncApi = Boolean(funcApiConfig);
|
|
99
|
+
handler = usesFuncApi ? funcApi(handlerFn, funcApiConfig) : handlerFn;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const body = await getRequestBody(req);
|
|
103
|
+
const args = argsFromBody(body);
|
|
104
|
+
return usesFuncApi
|
|
105
|
+
? await handler({ req, res, body, args })
|
|
106
|
+
: await handler(...args);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const wrappedHandler = wrapFunction(coreHandler, wrappers);
|
|
110
|
+
|
|
111
|
+
return async (req, res) => {
|
|
112
|
+
try {
|
|
113
|
+
const result = await wrappedHandler(req, res);
|
|
114
|
+
|
|
115
|
+
if (res.headersSent) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (result === undefined) {
|
|
120
|
+
respondJson(res, 200, { ok: true });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const statusCode = statusCodeFromResult(result);
|
|
125
|
+
respondJson(res, statusCode, result);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (res.headersSent) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
respondJson(res, 500, {
|
|
132
|
+
ok: false,
|
|
133
|
+
error: {
|
|
134
|
+
code: 'UNHANDLED_ERROR',
|
|
135
|
+
message: 'Unhandled server error.',
|
|
136
|
+
details: errorToReadable(error),
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const readHostingYml = (workspace) => {
|
|
144
|
+
const hostingPath = `${ workspace }/.hosting.yml`;
|
|
145
|
+
|
|
146
|
+
if (!fs.existsSync(hostingPath)) {
|
|
147
|
+
throw new Error(`Missing .hosting.yml in workspace: ${ workspace }`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const hostingText = fs.readFileSync(hostingPath, 'utf8');
|
|
151
|
+
const hostingConfig = yaml.parse(hostingText);
|
|
152
|
+
|
|
153
|
+
if (!hostingConfig || typeof hostingConfig !== 'object' || Array.isArray(hostingConfig)) {
|
|
154
|
+
throw new Error('Invalid .hosting.yml');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return hostingConfig;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const getCredsJsonForDeploy = (workspace) => {
|
|
161
|
+
const credsPath = `${ workspace }/.creds.yml`;
|
|
162
|
+
|
|
163
|
+
if (!fs.existsSync(credsPath)) {
|
|
164
|
+
throw new Error(`Missing .creds.yml in workspace: ${ workspace }`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const credsText = fs.readFileSync(credsPath, 'utf8');
|
|
168
|
+
return JSON.stringify(yaml.parse(credsText));
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const getHostedApiKeyForDeploy = (workspace) => {
|
|
172
|
+
const envPath = `${ workspace }/.env`;
|
|
173
|
+
|
|
174
|
+
if (!fs.existsSync(envPath)) {
|
|
175
|
+
return '';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const envText = fs.readFileSync(envPath, 'utf8');
|
|
179
|
+
const match = envText.match(/^HOSTED_API_KEY=(.*)$/m);
|
|
180
|
+
return match ? match[1].trim() : '';
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const resolveHostedHandlersForDeploy = ({
|
|
184
|
+
functions = {},
|
|
185
|
+
workspace,
|
|
186
|
+
handlersByName,
|
|
187
|
+
}) => {
|
|
188
|
+
const workspaceRequire = createRequire(`${ workspace.replace(/\/$/, '') }/package.json`);
|
|
189
|
+
const hostedEntries = getHostedEntries(functions);
|
|
190
|
+
|
|
191
|
+
return hostedEntries.map((hostedEntry) => {
|
|
192
|
+
const { entryPoint, source } = hostedEntry;
|
|
193
|
+
|
|
194
|
+
if (source) {
|
|
195
|
+
workspaceRequire.resolve(source);
|
|
196
|
+
const moduleExports = workspaceRequire(source);
|
|
197
|
+
if (typeof moduleExports[entryPoint] !== 'function') {
|
|
198
|
+
throw new Error(`Hosted export not found: ${ entryPoint } in ${ source }`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
...hostedEntry,
|
|
203
|
+
requirePath: source,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const handler = handlersByName.get(entryPoint);
|
|
208
|
+
if (!handler) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
`entry_point "${ entryPoint }" not found in workspace handlers — add api/ handler or source in .hosting.yml`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
...hostedEntry,
|
|
216
|
+
requirePath: `./${ handler.filePath.slice(workspace.length + 1) }`,
|
|
217
|
+
};
|
|
218
|
+
});
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// TODO: support credsPayload in google_cloud_info instead of full workspace .creds.yml
|
|
222
|
+
|
|
223
|
+
module.exports = {
|
|
224
|
+
getFuncApiConfig,
|
|
225
|
+
wrapHostedFunction,
|
|
226
|
+
readHostingYml,
|
|
227
|
+
getCredsJsonForDeploy,
|
|
228
|
+
getHostedApiKeyForDeploy,
|
|
229
|
+
getHostedEntries,
|
|
230
|
+
resolveHostedHandlersForDeploy,
|
|
231
|
+
functionUsesWrapper,
|
|
232
|
+
};
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -1,49 +1,12 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const http = require('http');
|
|
3
|
-
const { respondJson, errorToReadable, getRequestBody, argsFromBody, funcApi } = require('./server.utils');
|
|
3
|
+
const { respondJson, errorToReadable, getRequestBody, argsFromBody, funcApi, statusCodeFromResult } = require('./server.utils');
|
|
4
4
|
const { getWorkspace, setWorkspace, loadWorkspaceEnv, toAbsolutePath } = require('./api/workspace');
|
|
5
|
+
const { getApiDirs, readCliFlag } = require('./cli');
|
|
6
|
+
const { getFuncApiConfig } = require('./hosting.utils');
|
|
5
7
|
|
|
6
8
|
const MINERAL_API_DIR = `${ __dirname }/api`;
|
|
7
9
|
|
|
8
|
-
// --- Config ---
|
|
9
|
-
|
|
10
|
-
const splitCommaList = (value = '') => value
|
|
11
|
-
.split(',')
|
|
12
|
-
.map((item) => item.trim())
|
|
13
|
-
.filter(Boolean);
|
|
14
|
-
|
|
15
|
-
const readCliFlag = (flag) => {
|
|
16
|
-
const args = process.argv.slice(2);
|
|
17
|
-
const equalsPrefix = `${ flag }=`;
|
|
18
|
-
|
|
19
|
-
for (let index = 0; index < args.length; index++) {
|
|
20
|
-
if (args[index].startsWith(equalsPrefix)) {
|
|
21
|
-
return args[index].slice(equalsPrefix.length);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (args[index] === flag) {
|
|
25
|
-
return args[index + 1];
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const getApiDirs = (options = {}) => {
|
|
31
|
-
if (options.api_dirs) {
|
|
32
|
-
return options.api_dirs;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const fromCli = readCliFlag('--api_dirs');
|
|
36
|
-
if (fromCli) {
|
|
37
|
-
return splitCommaList(fromCli);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (process.env.MINERAL_API_DIRS) {
|
|
41
|
-
return splitCommaList(process.env.MINERAL_API_DIRS);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return [];
|
|
45
|
-
};
|
|
46
|
-
|
|
47
10
|
const getConfig = (options = {}) => ({
|
|
48
11
|
port: Number(options.port ?? process.env.PORT ?? 8000),
|
|
49
12
|
workspace: toAbsolutePath(
|
|
@@ -53,6 +16,7 @@ const getConfig = (options = {}) => ({
|
|
|
53
16
|
?? process.cwd(),
|
|
54
17
|
),
|
|
55
18
|
api_dirs: getApiDirs(options),
|
|
19
|
+
host_mode: Boolean(options.host_mode),
|
|
56
20
|
});
|
|
57
21
|
|
|
58
22
|
// --- Handler discovery ---
|
|
@@ -105,6 +69,7 @@ const listJsFiles = (directory) => {
|
|
|
105
69
|
const directoriesToScan = ({
|
|
106
70
|
workspace,
|
|
107
71
|
api_dirs,
|
|
72
|
+
host_mode = false,
|
|
108
73
|
}) => {
|
|
109
74
|
const extraDirs = api_dirs.map((dir) => (
|
|
110
75
|
dir.startsWith('/')
|
|
@@ -112,30 +77,15 @@ const directoriesToScan = ({
|
|
|
112
77
|
: `${ workspace }/${ dir }`
|
|
113
78
|
));
|
|
114
79
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const getFuncApiConfig = ({
|
|
119
|
-
moduleExports,
|
|
120
|
-
routeName,
|
|
121
|
-
}) => {
|
|
122
|
-
const { funcApiConfig } = moduleExports;
|
|
123
|
-
if (!funcApiConfig || typeof funcApiConfig !== 'object') {
|
|
124
|
-
return undefined;
|
|
80
|
+
if (host_mode && api_dirs.length) {
|
|
81
|
+
return extraDirs;
|
|
125
82
|
}
|
|
126
83
|
|
|
127
|
-
|
|
128
|
-
return funcApiConfig[routeName];
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const exportNames = Object.keys(moduleExports).filter((key) => key !== 'funcApiConfig');
|
|
132
|
-
const configIsShared = !exportNames.some((name) => funcApiConfig[name]);
|
|
133
|
-
|
|
134
|
-
if (configIsShared) {
|
|
135
|
-
return funcApiConfig;
|
|
136
|
-
}
|
|
84
|
+
return [MINERAL_API_DIR, ...extraDirs];
|
|
137
85
|
};
|
|
138
86
|
|
|
87
|
+
const getFuncApiConfigFromModule = getFuncApiConfig;
|
|
88
|
+
|
|
139
89
|
const addHandlerFromFile = (filePath, handlers) => {
|
|
140
90
|
const moduleExports = require(filePath);
|
|
141
91
|
if (!moduleExports || typeof moduleExports !== 'object') {
|
|
@@ -148,7 +98,7 @@ const addHandlerFromFile = (filePath, handlers) => {
|
|
|
148
98
|
return;
|
|
149
99
|
}
|
|
150
100
|
|
|
151
|
-
const funcApiConfig =
|
|
101
|
+
const funcApiConfig = getFuncApiConfigFromModule({
|
|
152
102
|
moduleExports,
|
|
153
103
|
routeName,
|
|
154
104
|
});
|
|
@@ -218,7 +168,7 @@ const createServer = (handlers) => http.createServer(async (req, res) => {
|
|
|
218
168
|
return;
|
|
219
169
|
}
|
|
220
170
|
|
|
221
|
-
respondJson(res,
|
|
171
|
+
respondJson(res, statusCodeFromResult(result), result);
|
|
222
172
|
} catch (error) {
|
|
223
173
|
respondJson(res, 500, {
|
|
224
174
|
ok: false,
|
package/server.utils.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { logDeep } = require('./api/utils');
|
|
2
2
|
const { StringDecoder } = require('string_decoder');
|
|
3
|
+
const { HOSTED } = require('./api/constants');
|
|
3
4
|
|
|
4
5
|
const respondJson = (res, statusCode, payload) => {
|
|
5
6
|
logDeep(payload);
|
|
@@ -36,6 +37,10 @@ const getRequestBody = async (req) => {
|
|
|
36
37
|
return undefined;
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
if (req.body !== undefined) {
|
|
41
|
+
return req.body;
|
|
42
|
+
}
|
|
43
|
+
|
|
39
44
|
const decoder = new StringDecoder('utf8');
|
|
40
45
|
|
|
41
46
|
return await new Promise((resolve, reject) => {
|
|
@@ -116,6 +121,42 @@ const runRequestHandler = async (requestHandler, requestContext) => {
|
|
|
116
121
|
return mergeRequestContext(requestContext, handlerOutput);
|
|
117
122
|
};
|
|
118
123
|
|
|
124
|
+
const wrapFunction = (func, wrappers = []) => async (req, res, ...rest) => {
|
|
125
|
+
for (const wrapper of wrappers) {
|
|
126
|
+
const rejected = await wrapper(req, res);
|
|
127
|
+
if (rejected) {
|
|
128
|
+
return rejected;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return func(req, res, ...rest);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const requireHostedApiKey = async (req) => {
|
|
136
|
+
if (!HOSTED) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (req.headers['x-api-key'] !== process.env.HOSTED_API_KEY) {
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
error: {
|
|
144
|
+
code: 'UNAUTHORIZED',
|
|
145
|
+
message: 'Unauthorized',
|
|
146
|
+
statusCode: 401,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const statusCodeFromResult = (result) => {
|
|
153
|
+
if (result?.ok === false) {
|
|
154
|
+
return result?.error?.statusCode ?? 400;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return 200;
|
|
158
|
+
};
|
|
159
|
+
|
|
119
160
|
const funcApi = (func, config = {}) => {
|
|
120
161
|
const {
|
|
121
162
|
requestHandler,
|
|
@@ -224,5 +265,8 @@ module.exports = {
|
|
|
224
265
|
errorToReadable,
|
|
225
266
|
getRequestBody,
|
|
226
267
|
argsFromBody,
|
|
268
|
+
wrapFunction,
|
|
269
|
+
requireHostedApiKey,
|
|
270
|
+
statusCodeFromResult,
|
|
227
271
|
funcApi,
|
|
228
272
|
};
|