@foxtware/mineral 0.1.0 → 0.1.2
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/.creds.yml.sample +5 -0
- package/.gcloudignore +17 -0
- package/.hosting.yml.sample +23 -0
- package/README.md +3 -1
- package/_build_scripts/publish.js +105 -0
- 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/api/logiwa/logiwa.constants.js +8 -0
- package/api/logiwa/logiwa.utils.js +53 -0
- package/api/logiwa/logiwaAuthGet.js +86 -0
- package/api/logiwa/logiwaInventoriesGet.js +67 -0
- package/api/logiwa/logiwaOrderGet.js +54 -0
- package/api/logiwa/logiwaOrdersGet.js +92 -0
- package/api/logiwa/logiwaProductGet.js +54 -0
- package/api/logiwa/logiwaProductsGet.js +80 -0
- package/api/logiwa/logiwaReportGetAvailableToPromise.js +179 -0
- package/api/logiwa/logiwaReportGetInventoryCalculation.js +62 -0
- package/api/logiwa/logiwaReportGetInventorySnapshot.js +66 -0
- package/api/logiwa/logiwaReportGetTotalInventory.js +72 -0
- package/api/logiwa/logiwaWebhookStatusGet.js +54 -0
- package/api/logiwa/logiwaWebhookSubscribe.js +64 -0
- package/api/logiwa/logiwaWebhookUnsubscribe.js +54 -0
- package/api/logiwa/logiwaWebhooksGet.js +48 -0
- package/api/shopify/_example.get.js +46 -0
- package/api/shopify/shopifyAbandonedCheckoutsGet.js +46 -0
- package/api/shopify/shopifyArticlesGet.js +46 -0
- package/api/shopify/shopifyBlogsGet.js +46 -0
- package/api/shopify/shopifyBulkOperationsGet.js +46 -0
- package/api/shopify/shopifyCatalogsGet.js +46 -0
- package/api/shopify/shopifyChannelsGet.js +46 -0
- package/api/shopify/shopifyCheckoutAndAccountsConfigurationsGet.js +46 -0
- package/api/shopify/shopifyCollectionsGet.js +46 -0
- package/api/shopify/shopifyCustomersGet.js +2 -0
- package/api/shopify/shopifyDeliveryCustomizationsGet.js +46 -0
- package/api/shopify/shopifyDeliveryProfilesGet.js +46 -0
- package/api/shopify/shopifyDiscountNodesGet.js +46 -0
- package/api/shopify/shopifyDisputesGet.js +46 -0
- package/api/shopify/shopifyDraftOrdersGet.js +46 -0
- package/api/shopify/shopifyEventsGet.js +46 -0
- package/api/shopify/shopifyFilesGet.js +46 -0
- package/api/shopify/shopifyFulfillmentOrdersGet.js +46 -0
- package/api/shopify/shopifyGiftCardsGet.js +46 -0
- package/api/shopify/shopifyInventoryItemsGet.js +46 -0
- package/api/shopify/shopifyLocationsGet.js +46 -0
- package/api/shopify/shopifyMarketingEventsGet.js +46 -0
- package/api/shopify/shopifyMarketsGet.js +46 -0
- package/api/shopify/shopifyMenusGet.js +46 -0
- package/api/shopify/shopifyMetafieldDefinitionsGet.js +50 -0
- package/api/shopify/shopifyMetaobjectDefinitionsGet.js +46 -0
- package/api/shopify/shopifyMetaobjectsGet.js +50 -0
- package/api/shopify/shopifyPagesGet.js +46 -0
- package/api/shopify/shopifyPaymentCustomizationsGet.js +46 -0
- package/api/shopify/shopifyProductFeedsGet.js +46 -0
- package/api/shopify/shopifyProductVariantsGet.js +46 -0
- package/api/shopify/shopifyProductsGet.js +46 -0
- package/api/shopify/shopifyPublicationsGet.js +46 -0
- package/api/shopify/shopifySegmentsGet.js +46 -0
- package/api/shopify/shopifyUrlRedirectsGet.js +46 -0
- package/api/shopify/shopifyWebhookSubscriptionsGet.js +46 -0
- package/api/utils.js +4 -3
- package/api/workspace.js +43 -0
- package/cli.js +42 -0
- package/hosting.utils.js +232 -0
- package/package.json +5 -3
- package/server.js +157 -107
- package/server.utils.js +40 -0
package/.creds.yml.sample
CHANGED
|
@@ -8,6 +8,11 @@ peoplevox:
|
|
|
8
8
|
USERNAME: ______________________________
|
|
9
9
|
PASSWORD: ______________________________
|
|
10
10
|
|
|
11
|
+
logiwa:
|
|
12
|
+
BASE_URL: https://myapi.logiwa.com
|
|
13
|
+
EMAIL: _________________________________
|
|
14
|
+
PASSWORD: ______________________________
|
|
15
|
+
|
|
11
16
|
youtube:
|
|
12
17
|
API_KEY: _______________________________
|
|
13
18
|
|
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
|
@@ -49,4 +49,6 @@ The [bedrock](https://github.com/GorgonFreeman/bedrock) middleware, refactored f
|
|
|
49
49
|
}
|
|
50
50
|
```
|
|
51
51
|
- **Monorepo structure**
|
|
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.
|
|
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`.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
const mineralRoot = path.join(__dirname, '..');
|
|
7
|
+
const packageJsonPath = path.join(mineralRoot, 'package.json');
|
|
8
|
+
|
|
9
|
+
const readPackageJson = () => JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
10
|
+
|
|
11
|
+
const writePackageJson = (packageJson) => {
|
|
12
|
+
fs.writeFileSync(packageJsonPath, `${ JSON.stringify(packageJson, null, 2) }\n`);
|
|
13
|
+
};
|
|
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
|
+
|
|
32
|
+
const bumpPatch = (version) => {
|
|
33
|
+
const [major, minor, patch] = parseVersion(version);
|
|
34
|
+
return [major, minor, patch + 1].join('.');
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const getPublishedVersion = () => {
|
|
38
|
+
try {
|
|
39
|
+
return execSync('npm view @foxtware/mineral version', {
|
|
40
|
+
cwd: mineralRoot,
|
|
41
|
+
encoding: 'utf8',
|
|
42
|
+
}).trim();
|
|
43
|
+
} catch {
|
|
44
|
+
return '';
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const ensureLoggedIn = () => {
|
|
49
|
+
try {
|
|
50
|
+
execSync('npm whoami', {
|
|
51
|
+
cwd: mineralRoot,
|
|
52
|
+
encoding: 'utf8',
|
|
53
|
+
stdio: 'pipe',
|
|
54
|
+
});
|
|
55
|
+
} catch {
|
|
56
|
+
throw new Error('Not logged in to npm. Run `npm login` with an account that can publish @foxtware/mineral.');
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const askWithDefault = async (question, defaultAnswer) => {
|
|
61
|
+
const rl = readline.createInterface({
|
|
62
|
+
input: process.stdin,
|
|
63
|
+
output: process.stdout,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const answer = await new Promise((resolve) => {
|
|
67
|
+
rl.question(`${ question } [${ defaultAnswer }] `, resolve);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
rl.close();
|
|
71
|
+
return answer.trim() || defaultAnswer;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const publish = async () => {
|
|
75
|
+
ensureLoggedIn();
|
|
76
|
+
|
|
77
|
+
const packageJson = readPackageJson();
|
|
78
|
+
const localVersion = packageJson.version;
|
|
79
|
+
const publishedVersion = getPublishedVersion();
|
|
80
|
+
|
|
81
|
+
let version = localVersion;
|
|
82
|
+
|
|
83
|
+
const localIsAhead = publishedVersion && isVersionGreater(localVersion, publishedVersion);
|
|
84
|
+
|
|
85
|
+
if (!localIsAhead) {
|
|
86
|
+
const latestVersion = publishedVersion || localVersion;
|
|
87
|
+
const suggestedVersion = bumpPatch(latestVersion);
|
|
88
|
+
version = await askWithDefault('What version should we use?', suggestedVersion);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
packageJson.version = version;
|
|
92
|
+
writePackageJson(packageJson);
|
|
93
|
+
|
|
94
|
+
execSync('npm publish', {
|
|
95
|
+
cwd: mineralRoot,
|
|
96
|
+
stdio: 'inherit',
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
console.log(`Published @foxtware/mineral@${ version }`);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
publish().catch((error) => {
|
|
103
|
+
console.error(error);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
});
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const { DEFAULT_API_VERSION } = require('../logiwa/logiwa.constants');
|
|
2
|
+
const { logiwaAuthGet } = require('../logiwa/logiwaAuthGet');
|
|
3
|
+
const {
|
|
4
|
+
FetchClient,
|
|
5
|
+
Chain,
|
|
6
|
+
appendUrlToBase,
|
|
7
|
+
credsFromPayload,
|
|
8
|
+
fetchClientCommonSteps,
|
|
9
|
+
} = require('../utils');
|
|
10
|
+
|
|
11
|
+
const addUrlAndAuthHeaders = async (state) => {
|
|
12
|
+
const { requestPayload, context } = state;
|
|
13
|
+
const { credsPayload, apiVersion = DEFAULT_API_VERSION } = context;
|
|
14
|
+
|
|
15
|
+
const creds = context.creds ?? await credsFromPayload(credsPayload);
|
|
16
|
+
const authResponse = await logiwaAuthGet(credsPayload);
|
|
17
|
+
const { data } = authResponse;
|
|
18
|
+
const { token } = data;
|
|
19
|
+
|
|
20
|
+
// TODO: Handle failed auth
|
|
21
|
+
|
|
22
|
+
const { BASE_URL } = creds;
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
context: {
|
|
26
|
+
creds,
|
|
27
|
+
},
|
|
28
|
+
requestPayload: {
|
|
29
|
+
url: appendUrlToBase(`${ BASE_URL }/${ apiVersion }`, requestPayload.url),
|
|
30
|
+
headers: {
|
|
31
|
+
Authorization: `Bearer ${ token }`,
|
|
32
|
+
...requestPayload.headers,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const logiwaClientRequestPreparer = new Chain([
|
|
39
|
+
addUrlAndAuthHeaders,
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
const logiwaClientResponseInterpreter = new Chain([
|
|
43
|
+
fetchClientCommonSteps.exitEarlyOnNotOk,
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
const logiwaClient = new FetchClient({
|
|
47
|
+
requestPreparer: logiwaClientRequestPreparer,
|
|
48
|
+
responseInterpreter: logiwaClientResponseInterpreter,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
module.exports = {
|
|
52
|
+
logiwaClient,
|
|
53
|
+
};
|