@foxtware/mineral 0.1.2 → 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.
@@ -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 { readHostingYml, getCredsJsonForDeploy, getHostedApiKeyForDeploy, resolveHostedHandlersForDeploy, functionUsesWrapper } = require('../hosting.utils');
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
- hostedApiKey,
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
- 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
- }
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
- hostedApiKey,
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
- 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
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
- npm run host all
311
+ mineral host all
313
312
 
314
313
  Deploy a single function:
315
- npm run host function
314
+ mineral host function
316
315
 
317
316
  Deploy a group of functions:
318
- npm run host group
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 = wrappers.length ? `, ${ JSON.stringify(wrappers) }` : '';
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('${ hostingUtilsRequire }');
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 wrappersByName = {
16
- requireHostedApiKey,
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 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
+ 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 = {}, wrapperName) => (
55
- Array.isArray(functionConfig.wrappers) && functionConfig.wrappers.includes(wrapperName)
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, wrapperNames = []) => {
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 getHostedApiKeyForDeploy = (workspace) => {
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(/^HOSTED_API_KEY=(.*)$/m);
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
- getHostedApiKeyForDeploy,
262
+ getEnvVarsForDeploy,
263
+ validateEnvVarsForDeploy,
229
264
  getHostedEntries,
230
265
  resolveHostedHandlersForDeploy,
231
266
  functionUsesWrapper,
267
+ wrapperExportName,
232
268
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxtware/mineral",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "bin": {
5
5
  "mineral": "bin/mineral.js"
6
6
  },
package/server.utils.js CHANGED
@@ -37,6 +37,10 @@ const getRequestBody = async (req) => {
37
37
  return undefined;
38
38
  }
39
39
 
40
+ if (req.body !== undefined) {
41
+ return req.body;
42
+ }
43
+
40
44
  const decoder = new StringDecoder('utf8');
41
45
 
42
46
  return await new Promise((resolve, reject) => {
@@ -145,6 +149,20 @@ const requireHostedApiKey = async (req) => {
145
149
  }
146
150
  };
147
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
+
148
166
  const statusCodeFromResult = (result) => {
149
167
  if (result?.ok === false) {
150
168
  return result?.error?.statusCode ?? 400;
@@ -263,6 +281,7 @@ module.exports = {
263
281
  argsFromBody,
264
282
  wrapFunction,
265
283
  requireHostedApiKey,
284
+ allowCrossOriginCallsAndHandleOptions,
266
285
  statusCodeFromResult,
267
286
  funcApi,
268
287
  };