@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/api/workspace.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const dotenv = require('dotenv');
|
|
3
|
+
|
|
4
|
+
let workspace = process.cwd();
|
|
5
|
+
|
|
6
|
+
// Turn ".", "geode", or "/full/path" into an absolute directory.
|
|
7
|
+
const toAbsolutePath = (dir) => {
|
|
8
|
+
if (!dir || dir === '.') {
|
|
9
|
+
return process.cwd();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (dir.startsWith('/')) {
|
|
13
|
+
return dir;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return `${ process.cwd() }/${ dir }`;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const getWorkspace = () => workspace;
|
|
20
|
+
|
|
21
|
+
const setWorkspace = (dir) => {
|
|
22
|
+
workspace = toAbsolutePath(dir);
|
|
23
|
+
process.env.MINERAL_WORKSPACE = workspace;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const loadWorkspaceEnv = () => {
|
|
27
|
+
const envFile = `${ workspace }/.env`;
|
|
28
|
+
if (!fs.existsSync(envFile)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
dotenv.config({
|
|
33
|
+
path: envFile,
|
|
34
|
+
override: true,
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
module.exports = {
|
|
39
|
+
getWorkspace,
|
|
40
|
+
setWorkspace,
|
|
41
|
+
loadWorkspaceEnv,
|
|
42
|
+
toAbsolutePath,
|
|
43
|
+
};
|
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
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@foxtware/mineral",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"bin": {
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"bin": {
|
|
5
|
+
"mineral": "bin/mineral.js"
|
|
6
|
+
},
|
|
5
7
|
"main": "server.js",
|
|
6
8
|
"publishConfig": {
|
|
7
9
|
"access": "public"
|
|
@@ -12,7 +14,7 @@
|
|
|
12
14
|
"new": "node _build_scripts/createNewFunction.js",
|
|
13
15
|
"serve": "PORT=8100 node server.js",
|
|
14
16
|
"start": "node server.js",
|
|
15
|
-
"npm:publish": "
|
|
17
|
+
"npm:publish": "node _build_scripts/publish.js"
|
|
16
18
|
},
|
|
17
19
|
"dependencies": {
|
|
18
20
|
"csvtojson": "^2.0.14",
|
package/server.js
CHANGED
|
@@ -1,45 +1,64 @@
|
|
|
1
|
-
require('dotenv').config();
|
|
2
|
-
|
|
3
1
|
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
2
|
const http = require('http');
|
|
6
|
-
const { respondJson, errorToReadable, getRequestBody, argsFromBody, funcApi } = require('./server.utils');
|
|
3
|
+
const { respondJson, errorToReadable, getRequestBody, argsFromBody, funcApi, statusCodeFromResult } = require('./server.utils');
|
|
4
|
+
const { getWorkspace, setWorkspace, loadWorkspaceEnv, toAbsolutePath } = require('./api/workspace');
|
|
5
|
+
const { getApiDirs, readCliFlag } = require('./cli');
|
|
6
|
+
const { getFuncApiConfig } = require('./hosting.utils');
|
|
7
|
+
|
|
8
|
+
const MINERAL_API_DIR = `${ __dirname }/api`;
|
|
9
|
+
|
|
10
|
+
const getConfig = (options = {}) => ({
|
|
11
|
+
port: Number(options.port ?? process.env.PORT ?? 8000),
|
|
12
|
+
workspace: toAbsolutePath(
|
|
13
|
+
options.workspace
|
|
14
|
+
?? readCliFlag('--workspace')
|
|
15
|
+
?? process.env.MINERAL_WORKSPACE
|
|
16
|
+
?? process.cwd(),
|
|
17
|
+
),
|
|
18
|
+
api_dirs: getApiDirs(options),
|
|
19
|
+
host_mode: Boolean(options.host_mode),
|
|
20
|
+
});
|
|
7
21
|
|
|
8
|
-
|
|
22
|
+
// --- Handler discovery ---
|
|
9
23
|
|
|
10
|
-
const
|
|
24
|
+
const isHandlerFile = (fileName) => {
|
|
11
25
|
if (!fileName.endsWith('.js')) {
|
|
12
|
-
return
|
|
26
|
+
return false;
|
|
13
27
|
}
|
|
14
28
|
|
|
15
29
|
if (fileName.startsWith('_')) {
|
|
16
|
-
return
|
|
30
|
+
return false;
|
|
17
31
|
}
|
|
18
32
|
|
|
19
33
|
if (fileName.endsWith('.utils.js')) {
|
|
20
|
-
return
|
|
34
|
+
return false;
|
|
21
35
|
}
|
|
22
36
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
'validators.js',
|
|
26
|
-
]);
|
|
37
|
+
return true;
|
|
38
|
+
};
|
|
27
39
|
|
|
28
|
-
|
|
40
|
+
const routeNameFromFile = (filePath) => {
|
|
41
|
+
const fileName = filePath.split('/').pop();
|
|
42
|
+
return fileName.slice(0, -3);
|
|
29
43
|
};
|
|
30
44
|
|
|
31
|
-
const
|
|
32
|
-
|
|
45
|
+
const listJsFiles = (directory) => {
|
|
46
|
+
if (!fs.existsSync(directory)) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
|
|
33
50
|
const files = [];
|
|
51
|
+
const entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
34
52
|
|
|
35
53
|
for (const entry of entries) {
|
|
36
|
-
const entryPath =
|
|
54
|
+
const entryPath = `${ directory }/${ entry.name }`;
|
|
55
|
+
|
|
37
56
|
if (entry.isDirectory()) {
|
|
38
|
-
files.push(...
|
|
57
|
+
files.push(...listJsFiles(entryPath));
|
|
39
58
|
continue;
|
|
40
59
|
}
|
|
41
60
|
|
|
42
|
-
if (entry.isFile()) {
|
|
61
|
+
if (entry.isFile() && isHandlerFile(entry.name)) {
|
|
43
62
|
files.push(entryPath);
|
|
44
63
|
}
|
|
45
64
|
}
|
|
@@ -47,92 +66,91 @@ const walkFilesRecursive = (directory) => {
|
|
|
47
66
|
return files;
|
|
48
67
|
};
|
|
49
68
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
69
|
+
const directoriesToScan = ({
|
|
70
|
+
workspace,
|
|
71
|
+
api_dirs,
|
|
72
|
+
host_mode = false,
|
|
73
|
+
}) => {
|
|
74
|
+
const extraDirs = api_dirs.map((dir) => (
|
|
75
|
+
dir.startsWith('/')
|
|
76
|
+
? dir
|
|
77
|
+
: `${ workspace }/${ dir }`
|
|
78
|
+
));
|
|
79
|
+
|
|
80
|
+
if (host_mode && api_dirs.length) {
|
|
81
|
+
return extraDirs;
|
|
82
|
+
}
|
|
53
83
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (!moduleExports || typeof moduleExports !== 'object') {
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
84
|
+
return [MINERAL_API_DIR, ...extraDirs];
|
|
85
|
+
};
|
|
59
86
|
|
|
60
|
-
|
|
61
|
-
const exportedValue = moduleExports[fileBaseName];
|
|
87
|
+
const getFuncApiConfigFromModule = getFuncApiConfig;
|
|
62
88
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
89
|
+
const addHandlerFromFile = (filePath, handlers) => {
|
|
90
|
+
const moduleExports = require(filePath);
|
|
91
|
+
if (!moduleExports || typeof moduleExports !== 'object') {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
66
94
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
.map(([exportName]) => exportName);
|
|
73
|
-
|
|
74
|
-
const getFuncApiConfigForExport = (exportName) => {
|
|
75
|
-
if (!funcApiConfig || typeof funcApiConfig !== 'object') {
|
|
76
|
-
return undefined;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const configByExportName = funcApiConfig[exportName];
|
|
80
|
-
if (configByExportName && typeof configByExportName === 'object' && !Array.isArray(configByExportName)) {
|
|
81
|
-
return configByExportName;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// If config isn't keyed by function names and this export matches the filename,
|
|
85
|
-
// treat funcApiConfig as the config for that function.
|
|
86
|
-
const configIsKeyedByFunctionName = functionExportNames.some((functionExportName) => funcApiConfig[functionExportName] !== undefined);
|
|
87
|
-
if (!configIsKeyedByFunctionName && exportName === fileBaseName) {
|
|
88
|
-
return funcApiConfig;
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const exportFuncApiConfig = getFuncApiConfigForExport(fileBaseName);
|
|
93
|
-
const handler = exportFuncApiConfig
|
|
94
|
-
? funcApi(exportedValue, exportFuncApiConfig)
|
|
95
|
-
: exportedValue;
|
|
96
|
-
|
|
97
|
-
const routePath = `/${ fileBaseName }`;
|
|
98
|
-
if (routeToHandler.has(routePath)) {
|
|
99
|
-
const existing = routeToHandler.get(routePath);
|
|
100
|
-
throw new Error(`Duplicate handler route '${ routePath }' from ${ filePath } and ${ existing.filePath }`);
|
|
101
|
-
}
|
|
95
|
+
const routeName = routeNameFromFile(filePath);
|
|
96
|
+
const handlerFn = moduleExports[routeName];
|
|
97
|
+
if (typeof handlerFn !== 'function') {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
102
100
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
101
|
+
const funcApiConfig = getFuncApiConfigFromModule({
|
|
102
|
+
moduleExports,
|
|
103
|
+
routeName,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const route = `/${ routeName }`;
|
|
107
|
+
if (handlers.has(route)) {
|
|
108
|
+
const existing = handlers.get(route);
|
|
109
|
+
throw new Error(`Duplicate route '${ route }' from ${ filePath } and ${ existing.filePath }`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
handlers.set(route, {
|
|
113
|
+
filePath,
|
|
114
|
+
routeName,
|
|
115
|
+
handler: funcApiConfig ? funcApi(handlerFn, funcApiConfig) : handlerFn,
|
|
116
|
+
usesFuncApi: Boolean(funcApiConfig),
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const loadHandlers = (config) => {
|
|
121
|
+
const handlers = new Map();
|
|
122
|
+
|
|
123
|
+
for (const directory of directoriesToScan(config)) {
|
|
124
|
+
for (const filePath of listJsFiles(directory)) {
|
|
125
|
+
addHandlerFromFile(filePath, handlers);
|
|
126
|
+
}
|
|
109
127
|
}
|
|
110
128
|
|
|
111
|
-
return
|
|
129
|
+
return handlers;
|
|
112
130
|
};
|
|
113
131
|
|
|
114
|
-
|
|
132
|
+
// --- HTTP ---
|
|
115
133
|
|
|
116
|
-
const
|
|
134
|
+
const createServer = (handlers) => http.createServer(async (req, res) => {
|
|
117
135
|
if (req.method === 'GET' && req.url === '/') {
|
|
118
|
-
const routes = Array.from(handlers.keys()).sort();
|
|
119
136
|
respondJson(res, 200, {
|
|
120
137
|
ok: true,
|
|
121
138
|
data: {
|
|
122
|
-
routes,
|
|
139
|
+
routes: [...handlers.keys()].sort(),
|
|
140
|
+
workspace: getWorkspace(),
|
|
123
141
|
},
|
|
124
142
|
});
|
|
125
143
|
return;
|
|
126
144
|
}
|
|
127
145
|
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
if (!
|
|
146
|
+
const route = (req.url || '/').split('?')[0];
|
|
147
|
+
const handler = handlers.get(route);
|
|
148
|
+
if (!handler) {
|
|
131
149
|
respondJson(res, 404, {
|
|
132
150
|
ok: false,
|
|
133
151
|
error: {
|
|
134
152
|
code: 'NOT_FOUND',
|
|
135
|
-
message: `No handler found for route ${
|
|
153
|
+
message: `No handler found for route ${ route }`,
|
|
136
154
|
},
|
|
137
155
|
});
|
|
138
156
|
return;
|
|
@@ -141,23 +159,16 @@ const server = http.createServer(async (req, res) => {
|
|
|
141
159
|
try {
|
|
142
160
|
const body = await getRequestBody(req);
|
|
143
161
|
const args = argsFromBody(body);
|
|
144
|
-
const result =
|
|
145
|
-
? await
|
|
146
|
-
|
|
147
|
-
res,
|
|
148
|
-
body,
|
|
149
|
-
args,
|
|
150
|
-
})
|
|
151
|
-
: await matchedHandler.handler(...args);
|
|
162
|
+
const result = handler.usesFuncApi
|
|
163
|
+
? await handler.handler({ req, res, body, args })
|
|
164
|
+
: await handler.handler(...args);
|
|
152
165
|
|
|
153
166
|
if (result === undefined) {
|
|
154
|
-
respondJson(res, 200, {
|
|
155
|
-
ok: true,
|
|
156
|
-
});
|
|
167
|
+
respondJson(res, 200, { ok: true });
|
|
157
168
|
return;
|
|
158
169
|
}
|
|
159
170
|
|
|
160
|
-
respondJson(res,
|
|
171
|
+
respondJson(res, statusCodeFromResult(result), result);
|
|
161
172
|
} catch (error) {
|
|
162
173
|
respondJson(res, 500, {
|
|
163
174
|
ok: false,
|
|
@@ -170,25 +181,64 @@ const server = http.createServer(async (req, res) => {
|
|
|
170
181
|
}
|
|
171
182
|
});
|
|
172
183
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
184
|
+
// --- Start ---
|
|
185
|
+
|
|
186
|
+
let server = null;
|
|
187
|
+
let handlers = null;
|
|
188
|
+
|
|
189
|
+
const logStartup = ({
|
|
190
|
+
port,
|
|
191
|
+
workspace,
|
|
192
|
+
api_dirs,
|
|
193
|
+
handlers: routeHandlers,
|
|
194
|
+
}) => {
|
|
195
|
+
console.log(`Mineral server running on port ${ port }`);
|
|
196
|
+
console.log(`Workspace: ${ workspace }`);
|
|
197
|
+
|
|
198
|
+
if (api_dirs.length) {
|
|
199
|
+
console.log('Extra API dirs:', api_dirs.join(', '));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
console.log('Registered routes:');
|
|
203
|
+
for (const route of [...routeHandlers.keys()].sort()) {
|
|
204
|
+
console.log(route);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const startServer = (options = {}) => {
|
|
209
|
+
const config = getConfig(options);
|
|
210
|
+
|
|
211
|
+
setWorkspace(config.workspace);
|
|
212
|
+
loadWorkspaceEnv();
|
|
213
|
+
|
|
214
|
+
handlers = loadHandlers(config);
|
|
215
|
+
server = createServer(handlers);
|
|
216
|
+
|
|
217
|
+
server.listen(config.port, () => logStartup({
|
|
218
|
+
...config,
|
|
219
|
+
handlers,
|
|
220
|
+
}));
|
|
183
221
|
|
|
184
222
|
return server;
|
|
185
223
|
};
|
|
186
224
|
|
|
187
225
|
module.exports = {
|
|
188
|
-
server,
|
|
189
|
-
handlers,
|
|
190
|
-
loadHandlers,
|
|
191
226
|
startServer,
|
|
227
|
+
loadHandlers,
|
|
228
|
+
get server() {
|
|
229
|
+
if (!server) {
|
|
230
|
+
startServer();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return server;
|
|
234
|
+
},
|
|
235
|
+
get handlers() {
|
|
236
|
+
if (!handlers) {
|
|
237
|
+
startServer();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return handlers;
|
|
241
|
+
},
|
|
192
242
|
};
|
|
193
243
|
|
|
194
244
|
if (require.main === module) {
|