@foxtware/mineral 0.1.4 → 0.1.6
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 +1 -4
- package/README.md +1 -1
- package/_build_scripts/createNewFunction.js +248 -38
- package/bin/mineral.js +1 -1
- package/hosting/.hosting.yml.sample +23 -0
- package/hosting/copyCredsToEnv.js +48 -0
- package/{_deploy_scripts → hosting}/deployFromHostingYml.js +14 -36
- package/hosting/generateHosted.js +60 -0
- package/hosting/handlerPaths.js +23 -0
- package/{hosting.utils.js → hosting/hosting.utils.js} +73 -91
- package/hosting/wrappers.js +37 -0
- package/package.json +2 -2
- package/server.js +3 -8
- package/server.utils.js +0 -34
- package/.hosting.yml.sample +0 -27
- package/_build_scripts/copyCredsToEnv.js +0 -45
- package/_deploy_scripts/generateHosted.js +0 -57
- /package/{_deploy_scripts → hosting}/execCommand.js +0 -0
- /package/{_deploy_scripts → hosting}/setEnvVarsGcloud.js +0 -0
package/.gcloudignore
CHANGED
|
@@ -7,11 +7,8 @@
|
|
|
7
7
|
# $ gcloud topic gcloudignore
|
|
8
8
|
#
|
|
9
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
10
|
.git
|
|
14
11
|
.gitignore
|
|
15
12
|
|
|
16
13
|
node_modules
|
|
17
|
-
|
|
14
|
+
.creds.yml
|
package/README.md
CHANGED
|
@@ -51,4 +51,4 @@ The [bedrock](https://github.com/GorgonFreeman/bedrock) middleware, refactored f
|
|
|
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
53
|
|
|
54
|
-
For cloud deploy, workspaces use
|
|
54
|
+
For cloud deploy, workspaces use `hosting/.hosting.yml` and `npm run host` (same `--workspace` / `--api_dirs` flags as dev/serve). See `hosting/.hosting.yml.sample`.
|
|
@@ -3,31 +3,51 @@ const path = require('path');
|
|
|
3
3
|
const { spawn } = require('child_process');
|
|
4
4
|
const { askQuestion, capitaliseString } = require('../api/utils');
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
const
|
|
6
|
+
const mineralRoot = path.join(__dirname, '..');
|
|
7
|
+
const mineralApiDirectory = path.join(mineralRoot, 'api');
|
|
8
|
+
const rootExampleJsPath = path.join(mineralApiDirectory, '_example.js');
|
|
8
9
|
|
|
9
10
|
const excludedDirs = new Set([
|
|
10
11
|
'node_modules',
|
|
11
12
|
]);
|
|
12
13
|
|
|
13
|
-
const
|
|
14
|
+
const normalizePath = (dirPath) => dirPath.replace(/\/$/, '');
|
|
15
|
+
|
|
16
|
+
const isMineralCwd = () => normalizePath(process.cwd()) === normalizePath(mineralRoot);
|
|
17
|
+
|
|
18
|
+
const getContext = () => {
|
|
19
|
+
const inMineral = isMineralCwd();
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
inMineral,
|
|
23
|
+
apiDirectory: inMineral ? mineralApiDirectory : path.join(process.cwd(), 'api'),
|
|
24
|
+
mineralApiDirectory,
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const printHelp = (context) => {
|
|
29
|
+
const templateHelp = context.inMineral
|
|
30
|
+
? `example template to copy (default: _example.js in dir, else api/_example.js)
|
|
31
|
+
examples: _example.js, getsingle, _example.getsingle.js`
|
|
32
|
+
: `mineral example template to copy (default: _example.js)
|
|
33
|
+
examples: _example.js, shopify/_example.js, shopify/getsingle`;
|
|
34
|
+
|
|
14
35
|
console.log(`
|
|
15
36
|
Usage:
|
|
16
37
|
npm run new
|
|
17
38
|
npm run new -- --dir <apiSubdir> --name <name> [--template <exampleTemplate>] [--commit]
|
|
18
39
|
|
|
19
40
|
Options:
|
|
20
|
-
--dir, -d api subdirectory (e.g. peoplevox, shopify)
|
|
41
|
+
--dir, -d api subdirectory (e.g. peoplevox, shopify). Omit for flat api/ dirs.
|
|
21
42
|
--name, -n function name suffix (e.g. orderGet → peoplevoxOrderGet)
|
|
22
|
-
--template, -t
|
|
23
|
-
examples: _example.js, getsingle, _example.getsingle.js
|
|
43
|
+
--template, -t ${ templateHelp }
|
|
24
44
|
--commit auto-commit the stub after creation
|
|
25
45
|
--help, -h show this help
|
|
26
46
|
|
|
27
47
|
Examples:
|
|
28
48
|
npm run new -- --dir peoplevox --name orderGet
|
|
29
|
-
npm run new -- --dir peoplevox --name orderGet --template _example.js
|
|
30
49
|
npm run new -- --dir shopify --name pageGet --template getsingle
|
|
50
|
+
npm run new -- --name geodeBye --template shopify/_example.js
|
|
31
51
|
`);
|
|
32
52
|
};
|
|
33
53
|
|
|
@@ -124,11 +144,51 @@ const findExampleFiles = async (dirPath) => {
|
|
|
124
144
|
}
|
|
125
145
|
};
|
|
126
146
|
|
|
147
|
+
const collectMineralExampleFiles = async (dirPath, relativePrefix = '') => {
|
|
148
|
+
const results = [];
|
|
149
|
+
const localExamples = await findExampleFiles(dirPath);
|
|
150
|
+
|
|
151
|
+
for (const file of localExamples) {
|
|
152
|
+
const relativePath = relativePrefix ? `${ relativePrefix }/${ file.filename }` : file.filename;
|
|
153
|
+
|
|
154
|
+
results.push({
|
|
155
|
+
...file,
|
|
156
|
+
relativePath,
|
|
157
|
+
displayName: relativePath,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let childDirs = [];
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const dirents = await fs.readdir(dirPath, { withFileTypes: true });
|
|
165
|
+
childDirs = dirents
|
|
166
|
+
.filter(dirent => dirent.isDirectory())
|
|
167
|
+
.map(dirent => dirent.name)
|
|
168
|
+
.filter(name => name[0] !== '_' && name[0] !== '.')
|
|
169
|
+
.filter(name => !excludedDirs.has(name));
|
|
170
|
+
} catch (err) {
|
|
171
|
+
return results;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (const childDir of childDirs) {
|
|
175
|
+
const childPrefix = relativePrefix ? `${ relativePrefix }/${ childDir }` : childDir;
|
|
176
|
+
const childResults = await collectMineralExampleFiles(path.join(dirPath, childDir), childPrefix);
|
|
177
|
+
results.push(...childResults);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return results;
|
|
181
|
+
};
|
|
182
|
+
|
|
127
183
|
const sortExampleFiles = (exampleFiles) => {
|
|
128
184
|
return [...exampleFiles].sort((a, b) => {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
185
|
+
const aKey = a.relativePath || a.displayName;
|
|
186
|
+
const bKey = b.relativePath || b.displayName;
|
|
187
|
+
|
|
188
|
+
if (aKey === '_example.js') return -1;
|
|
189
|
+
if (bKey === '_example.js') return 1;
|
|
190
|
+
|
|
191
|
+
return aKey.localeCompare(bKey);
|
|
132
192
|
});
|
|
133
193
|
};
|
|
134
194
|
|
|
@@ -148,7 +208,68 @@ const normalizeTemplateArg = (templateArg) => {
|
|
|
148
208
|
return `_example.${ templateArg }.js`;
|
|
149
209
|
};
|
|
150
210
|
|
|
151
|
-
const
|
|
211
|
+
const findWorkspaceTemplate = (exampleFiles, templateArg) => {
|
|
212
|
+
if (!templateArg) {
|
|
213
|
+
const defaultTemplate = exampleFiles.find(file => file.relativePath === '_example.js');
|
|
214
|
+
if (defaultTemplate) {
|
|
215
|
+
return defaultTemplate;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (exampleFiles.length === 0) {
|
|
219
|
+
throw new Error('No mineral example templates found');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return exampleFiles[0];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const directMatch = exampleFiles.find(file => file.relativePath === templateArg);
|
|
226
|
+
if (directMatch) {
|
|
227
|
+
return directMatch;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const normalizedFilename = normalizeTemplateArg(templateArg);
|
|
231
|
+
const filenameMatches = exampleFiles.filter(file => file.filename === normalizedFilename);
|
|
232
|
+
|
|
233
|
+
if (templateArg.includes('/')) {
|
|
234
|
+
const [ prefix, suffix ] = templateArg.split('/');
|
|
235
|
+
const normalizedSuffix = normalizeTemplateArg(suffix);
|
|
236
|
+
const prefixedMatch = exampleFiles.find(file => file.relativePath === `${ prefix }/${ normalizedSuffix }`);
|
|
237
|
+
|
|
238
|
+
if (prefixedMatch) {
|
|
239
|
+
return prefixedMatch;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (filenameMatches.length === 1) {
|
|
244
|
+
return filenameMatches[0];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const suffixMatches = exampleFiles.filter(file => file.relativePath.endsWith(`/${ normalizedFilename }`));
|
|
248
|
+
|
|
249
|
+
if (suffixMatches.length === 1) {
|
|
250
|
+
return suffixMatches[0];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (suffixMatches.length > 1) {
|
|
254
|
+
throw new Error(
|
|
255
|
+
`Ambiguous template "${ templateArg }". Use one of: ${ suffixMatches.map(file => file.relativePath).join(', ') }`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
throw new Error(`Template not found: ${ templateArg }`);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const resolveTemplateFromArg = async ({
|
|
263
|
+
exampleFiles,
|
|
264
|
+
templateArg,
|
|
265
|
+
dir,
|
|
266
|
+
inMineral,
|
|
267
|
+
}) => {
|
|
268
|
+
if (!inMineral) {
|
|
269
|
+
const selectedTemplate = findWorkspaceTemplate(exampleFiles, templateArg);
|
|
270
|
+
return selectedTemplate.fullPath;
|
|
271
|
+
}
|
|
272
|
+
|
|
152
273
|
if (!templateArg) {
|
|
153
274
|
const defaultInDir = exampleFiles.find(file => file.displayName === '_example.js');
|
|
154
275
|
if (defaultInDir) {
|
|
@@ -184,13 +305,51 @@ const scriptFileContents = async (name, selectedTemplate) => {
|
|
|
184
305
|
return exampleFileContents.replace(/FUNC/g, name);
|
|
185
306
|
};
|
|
186
307
|
|
|
187
|
-
const getDirs = async () => {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
308
|
+
const getDirs = async (targetApiDirectory) => {
|
|
309
|
+
try {
|
|
310
|
+
const dirents = await fs.readdir(targetApiDirectory, { withFileTypes: true });
|
|
311
|
+
return dirents
|
|
312
|
+
.filter(dirent => dirent.isDirectory())
|
|
313
|
+
.map(dirent => dirent.name)
|
|
314
|
+
.filter(name => name[0] !== '_' && name[0] !== '.')
|
|
315
|
+
.filter(name => !excludedDirs.has(name));
|
|
316
|
+
} catch (err) {
|
|
317
|
+
if (err.code === 'ENOENT') {
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
throw err;
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const normalizeDirArg = (dirArg) => {
|
|
326
|
+
if (!dirArg || dirArg === '.' || dirArg === 'api') {
|
|
327
|
+
return '';
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return dirArg;
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const resolveDirArg = ({ dirArg, dirs, inMineral, name }) => {
|
|
334
|
+
const normalizedDir = normalizeDirArg(dirArg);
|
|
335
|
+
|
|
336
|
+
if (dirs.includes(normalizedDir)) {
|
|
337
|
+
return normalizedDir;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (!inMineral && dirs.length === 0 && normalizedDir === '') {
|
|
341
|
+
return '';
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (!inMineral && !dirArg && name) {
|
|
345
|
+
return '';
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (inMineral && !dirArg) {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return normalizedDir;
|
|
194
353
|
};
|
|
195
354
|
|
|
196
355
|
const buildFuncName = (dir, name) => {
|
|
@@ -201,6 +360,11 @@ const buildFuncName = (dir, name) => {
|
|
|
201
360
|
};
|
|
202
361
|
|
|
203
362
|
const selectDirInteractive = async (dirs) => {
|
|
363
|
+
if (dirs.length === 0) {
|
|
364
|
+
console.log('\nCreating in api/');
|
|
365
|
+
return '';
|
|
366
|
+
}
|
|
367
|
+
|
|
204
368
|
const dirIndex = await askQuestion(`Where does your new function live? \n${
|
|
205
369
|
dirs.map((dir, index) => {
|
|
206
370
|
return `[${ index + 1 }] ${ dir }`;
|
|
@@ -216,33 +380,54 @@ const selectDirInteractive = async (dirs) => {
|
|
|
216
380
|
return dir;
|
|
217
381
|
};
|
|
218
382
|
|
|
219
|
-
const
|
|
383
|
+
const getExampleFilesForContext = async ({ context, dir }) => {
|
|
384
|
+
if (context.inMineral) {
|
|
385
|
+
return sortExampleFiles(await findExampleFiles(path.join(context.apiDirectory, dir)));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return sortExampleFiles(await collectMineralExampleFiles(context.mineralApiDirectory));
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const formatTemplateLabel = (file, { inMineral }) => {
|
|
392
|
+
if (inMineral) {
|
|
393
|
+
return file.displayName;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return file.relativePath;
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const selectTemplateInteractive = async ({ exampleFiles, dir, inMineral }) => {
|
|
220
400
|
if (exampleFiles.length === 0) {
|
|
221
401
|
await fs.access(rootExampleJsPath);
|
|
222
|
-
console.log('\nUsing template:
|
|
402
|
+
console.log('\nUsing template: _example.js');
|
|
223
403
|
return rootExampleJsPath;
|
|
224
404
|
}
|
|
225
405
|
|
|
226
406
|
if (exampleFiles.length === 1) {
|
|
227
|
-
console.log(`\nUsing template: ${ exampleFiles[0]
|
|
407
|
+
console.log(`\nUsing template: ${ formatTemplateLabel(exampleFiles[0], { inMineral }) }`);
|
|
228
408
|
return exampleFiles[0].fullPath;
|
|
229
409
|
}
|
|
230
410
|
|
|
231
|
-
|
|
411
|
+
const templateScope = inMineral ? `in api/${ dir }` : 'from mineral';
|
|
412
|
+
console.log(`\nFound ${ exampleFiles.length } template(s) ${ templateScope }:`);
|
|
413
|
+
|
|
232
414
|
const templateIndex = await askQuestion(`Which template would you like to use? (press enter for _example.js) \n${
|
|
233
415
|
exampleFiles.map((file, index) => {
|
|
234
|
-
return `[${ index + 1 }] ${ file
|
|
416
|
+
return `[${ index + 1 }] ${ formatTemplateLabel(file, { inMineral }) }`;
|
|
235
417
|
}).join('\n')
|
|
236
418
|
}\n`);
|
|
237
419
|
|
|
238
420
|
if (!templateIndex || templateIndex.trim() === '') {
|
|
239
|
-
const defaultFile = exampleFiles.find(file =>
|
|
421
|
+
const defaultFile = exampleFiles.find(file => (
|
|
422
|
+
file.relativePath === '_example.js' || file.displayName === '_example.js'
|
|
423
|
+
));
|
|
424
|
+
|
|
240
425
|
if (defaultFile) {
|
|
241
426
|
return defaultFile.fullPath;
|
|
242
427
|
}
|
|
243
428
|
|
|
244
429
|
await fs.access(rootExampleJsPath);
|
|
245
|
-
console.log('\nUsing template:
|
|
430
|
+
console.log('\nUsing template: _example.js');
|
|
246
431
|
return rootExampleJsPath;
|
|
247
432
|
}
|
|
248
433
|
|
|
@@ -266,6 +451,7 @@ const selectNameInteractive = async (dir) => {
|
|
|
266
451
|
};
|
|
267
452
|
|
|
268
453
|
const writeNewFunction = async ({
|
|
454
|
+
apiDirectory,
|
|
269
455
|
dir,
|
|
270
456
|
name,
|
|
271
457
|
selectedTemplate,
|
|
@@ -283,6 +469,8 @@ const writeNewFunction = async ({
|
|
|
283
469
|
}
|
|
284
470
|
}
|
|
285
471
|
|
|
472
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
473
|
+
|
|
286
474
|
const script = await scriptFileContents(funcName, selectedTemplate);
|
|
287
475
|
await fs.writeFile(outputPath, script);
|
|
288
476
|
|
|
@@ -296,38 +484,55 @@ const writeNewFunction = async ({
|
|
|
296
484
|
};
|
|
297
485
|
|
|
298
486
|
const createNewFunction = async () => {
|
|
487
|
+
const context = getContext();
|
|
299
488
|
const cliArgs = parseCliArgs(process.argv.slice(2));
|
|
300
489
|
|
|
301
490
|
if (cliArgs.help) {
|
|
302
|
-
printHelp();
|
|
491
|
+
printHelp(context);
|
|
303
492
|
return;
|
|
304
493
|
}
|
|
305
494
|
|
|
306
|
-
const dirs = await getDirs();
|
|
495
|
+
const dirs = await getDirs(context.apiDirectory);
|
|
307
496
|
const nonInteractive = Boolean(cliArgs.dir || cliArgs.name);
|
|
308
497
|
|
|
309
498
|
if (nonInteractive) {
|
|
310
|
-
if (!cliArgs.
|
|
311
|
-
console.error('Non-interactive mode requires
|
|
312
|
-
printHelp();
|
|
499
|
+
if (!cliArgs.name) {
|
|
500
|
+
console.error('Non-interactive mode requires --name.');
|
|
501
|
+
printHelp(context);
|
|
313
502
|
process.exitCode = 1;
|
|
314
503
|
return;
|
|
315
504
|
}
|
|
316
505
|
|
|
317
|
-
|
|
318
|
-
|
|
506
|
+
const dir = resolveDirArg({
|
|
507
|
+
dirArg: cliArgs.dir,
|
|
508
|
+
dirs,
|
|
509
|
+
inMineral: context.inMineral,
|
|
510
|
+
name: cliArgs.name,
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
if (dir === null || (!dirs.includes(dir) && !(dir === '' && dirs.length === 0 && !context.inMineral))) {
|
|
514
|
+
const availableDirs = dirs.length ? dirs.join(', ') : 'api/ (flat)';
|
|
515
|
+
console.error(`Invalid --dir "${ cliArgs.dir }". Available: ${ availableDirs }`);
|
|
319
516
|
process.exitCode = 1;
|
|
320
517
|
return;
|
|
321
518
|
}
|
|
322
519
|
|
|
323
|
-
const exampleFiles =
|
|
520
|
+
const exampleFiles = await getExampleFilesForContext({ context, dir });
|
|
324
521
|
|
|
325
522
|
try {
|
|
326
|
-
const selectedTemplate = await resolveTemplateFromArg(
|
|
327
|
-
|
|
523
|
+
const selectedTemplate = await resolveTemplateFromArg({
|
|
524
|
+
exampleFiles,
|
|
525
|
+
templateArg: cliArgs.template,
|
|
526
|
+
dir,
|
|
527
|
+
inMineral: context.inMineral,
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
const templateLabel = path.relative(mineralRoot, selectedTemplate);
|
|
531
|
+
console.log(`Using template: ${ templateLabel }`);
|
|
328
532
|
|
|
329
533
|
await writeNewFunction({
|
|
330
|
-
|
|
534
|
+
apiDirectory: context.apiDirectory,
|
|
535
|
+
dir,
|
|
331
536
|
name: cliArgs.name,
|
|
332
537
|
selectedTemplate,
|
|
333
538
|
shouldAutoCommit: process.env.AUTO_COMMIT_STUBS === 'true' || cliArgs.commit,
|
|
@@ -342,11 +547,16 @@ const createNewFunction = async () => {
|
|
|
342
547
|
|
|
343
548
|
try {
|
|
344
549
|
const dir = await selectDirInteractive(dirs);
|
|
345
|
-
const exampleFiles =
|
|
346
|
-
const selectedTemplate = await selectTemplateInteractive(
|
|
550
|
+
const exampleFiles = await getExampleFilesForContext({ context, dir });
|
|
551
|
+
const selectedTemplate = await selectTemplateInteractive({
|
|
552
|
+
exampleFiles,
|
|
553
|
+
dir,
|
|
554
|
+
inMineral: context.inMineral,
|
|
555
|
+
});
|
|
347
556
|
const name = await selectNameInteractive(dir);
|
|
348
557
|
|
|
349
558
|
await writeNewFunction({
|
|
559
|
+
apiDirectory: context.apiDirectory,
|
|
350
560
|
dir,
|
|
351
561
|
name,
|
|
352
562
|
selectedTemplate,
|
package/bin/mineral.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const command = process.argv[2];
|
|
4
4
|
|
|
5
5
|
if (command === 'host') {
|
|
6
|
-
const { deployFromHostingYml } = require('../
|
|
6
|
+
const { deployFromHostingYml } = require('../hosting/deployFromHostingYml');
|
|
7
7
|
deployFromHostingYml().catch((error) => {
|
|
8
8
|
console.error(error);
|
|
9
9
|
process.exit(1);
|
|
@@ -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
|
+
- allowCrossOriginCallsAndHandleOptions
|
|
14
|
+
# entry_point: otherHandlerName
|
|
15
|
+
|
|
16
|
+
pokemonPokeballThrow:
|
|
17
|
+
max_instances: 1
|
|
18
|
+
wrappers:
|
|
19
|
+
- checkTrainer
|
|
20
|
+
|
|
21
|
+
groups:
|
|
22
|
+
example_group:
|
|
23
|
+
- exampleFunction
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const yaml = require('yaml');
|
|
3
|
+
|
|
4
|
+
const copyCredsToEnv = (workspace) => {
|
|
5
|
+
const normalizedWorkspace = workspace.replace(/\/$/, '');
|
|
6
|
+
const credsPath = `${ normalizedWorkspace }/.creds.yml`;
|
|
7
|
+
const envPath = `${ normalizedWorkspace }/.env`;
|
|
8
|
+
|
|
9
|
+
if (!fs.existsSync(credsPath)) {
|
|
10
|
+
throw new Error(`Missing .creds.yml in workspace: ${ workspace }`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const credsText = fs.readFileSync(credsPath, 'utf8');
|
|
14
|
+
const credsFromYml = yaml.parse(credsText);
|
|
15
|
+
const newCredsLine = `CREDS=${ JSON.stringify(credsFromYml) }`;
|
|
16
|
+
|
|
17
|
+
let envFileContents = '';
|
|
18
|
+
if (fs.existsSync(envPath)) {
|
|
19
|
+
envFileContents = fs.readFileSync(envPath, 'utf8');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!envFileContents) {
|
|
23
|
+
fs.writeFileSync(envPath, `${ newCredsLine }\n`);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (/^CREDS=/m.test(envFileContents)) {
|
|
28
|
+
const updatedFileContents = envFileContents.replace(/^CREDS=.*$/m, newCredsLine);
|
|
29
|
+
|
|
30
|
+
if (updatedFileContents !== envFileContents) {
|
|
31
|
+
fs.writeFileSync(envPath, updatedFileContents);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fs.appendFileSync(envPath, `\n\n${ newCredsLine }\n`);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
if (require.main === module) {
|
|
41
|
+
const workspace = process.argv[2] || process.cwd();
|
|
42
|
+
copyCredsToEnv(workspace);
|
|
43
|
+
console.log('Copied CREDS from .creds.yml to .env');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = {
|
|
47
|
+
copyCredsToEnv,
|
|
48
|
+
};
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
const readline = require('readline');
|
|
2
|
-
const { toAbsolutePath, setWorkspace } = require('../api/workspace');
|
|
2
|
+
const { toAbsolutePath, setWorkspace, loadWorkspaceEnv } = require('../api/workspace');
|
|
3
3
|
const { getApiDirs, readCliFlag } = require('../cli');
|
|
4
4
|
const { loadHandlers } = require('../server');
|
|
5
5
|
const {
|
|
6
6
|
readHostingYml,
|
|
7
|
-
|
|
8
|
-
validateEnvVarsForDeploy,
|
|
7
|
+
ensureWorkspaceEnvForDeploy,
|
|
9
8
|
resolveHostedHandlersForDeploy,
|
|
10
9
|
functionUsesWrapper,
|
|
11
|
-
} = require('
|
|
10
|
+
} = require('./hosting.utils');
|
|
12
11
|
const { writeHostedJs } = require('./generateHosted');
|
|
13
12
|
const { execCommand } = require('./execCommand');
|
|
14
13
|
const { formatSetEnvVarsForGcloud, shellQuoteSingle } = require('./setEnvVarsGcloud');
|
|
@@ -91,8 +90,6 @@ const deployFunction = async ({
|
|
|
91
90
|
functionConfig,
|
|
92
91
|
googleCloudInfo,
|
|
93
92
|
workspace,
|
|
94
|
-
credsJson,
|
|
95
|
-
deployEnvVars = {},
|
|
96
93
|
}) => {
|
|
97
94
|
const config = {
|
|
98
95
|
...googleCloudInfo,
|
|
@@ -107,20 +104,16 @@ const deployFunction = async ({
|
|
|
107
104
|
allow_unauthenticated: allowUnauthenticated = true,
|
|
108
105
|
gen2 = true,
|
|
109
106
|
set_env_vars: extraSetEnvVars,
|
|
110
|
-
entry_point
|
|
107
|
+
entry_point,
|
|
111
108
|
schedules,
|
|
112
109
|
groups,
|
|
113
110
|
wrappers,
|
|
114
111
|
source,
|
|
112
|
+
env,
|
|
115
113
|
...gcloudArgs
|
|
116
114
|
} = config;
|
|
117
115
|
|
|
118
|
-
const
|
|
119
|
-
const envParts = [
|
|
120
|
-
'HOSTED=true',
|
|
121
|
-
`CREDS=${ credsJson }`,
|
|
122
|
-
...Object.entries(deployEnvVars).map(([envName, envValue]) => `${ envName }=${ envValue }`),
|
|
123
|
-
];
|
|
116
|
+
const envParts = ['HOSTED=true'];
|
|
124
117
|
|
|
125
118
|
if (extraSetEnvVars) {
|
|
126
119
|
envParts.push(extraSetEnvVars);
|
|
@@ -134,7 +127,7 @@ const deployFunction = async ({
|
|
|
134
127
|
`--project ${ project }`,
|
|
135
128
|
`--region ${ region }`,
|
|
136
129
|
`--source ${ shellQuoteSingle(workspace) }`,
|
|
137
|
-
`--entry-point ${
|
|
130
|
+
`--entry-point ${ functionName }`,
|
|
138
131
|
`--trigger-${ trigger }`,
|
|
139
132
|
`--runtime ${ runtime }`,
|
|
140
133
|
`--set-env-vars ${ shellQuoteSingle(setEnvVarsForGcloud) }`,
|
|
@@ -157,7 +150,7 @@ const deployFunction = async ({
|
|
|
157
150
|
}
|
|
158
151
|
|
|
159
152
|
const requiresHostedApiKey = functionUsesWrapper(functionConfig, 'requireHostedApiKey');
|
|
160
|
-
const hostedApiKey =
|
|
153
|
+
const hostedApiKey = process.env.HOSTED_API_KEY;
|
|
161
154
|
|
|
162
155
|
for (const schedule of schedules) {
|
|
163
156
|
const {
|
|
@@ -203,17 +196,18 @@ const deployFunction = async ({
|
|
|
203
196
|
const deployFromHostingYml = async (options = {}) => {
|
|
204
197
|
const config = getHostConfig(options);
|
|
205
198
|
setWorkspace(config.workspace);
|
|
199
|
+
ensureWorkspaceEnvForDeploy(config.workspace);
|
|
200
|
+
loadWorkspaceEnv();
|
|
206
201
|
|
|
207
202
|
const hostingConfig = readHostingYml(config.workspace);
|
|
208
203
|
const {
|
|
209
204
|
google_cloud_info: googleCloudInfo,
|
|
210
|
-
env: envNames = [],
|
|
211
205
|
functions = {},
|
|
212
206
|
groups = {},
|
|
213
207
|
} = hostingConfig;
|
|
214
208
|
|
|
215
209
|
if (!googleCloudInfo?.project || !googleCloudInfo?.region) {
|
|
216
|
-
throw new Error('
|
|
210
|
+
throw new Error('hosting/.hosting.yml requires google_cloud_info.project and google_cloud_info.region');
|
|
217
211
|
}
|
|
218
212
|
|
|
219
213
|
const handlers = loadHandlers({
|
|
@@ -227,19 +221,16 @@ const deployFromHostingYml = async (options = {}) => {
|
|
|
227
221
|
handlersByName,
|
|
228
222
|
});
|
|
229
223
|
|
|
230
|
-
const deployEnvVars = validateEnvVarsForDeploy(config.workspace, envNames);
|
|
231
|
-
|
|
232
224
|
writeHostedJs({
|
|
233
225
|
workspace: config.workspace,
|
|
234
226
|
hostedHandlers,
|
|
235
227
|
});
|
|
236
228
|
|
|
237
|
-
const credsJson = getCredsJsonForDeploy(config.workspace);
|
|
238
229
|
const deployArgs = getDeployArgs();
|
|
239
230
|
|
|
240
231
|
const deployOne = async (functionName) => {
|
|
241
232
|
if (!functions[functionName]) {
|
|
242
|
-
console.log(`Function ${ functionName } not found in
|
|
233
|
+
console.log(`Function ${ functionName } not found in hosting/.hosting.yml, skipping`);
|
|
243
234
|
return;
|
|
244
235
|
}
|
|
245
236
|
|
|
@@ -248,15 +239,13 @@ const deployFromHostingYml = async (options = {}) => {
|
|
|
248
239
|
functionConfig: functions[functionName],
|
|
249
240
|
googleCloudInfo,
|
|
250
241
|
workspace: config.workspace,
|
|
251
|
-
credsJson,
|
|
252
|
-
deployEnvVars,
|
|
253
242
|
});
|
|
254
243
|
};
|
|
255
244
|
|
|
256
245
|
if (deployArgs.includes('group')) {
|
|
257
246
|
const groupNames = Object.keys(groups);
|
|
258
247
|
if (!groupNames.length) {
|
|
259
|
-
console.log('No groups defined in
|
|
248
|
+
console.log('No groups defined in hosting/.hosting.yml');
|
|
260
249
|
return;
|
|
261
250
|
}
|
|
262
251
|
|
|
@@ -270,7 +259,7 @@ const deployFromHostingYml = async (options = {}) => {
|
|
|
270
259
|
if (deployArgs.includes('function')) {
|
|
271
260
|
const functionNames = Object.keys(functions);
|
|
272
261
|
if (!functionNames.length) {
|
|
273
|
-
console.log('No functions defined in
|
|
262
|
+
console.log('No functions defined in hosting/.hosting.yml');
|
|
274
263
|
return;
|
|
275
264
|
}
|
|
276
265
|
|
|
@@ -305,14 +294,3 @@ module.exports = {
|
|
|
305
294
|
deployFromHostingYml,
|
|
306
295
|
getHostConfig,
|
|
307
296
|
};
|
|
308
|
-
|
|
309
|
-
/*
|
|
310
|
-
Deploy everything:
|
|
311
|
-
mineral host all
|
|
312
|
-
|
|
313
|
-
Deploy a single function:
|
|
314
|
-
mineral host function
|
|
315
|
-
|
|
316
|
-
Deploy a group of functions:
|
|
317
|
-
mineral host group
|
|
318
|
-
*/
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const { wrapHostedFunction } = require('./hosting.utils');
|
|
3
|
+
|
|
4
|
+
const formatResolvedWrapperForHostedJs = ({ modulePath, wrapperName }) => (
|
|
5
|
+
`require('${ modulePath }').${ wrapperName }`
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
const formatWrappersArg = (resolvedWrappers = []) => {
|
|
9
|
+
if (!resolvedWrappers.length) {
|
|
10
|
+
return '';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const wrapperLines = resolvedWrappers.map((resolvedWrapper) => (
|
|
14
|
+
` ${ formatResolvedWrapperForHostedJs(resolvedWrapper) },`
|
|
15
|
+
));
|
|
16
|
+
return `, [\n${ wrapperLines.join('\n') }\n ]`;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const generateHostedJs = ({
|
|
20
|
+
hostedHandlers,
|
|
21
|
+
}) => {
|
|
22
|
+
const exportLines = [];
|
|
23
|
+
|
|
24
|
+
for (const hostedHandler of hostedHandlers) {
|
|
25
|
+
const { hostedName, handlerName, resolvedWrappers = [], requirePath } = hostedHandler;
|
|
26
|
+
const wrappersArg = formatWrappersArg(resolvedWrappers);
|
|
27
|
+
|
|
28
|
+
exportLines.push(
|
|
29
|
+
` ${ hostedName }: wrapHostedFunction(() => require('${ requirePath }'), '${ handlerName }'${ wrappersArg }),`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return `// Generated by mineral — do not edit
|
|
34
|
+
const { wrapHostedFunction } = require('@foxtware/mineral/hosting/hosting.utils');
|
|
35
|
+
|
|
36
|
+
module.exports = {
|
|
37
|
+
${ exportLines.join('\n') }
|
|
38
|
+
};
|
|
39
|
+
`;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const writeHostedJs = ({
|
|
43
|
+
workspace,
|
|
44
|
+
hostedHandlers,
|
|
45
|
+
}) => {
|
|
46
|
+
const { getHostingDir } = require('./hosting.utils');
|
|
47
|
+
const hostedPath = `${ getHostingDir(workspace) }/hosted.js`;
|
|
48
|
+
const content = generateHostedJs({
|
|
49
|
+
hostedHandlers,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
fs.mkdirSync(getHostingDir(workspace), { recursive: true });
|
|
53
|
+
fs.writeFileSync(hostedPath, content);
|
|
54
|
+
return hostedPath;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
generateHostedJs,
|
|
59
|
+
writeHostedJs,
|
|
60
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const MINERAL_ROOT = `${ __dirname }/..`;
|
|
2
|
+
const MINERAL_API_DIR = `${ MINERAL_ROOT }/api`;
|
|
3
|
+
|
|
4
|
+
const getRequirePathForHandler = (handler, workspace) => {
|
|
5
|
+
const normalizedWorkspace = workspace.replace(/\/$/, '');
|
|
6
|
+
const { filePath } = handler;
|
|
7
|
+
|
|
8
|
+
if (filePath.startsWith(`${ normalizedWorkspace }/`)) {
|
|
9
|
+
return `./${ filePath.slice(normalizedWorkspace.length + 1) }`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (filePath.startsWith(`${ MINERAL_API_DIR }/`)) {
|
|
13
|
+
const relativePath = filePath.slice(MINERAL_API_DIR.length + 1);
|
|
14
|
+
return `@foxtware/mineral/api/${ relativePath }`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
throw new Error(`Handler "${ handler.routeName }" is not in workspace or mineral api dirs`);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
MINERAL_API_DIR,
|
|
22
|
+
getRequirePathForHandler,
|
|
23
|
+
};
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const dotenv = require('dotenv');
|
|
2
4
|
const { createRequire } = require('module');
|
|
3
5
|
const yaml = require('yaml');
|
|
6
|
+
const { getRequirePathForHandler } = require('./handlerPaths');
|
|
4
7
|
const {
|
|
5
8
|
respondJson,
|
|
6
9
|
errorToReadable,
|
|
@@ -9,63 +12,71 @@ const {
|
|
|
9
12
|
funcApi,
|
|
10
13
|
wrapFunction,
|
|
11
14
|
statusCodeFromResult,
|
|
12
|
-
} = require('
|
|
15
|
+
} = require('../server.utils');
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
dotenv.config({
|
|
18
|
+
path: path.join(process.cwd(), '.env'),
|
|
19
|
+
});
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
21
|
+
const MINERAL_WRAPPERS_MODULE = '@foxtware/mineral/hosting/wrappers.js';
|
|
22
|
+
const WORKSPACE_WRAPPERS_MODULE = './hosting/wrappers.js';
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
modulePath: wrapperRef.slice(0, hashIndex),
|
|
23
|
-
exportName: wrapperRef.slice(hashIndex + 1),
|
|
24
|
-
};
|
|
25
|
-
};
|
|
24
|
+
const getHostingDir = (workspace) => `${ workspace.replace(/\/$/, '') }/hosting`;
|
|
26
25
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
const getMineralWrappers = (workspaceRequire) => {
|
|
27
|
+
try {
|
|
28
|
+
return workspaceRequire(MINERAL_WRAPPERS_MODULE);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (error.code !== 'MODULE_NOT_FOUND') {
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
throw new Error(`Wrapper export not found: ${ exportName } in ${ modulePath }`);
|
|
34
|
+
return require('./wrappers.js');
|
|
34
35
|
}
|
|
35
36
|
};
|
|
36
37
|
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
const getHostedEntries = (functions = {}) => {
|
|
42
|
-
const byEntryPoint = new Map();
|
|
38
|
+
const resolveWrapperName = (wrapperName, workspaceRequire) => {
|
|
39
|
+
if (typeof wrapperName !== 'string' || !wrapperName.trim()) {
|
|
40
|
+
throw new Error(`Invalid wrapper name: ${ wrapperName }`);
|
|
41
|
+
}
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
const mineralWrappers = getMineralWrappers(workspaceRequire);
|
|
44
|
+
if (typeof mineralWrappers[wrapperName] === 'function') {
|
|
45
|
+
return {
|
|
46
|
+
wrapperName,
|
|
47
|
+
modulePath: MINERAL_WRAPPERS_MODULE,
|
|
49
48
|
};
|
|
49
|
+
}
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
try {
|
|
52
|
+
const workspaceWrappers = workspaceRequire(WORKSPACE_WRAPPERS_MODULE);
|
|
53
|
+
if (typeof workspaceWrappers[wrapperName] === 'function') {
|
|
54
|
+
return {
|
|
55
|
+
wrapperName,
|
|
56
|
+
modulePath: WORKSPACE_WRAPPERS_MODULE,
|
|
57
|
+
};
|
|
54
58
|
}
|
|
55
|
-
|
|
56
|
-
if (
|
|
57
|
-
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error.code !== 'MODULE_NOT_FOUND') {
|
|
61
|
+
throw error;
|
|
58
62
|
}
|
|
59
|
-
|
|
60
|
-
byEntryPoint.set(entryPoint, existing);
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Wrapper "${ wrapperName }" not found in ${ MINERAL_WRAPPERS_MODULE } or ${ WORKSPACE_WRAPPERS_MODULE }`,
|
|
67
|
+
);
|
|
64
68
|
};
|
|
65
69
|
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
const getHostedEntries = (functions = {}) => (
|
|
71
|
+
Object.entries(functions).map(([hostedName, functionConfig = {}]) => ({
|
|
72
|
+
hostedName,
|
|
73
|
+
handlerName: functionConfig.entry_point || functionConfig.entryPoint || hostedName,
|
|
74
|
+
wrappers: Array.isArray(functionConfig.wrappers) ? functionConfig.wrappers : [],
|
|
75
|
+
}))
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const functionUsesWrapper = (functionConfig = {}, wrapperName) => (
|
|
79
|
+
Array.isArray(functionConfig.wrappers) && functionConfig.wrappers.includes(wrapperName)
|
|
69
80
|
);
|
|
70
81
|
|
|
71
82
|
const getFuncApiConfig = ({
|
|
@@ -153,17 +164,17 @@ const wrapHostedFunction = (loader, exportName, wrappers = []) => {
|
|
|
153
164
|
};
|
|
154
165
|
|
|
155
166
|
const readHostingYml = (workspace) => {
|
|
156
|
-
const hostingPath = `${ workspace }/.hosting.yml`;
|
|
167
|
+
const hostingPath = `${ getHostingDir(workspace) }/.hosting.yml`;
|
|
157
168
|
|
|
158
169
|
if (!fs.existsSync(hostingPath)) {
|
|
159
|
-
throw new Error(`Missing
|
|
170
|
+
throw new Error(`Missing hosting/.hosting.yml in workspace: ${ workspace }`);
|
|
160
171
|
}
|
|
161
172
|
|
|
162
173
|
const hostingText = fs.readFileSync(hostingPath, 'utf8');
|
|
163
174
|
const hostingConfig = yaml.parse(hostingText);
|
|
164
175
|
|
|
165
176
|
if (!hostingConfig || typeof hostingConfig !== 'object' || Array.isArray(hostingConfig)) {
|
|
166
|
-
throw new Error('Invalid
|
|
177
|
+
throw new Error('Invalid hosting/.hosting.yml');
|
|
167
178
|
}
|
|
168
179
|
|
|
169
180
|
return hostingConfig;
|
|
@@ -180,33 +191,15 @@ const getCredsJsonForDeploy = (workspace) => {
|
|
|
180
191
|
return JSON.stringify(yaml.parse(credsText));
|
|
181
192
|
};
|
|
182
193
|
|
|
183
|
-
const
|
|
184
|
-
const
|
|
194
|
+
const ensureWorkspaceEnvForDeploy = (workspace) => {
|
|
195
|
+
const { copyCredsToEnv } = require('./copyCredsToEnv');
|
|
196
|
+
const envPath = `${ workspace.replace(/\/$/, '') }/.env`;
|
|
185
197
|
|
|
186
|
-
|
|
187
|
-
return '';
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const envText = fs.readFileSync(envPath, 'utf8');
|
|
191
|
-
const match = envText.match(new RegExp(`^${ envName }=(.*)$`, 'm'));
|
|
192
|
-
return match ? match[1].trim() : '';
|
|
193
|
-
};
|
|
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]);
|
|
198
|
+
copyCredsToEnv(workspace);
|
|
204
199
|
|
|
205
|
-
if (
|
|
206
|
-
throw new Error(`Missing
|
|
200
|
+
if (!fs.existsSync(envPath)) {
|
|
201
|
+
throw new Error(`Missing .env in workspace: ${ workspace }`);
|
|
207
202
|
}
|
|
208
|
-
|
|
209
|
-
return envVars;
|
|
210
203
|
};
|
|
211
204
|
|
|
212
205
|
const resolveHostedHandlersForDeploy = ({
|
|
@@ -218,35 +211,23 @@ const resolveHostedHandlersForDeploy = ({
|
|
|
218
211
|
const hostedEntries = getHostedEntries(functions);
|
|
219
212
|
|
|
220
213
|
return hostedEntries.map((hostedEntry) => {
|
|
221
|
-
const {
|
|
214
|
+
const { handlerName, wrappers = [] } = hostedEntry;
|
|
222
215
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (source) {
|
|
228
|
-
workspaceRequire.resolve(source);
|
|
229
|
-
const moduleExports = workspaceRequire(source);
|
|
230
|
-
if (typeof moduleExports[entryPoint] !== 'function') {
|
|
231
|
-
throw new Error(`Hosted export not found: ${ entryPoint } in ${ source }`);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return {
|
|
235
|
-
...hostedEntry,
|
|
236
|
-
requirePath: source,
|
|
237
|
-
};
|
|
238
|
-
}
|
|
216
|
+
const resolvedWrappers = wrappers.map((wrapperName) => (
|
|
217
|
+
resolveWrapperName(wrapperName, workspaceRequire)
|
|
218
|
+
));
|
|
239
219
|
|
|
240
|
-
const handler = handlersByName.get(
|
|
220
|
+
const handler = handlersByName.get(handlerName);
|
|
241
221
|
if (!handler) {
|
|
242
222
|
throw new Error(
|
|
243
|
-
`
|
|
223
|
+
`Function "${ handlerName }" not found — add a handler in workspace api dirs or mineral api`,
|
|
244
224
|
);
|
|
245
225
|
}
|
|
246
226
|
|
|
247
227
|
return {
|
|
248
228
|
...hostedEntry,
|
|
249
|
-
|
|
229
|
+
resolvedWrappers,
|
|
230
|
+
requirePath: getRequirePathForHandler(handler, workspace),
|
|
250
231
|
};
|
|
251
232
|
});
|
|
252
233
|
};
|
|
@@ -254,15 +235,16 @@ const resolveHostedHandlersForDeploy = ({
|
|
|
254
235
|
// TODO: support credsPayload in google_cloud_info instead of full workspace .creds.yml
|
|
255
236
|
|
|
256
237
|
module.exports = {
|
|
257
|
-
|
|
238
|
+
getHostingDir,
|
|
239
|
+
resolveWrapperName,
|
|
258
240
|
getFuncApiConfig,
|
|
259
241
|
wrapHostedFunction,
|
|
260
242
|
readHostingYml,
|
|
261
243
|
getCredsJsonForDeploy,
|
|
262
|
-
|
|
263
|
-
validateEnvVarsForDeploy,
|
|
244
|
+
ensureWorkspaceEnvForDeploy,
|
|
264
245
|
getHostedEntries,
|
|
265
246
|
resolveHostedHandlersForDeploy,
|
|
266
247
|
functionUsesWrapper,
|
|
267
|
-
|
|
248
|
+
MINERAL_WRAPPERS_MODULE,
|
|
249
|
+
WORKSPACE_WRAPPERS_MODULE,
|
|
268
250
|
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const { HOSTED } = require('../api/constants');
|
|
2
|
+
|
|
3
|
+
const requireHostedApiKey = async (req) => {
|
|
4
|
+
if (!HOSTED) {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (req.headers['x-api-key'] !== process.env.HOSTED_API_KEY) {
|
|
9
|
+
return {
|
|
10
|
+
ok: false,
|
|
11
|
+
error: {
|
|
12
|
+
code: 'UNAUTHORIZED',
|
|
13
|
+
message: 'Unauthorized',
|
|
14
|
+
statusCode: 401,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const allowCrossOriginCallsAndHandleOptions = async (req, res) => {
|
|
21
|
+
const { origin } = req.headers;
|
|
22
|
+
|
|
23
|
+
res.setHeader('Access-Control-Allow-Origin', origin || '*');
|
|
24
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
25
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, x-api-key, x-wf-token, x-wf-value');
|
|
26
|
+
|
|
27
|
+
if (req.method === 'OPTIONS') {
|
|
28
|
+
res.writeHead(204);
|
|
29
|
+
res.end();
|
|
30
|
+
return { handled: true };
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
module.exports = {
|
|
35
|
+
requireHostedApiKey,
|
|
36
|
+
allowCrossOriginCallsAndHandleOptions,
|
|
37
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@foxtware/mineral",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"bin": {
|
|
5
5
|
"mineral": "bin/mineral.js"
|
|
6
6
|
},
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"access": "public"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"creds_to_env": "node
|
|
12
|
+
"creds_to_env": "node hosting/copyCredsToEnv.js",
|
|
13
13
|
"dev": "node --watch --watch-path=./api --watch-path=./server.js --watch-path=./server.utils.js --watch-path=./.creds.yml server.js",
|
|
14
14
|
"new": "node _build_scripts/createNewFunction.js",
|
|
15
15
|
"serve": "PORT=8100 node server.js",
|
package/server.js
CHANGED
|
@@ -3,9 +3,8 @@ const http = require('http');
|
|
|
3
3
|
const { respondJson, errorToReadable, getRequestBody, argsFromBody, funcApi, statusCodeFromResult } = require('./server.utils');
|
|
4
4
|
const { getWorkspace, setWorkspace, loadWorkspaceEnv, toAbsolutePath } = require('./api/workspace');
|
|
5
5
|
const { getApiDirs, readCliFlag } = require('./cli');
|
|
6
|
-
const { getFuncApiConfig } = require('./hosting.utils');
|
|
7
|
-
|
|
8
|
-
const MINERAL_API_DIR = `${ __dirname }/api`;
|
|
6
|
+
const { getFuncApiConfig } = require('./hosting/hosting.utils');
|
|
7
|
+
const { MINERAL_API_DIR } = require('./hosting/handlerPaths');
|
|
9
8
|
|
|
10
9
|
const getConfig = (options = {}) => ({
|
|
11
10
|
port: Number(options.port ?? process.env.PORT ?? 8000),
|
|
@@ -69,7 +68,6 @@ const listJsFiles = (directory) => {
|
|
|
69
68
|
const directoriesToScan = ({
|
|
70
69
|
workspace,
|
|
71
70
|
api_dirs,
|
|
72
|
-
host_mode = false,
|
|
73
71
|
}) => {
|
|
74
72
|
const extraDirs = api_dirs.map((dir) => (
|
|
75
73
|
dir.startsWith('/')
|
|
@@ -77,10 +75,6 @@ const directoriesToScan = ({
|
|
|
77
75
|
: `${ workspace }/${ dir }`
|
|
78
76
|
));
|
|
79
77
|
|
|
80
|
-
if (host_mode && api_dirs.length) {
|
|
81
|
-
return extraDirs;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
78
|
return [MINERAL_API_DIR, ...extraDirs];
|
|
85
79
|
};
|
|
86
80
|
|
|
@@ -225,6 +219,7 @@ const startServer = (options = {}) => {
|
|
|
225
219
|
module.exports = {
|
|
226
220
|
startServer,
|
|
227
221
|
loadHandlers,
|
|
222
|
+
MINERAL_API_DIR,
|
|
228
223
|
get server() {
|
|
229
224
|
if (!server) {
|
|
230
225
|
startServer();
|
package/server.utils.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const { logDeep } = require('./api/utils');
|
|
2
2
|
const { StringDecoder } = require('string_decoder');
|
|
3
|
-
const { HOSTED } = require('./api/constants');
|
|
4
3
|
|
|
5
4
|
const respondJson = (res, statusCode, payload) => {
|
|
6
5
|
logDeep(payload);
|
|
@@ -132,37 +131,6 @@ const wrapFunction = (func, wrappers = []) => async (req, res, ...rest) => {
|
|
|
132
131
|
return func(req, res, ...rest);
|
|
133
132
|
};
|
|
134
133
|
|
|
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 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
|
-
|
|
166
134
|
const statusCodeFromResult = (result) => {
|
|
167
135
|
if (result?.ok === false) {
|
|
168
136
|
return result?.error?.statusCode ?? 400;
|
|
@@ -280,8 +248,6 @@ module.exports = {
|
|
|
280
248
|
getRequestBody,
|
|
281
249
|
argsFromBody,
|
|
282
250
|
wrapFunction,
|
|
283
|
-
requireHostedApiKey,
|
|
284
|
-
allowCrossOriginCallsAndHandleOptions,
|
|
285
251
|
statusCodeFromResult,
|
|
286
252
|
funcApi,
|
|
287
253
|
};
|
package/.hosting.yml.sample
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
google_cloud_info:
|
|
2
|
-
project:
|
|
3
|
-
region:
|
|
4
|
-
|
|
5
|
-
# TODO: consider credsPayload in google_cloud_info instead of workspace .creds.yml
|
|
6
|
-
|
|
7
|
-
env:
|
|
8
|
-
- HOSTED_API_KEY
|
|
9
|
-
|
|
10
|
-
functions:
|
|
11
|
-
exampleFunction:
|
|
12
|
-
max_instances: 1
|
|
13
|
-
timeout: 300s
|
|
14
|
-
wrappers:
|
|
15
|
-
- '@foxtware/mineral/server.utils#requireHostedApiKey'
|
|
16
|
-
- '@foxtware/mineral/server.utils#allowCrossOriginCallsAndHandleOptions'
|
|
17
|
-
# entry_point: otherExportName
|
|
18
|
-
|
|
19
|
-
packagedFunction:
|
|
20
|
-
source: '@foxtware/mineral/api/pokemon/pokemonPokeballThrow.js'
|
|
21
|
-
max_instances: 1
|
|
22
|
-
wrappers:
|
|
23
|
-
- '@foxtware/mineral/server.utils#checkTrainer'
|
|
24
|
-
|
|
25
|
-
groups:
|
|
26
|
-
example_group:
|
|
27
|
-
- exampleFunction
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
const fs = require('fs').promises;
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const yaml = require('yaml');
|
|
4
|
-
|
|
5
|
-
const mineralRoot = path.join(__dirname, '..');
|
|
6
|
-
const credsYmlPath = path.join(mineralRoot, '.creds.yml');
|
|
7
|
-
const envPath = path.join(mineralRoot, '.env');
|
|
8
|
-
|
|
9
|
-
(async () => {
|
|
10
|
-
const credsText = await fs.readFile(credsYmlPath, 'utf8');
|
|
11
|
-
const credsFromYml = yaml.parse(credsText);
|
|
12
|
-
const credsJsonString = JSON.stringify(credsFromYml);
|
|
13
|
-
const newCredsLine = `CREDS=${ credsJsonString }`;
|
|
14
|
-
|
|
15
|
-
let envFileContents = '';
|
|
16
|
-
try {
|
|
17
|
-
envFileContents = await fs.readFile(envPath, 'utf8');
|
|
18
|
-
} catch (err) {
|
|
19
|
-
if (err.code !== 'ENOENT') {
|
|
20
|
-
throw err;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (!envFileContents) {
|
|
25
|
-
await fs.writeFile(envPath, `${ newCredsLine }\n`);
|
|
26
|
-
console.log('Created .env with CREDS');
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (/^CREDS=/m.test(envFileContents)) {
|
|
31
|
-
const updatedFileContents = envFileContents.replace(/^CREDS=.*$/m, newCredsLine);
|
|
32
|
-
|
|
33
|
-
if (updatedFileContents === envFileContents) {
|
|
34
|
-
console.log('CREDS already up to date');
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
await fs.writeFile(envPath, updatedFileContents);
|
|
39
|
-
console.log('Updated CREDS in .env');
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
await fs.appendFile(envPath, `\n\n${ newCredsLine }\n`);
|
|
44
|
-
console.log('Appended CREDS to .env');
|
|
45
|
-
})();
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
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
|
-
};
|
|
17
|
-
|
|
18
|
-
const generateHostedJs = ({
|
|
19
|
-
hostedHandlers,
|
|
20
|
-
}) => {
|
|
21
|
-
const exportLines = [];
|
|
22
|
-
|
|
23
|
-
for (const hostedHandler of hostedHandlers) {
|
|
24
|
-
const { entryPoint, wrappers = [], requirePath } = hostedHandler;
|
|
25
|
-
const wrappersArg = formatWrappersArg(wrappers);
|
|
26
|
-
|
|
27
|
-
exportLines.push(
|
|
28
|
-
` ${ entryPoint }: wrapHostedFunction(() => require('${ requirePath }'), '${ entryPoint }'${ wrappersArg }),`,
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return `// Generated by mineral — do not edit
|
|
33
|
-
const { wrapHostedFunction } = require('@foxtware/mineral/hosting.utils');
|
|
34
|
-
|
|
35
|
-
module.exports = {
|
|
36
|
-
${ exportLines.join('\n') }
|
|
37
|
-
};
|
|
38
|
-
`;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const writeHostedJs = ({
|
|
42
|
-
workspace,
|
|
43
|
-
hostedHandlers,
|
|
44
|
-
}) => {
|
|
45
|
-
const hostedPath = `${ workspace.replace(/\/$/, '') }/hosted.js`;
|
|
46
|
-
const content = generateHostedJs({
|
|
47
|
-
hostedHandlers,
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
fs.writeFileSync(hostedPath, content);
|
|
51
|
-
return hostedPath;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
module.exports = {
|
|
55
|
-
generateHostedJs,
|
|
56
|
-
writeHostedJs,
|
|
57
|
-
};
|
|
File without changes
|
|
File without changes
|