@foxtware/mineral 0.1.0 → 0.1.1

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.
Files changed (59) hide show
  1. package/.creds.yml.sample +5 -0
  2. package/README.md +1 -1
  3. package/_build_scripts/publish.js +85 -0
  4. package/api/logiwa/logiwa.constants.js +8 -0
  5. package/api/logiwa/logiwa.utils.js +53 -0
  6. package/api/logiwa/logiwaAuthGet.js +86 -0
  7. package/api/logiwa/logiwaInventoriesGet.js +67 -0
  8. package/api/logiwa/logiwaOrderGet.js +54 -0
  9. package/api/logiwa/logiwaOrdersGet.js +92 -0
  10. package/api/logiwa/logiwaProductGet.js +54 -0
  11. package/api/logiwa/logiwaProductsGet.js +80 -0
  12. package/api/logiwa/logiwaReportGetAvailableToPromise.js +179 -0
  13. package/api/logiwa/logiwaReportGetInventoryCalculation.js +62 -0
  14. package/api/logiwa/logiwaReportGetInventorySnapshot.js +66 -0
  15. package/api/logiwa/logiwaReportGetTotalInventory.js +72 -0
  16. package/api/logiwa/logiwaWebhookStatusGet.js +54 -0
  17. package/api/logiwa/logiwaWebhookSubscribe.js +64 -0
  18. package/api/logiwa/logiwaWebhookUnsubscribe.js +54 -0
  19. package/api/logiwa/logiwaWebhooksGet.js +48 -0
  20. package/api/shopify/_example.get.js +46 -0
  21. package/api/shopify/shopifyAbandonedCheckoutsGet.js +46 -0
  22. package/api/shopify/shopifyArticlesGet.js +46 -0
  23. package/api/shopify/shopifyBlogsGet.js +46 -0
  24. package/api/shopify/shopifyBulkOperationsGet.js +46 -0
  25. package/api/shopify/shopifyCatalogsGet.js +46 -0
  26. package/api/shopify/shopifyChannelsGet.js +46 -0
  27. package/api/shopify/shopifyCheckoutAndAccountsConfigurationsGet.js +46 -0
  28. package/api/shopify/shopifyCollectionsGet.js +46 -0
  29. package/api/shopify/shopifyCustomersGet.js +2 -0
  30. package/api/shopify/shopifyDeliveryCustomizationsGet.js +46 -0
  31. package/api/shopify/shopifyDeliveryProfilesGet.js +46 -0
  32. package/api/shopify/shopifyDiscountNodesGet.js +46 -0
  33. package/api/shopify/shopifyDisputesGet.js +46 -0
  34. package/api/shopify/shopifyDraftOrdersGet.js +46 -0
  35. package/api/shopify/shopifyEventsGet.js +46 -0
  36. package/api/shopify/shopifyFilesGet.js +46 -0
  37. package/api/shopify/shopifyFulfillmentOrdersGet.js +46 -0
  38. package/api/shopify/shopifyGiftCardsGet.js +46 -0
  39. package/api/shopify/shopifyInventoryItemsGet.js +46 -0
  40. package/api/shopify/shopifyLocationsGet.js +46 -0
  41. package/api/shopify/shopifyMarketingEventsGet.js +46 -0
  42. package/api/shopify/shopifyMarketsGet.js +46 -0
  43. package/api/shopify/shopifyMenusGet.js +46 -0
  44. package/api/shopify/shopifyMetafieldDefinitionsGet.js +50 -0
  45. package/api/shopify/shopifyMetaobjectDefinitionsGet.js +46 -0
  46. package/api/shopify/shopifyMetaobjectsGet.js +50 -0
  47. package/api/shopify/shopifyPagesGet.js +46 -0
  48. package/api/shopify/shopifyPaymentCustomizationsGet.js +46 -0
  49. package/api/shopify/shopifyProductFeedsGet.js +46 -0
  50. package/api/shopify/shopifyProductVariantsGet.js +46 -0
  51. package/api/shopify/shopifyProductsGet.js +46 -0
  52. package/api/shopify/shopifyPublicationsGet.js +46 -0
  53. package/api/shopify/shopifySegmentsGet.js +46 -0
  54. package/api/shopify/shopifyUrlRedirectsGet.js +46 -0
  55. package/api/shopify/shopifyWebhookSubscriptionsGet.js +46 -0
  56. package/api/utils.js +4 -3
  57. package/api/workspace.js +43 -0
  58. package/package.json +5 -3
  59. package/server.js +205 -105
package/server.js CHANGED
@@ -1,45 +1,100 @@
1
- require('dotenv').config();
2
-
3
1
  const fs = require('fs');
4
- const path = require('path');
5
2
  const http = require('http');
6
3
  const { respondJson, errorToReadable, getRequestBody, argsFromBody, funcApi } = require('./server.utils');
4
+ const { getWorkspace, setWorkspace, loadWorkspaceEnv, toAbsolutePath } = require('./api/workspace');
5
+
6
+ const MINERAL_API_DIR = `${ __dirname }/api`;
7
+
8
+ // --- Config ---
9
+
10
+ const splitCommaList = (value = '') => value
11
+ .split(',')
12
+ .map((item) => item.trim())
13
+ .filter(Boolean);
14
+
15
+ const readCliFlag = (flag) => {
16
+ const args = process.argv.slice(2);
17
+ const equalsPrefix = `${ flag }=`;
18
+
19
+ for (let index = 0; index < args.length; index++) {
20
+ if (args[index].startsWith(equalsPrefix)) {
21
+ return args[index].slice(equalsPrefix.length);
22
+ }
23
+
24
+ if (args[index] === flag) {
25
+ return args[index + 1];
26
+ }
27
+ }
28
+ };
29
+
30
+ const getApiDirs = (options = {}) => {
31
+ if (options.api_dirs) {
32
+ return options.api_dirs;
33
+ }
34
+
35
+ const fromCli = readCliFlag('--api_dirs');
36
+ if (fromCli) {
37
+ return splitCommaList(fromCli);
38
+ }
39
+
40
+ if (process.env.MINERAL_API_DIRS) {
41
+ return splitCommaList(process.env.MINERAL_API_DIRS);
42
+ }
43
+
44
+ return [];
45
+ };
46
+
47
+ const getConfig = (options = {}) => ({
48
+ port: Number(options.port ?? process.env.PORT ?? 8000),
49
+ workspace: toAbsolutePath(
50
+ options.workspace
51
+ ?? readCliFlag('--workspace')
52
+ ?? process.env.MINERAL_WORKSPACE
53
+ ?? process.cwd(),
54
+ ),
55
+ api_dirs: getApiDirs(options),
56
+ });
7
57
 
8
- const apiDirectory = path.join(__dirname, 'api');
58
+ // --- Handler discovery ---
9
59
 
10
- const shouldSkipApiFile = (fileName) => {
60
+ const isHandlerFile = (fileName) => {
11
61
  if (!fileName.endsWith('.js')) {
12
- return true;
62
+ return false;
13
63
  }
14
64
 
15
65
  if (fileName.startsWith('_')) {
16
- return true;
66
+ return false;
17
67
  }
18
68
 
19
69
  if (fileName.endsWith('.utils.js')) {
20
- return true;
70
+ return false;
21
71
  }
22
72
 
23
- const skippedFiles = new Set([
24
- 'utils.js',
25
- 'validators.js',
26
- ]);
73
+ return true;
74
+ };
27
75
 
28
- return skippedFiles.has(fileName);
76
+ const routeNameFromFile = (filePath) => {
77
+ const fileName = filePath.split('/').pop();
78
+ return fileName.slice(0, -3);
29
79
  };
30
80
 
31
- const walkFilesRecursive = (directory) => {
32
- const entries = fs.readdirSync(directory, { withFileTypes: true });
81
+ const listJsFiles = (directory) => {
82
+ if (!fs.existsSync(directory)) {
83
+ return [];
84
+ }
85
+
33
86
  const files = [];
87
+ const entries = fs.readdirSync(directory, { withFileTypes: true });
34
88
 
35
89
  for (const entry of entries) {
36
- const entryPath = path.join(directory, entry.name);
90
+ const entryPath = `${ directory }/${ entry.name }`;
91
+
37
92
  if (entry.isDirectory()) {
38
- files.push(...walkFilesRecursive(entryPath));
93
+ files.push(...listJsFiles(entryPath));
39
94
  continue;
40
95
  }
41
96
 
42
- if (entry.isFile()) {
97
+ if (entry.isFile() && isHandlerFile(entry.name)) {
43
98
  files.push(entryPath);
44
99
  }
45
100
  }
@@ -47,92 +102,105 @@ const walkFilesRecursive = (directory) => {
47
102
  return files;
48
103
  };
49
104
 
50
- const loadHandlers = () => {
51
- const files = walkFilesRecursive(apiDirectory).filter((filePath) => !shouldSkipApiFile(path.basename(filePath)));
52
- const routeToHandler = new Map();
105
+ const directoriesToScan = ({
106
+ workspace,
107
+ api_dirs,
108
+ }) => {
109
+ const extraDirs = api_dirs.map((dir) => (
110
+ dir.startsWith('/')
111
+ ? dir
112
+ : `${ workspace }/${ dir }`
113
+ ));
114
+
115
+ return [MINERAL_API_DIR, ...extraDirs];
116
+ };
53
117
 
54
- for (const filePath of files) {
55
- const moduleExports = require(filePath);
56
- if (!moduleExports || typeof moduleExports !== 'object') {
57
- continue;
58
- }
118
+ const getFuncApiConfig = ({
119
+ moduleExports,
120
+ routeName,
121
+ }) => {
122
+ const { funcApiConfig } = moduleExports;
123
+ if (!funcApiConfig || typeof funcApiConfig !== 'object') {
124
+ return undefined;
125
+ }
59
126
 
60
- const fileBaseName = path.basename(filePath, '.js');
61
- const exportedValue = moduleExports[fileBaseName];
127
+ if (funcApiConfig[routeName]) {
128
+ return funcApiConfig[routeName];
129
+ }
62
130
 
63
- if (typeof exportedValue !== 'function') {
64
- continue;
65
- }
131
+ const exportNames = Object.keys(moduleExports).filter((key) => key !== 'funcApiConfig');
132
+ const configIsShared = !exportNames.some((name) => funcApiConfig[name]);
66
133
 
67
- const {
68
- funcApiConfig,
69
- } = moduleExports;
70
- const functionExportNames = Object.entries(moduleExports)
71
- .filter(([exportName, exportValue]) => exportName !== 'funcApiConfig' && typeof exportValue === 'function')
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
- }
134
+ if (configIsShared) {
135
+ return funcApiConfig;
136
+ }
137
+ };
102
138
 
103
- routeToHandler.set(routePath, {
104
- filePath,
105
- exportName: fileBaseName,
106
- handler,
107
- usesFuncApi: Boolean(exportFuncApiConfig),
108
- });
139
+ const addHandlerFromFile = (filePath, handlers) => {
140
+ const moduleExports = require(filePath);
141
+ if (!moduleExports || typeof moduleExports !== 'object') {
142
+ return;
143
+ }
144
+
145
+ const routeName = routeNameFromFile(filePath);
146
+ const handlerFn = moduleExports[routeName];
147
+ if (typeof handlerFn !== 'function') {
148
+ return;
149
+ }
150
+
151
+ const funcApiConfig = getFuncApiConfig({
152
+ moduleExports,
153
+ routeName,
154
+ });
155
+
156
+ const route = `/${ routeName }`;
157
+ if (handlers.has(route)) {
158
+ const existing = handlers.get(route);
159
+ throw new Error(`Duplicate route '${ route }' from ${ filePath } and ${ existing.filePath }`);
109
160
  }
110
161
 
111
- return routeToHandler;
162
+ handlers.set(route, {
163
+ filePath,
164
+ routeName,
165
+ handler: funcApiConfig ? funcApi(handlerFn, funcApiConfig) : handlerFn,
166
+ usesFuncApi: Boolean(funcApiConfig),
167
+ });
112
168
  };
113
169
 
114
- const handlers = loadHandlers();
170
+ const loadHandlers = (config) => {
171
+ const handlers = new Map();
172
+
173
+ for (const directory of directoriesToScan(config)) {
174
+ for (const filePath of listJsFiles(directory)) {
175
+ addHandlerFromFile(filePath, handlers);
176
+ }
177
+ }
115
178
 
116
- const server = http.createServer(async (req, res) => {
179
+ return handlers;
180
+ };
181
+
182
+ // --- HTTP ---
183
+
184
+ const createServer = (handlers) => http.createServer(async (req, res) => {
117
185
  if (req.method === 'GET' && req.url === '/') {
118
- const routes = Array.from(handlers.keys()).sort();
119
186
  respondJson(res, 200, {
120
187
  ok: true,
121
188
  data: {
122
- routes,
189
+ routes: [...handlers.keys()].sort(),
190
+ workspace: getWorkspace(),
123
191
  },
124
192
  });
125
193
  return;
126
194
  }
127
195
 
128
- const pathOnly = (req.url || '/').split('?')[0];
129
- const matchedHandler = handlers.get(pathOnly);
130
- if (!matchedHandler) {
196
+ const route = (req.url || '/').split('?')[0];
197
+ const handler = handlers.get(route);
198
+ if (!handler) {
131
199
  respondJson(res, 404, {
132
200
  ok: false,
133
201
  error: {
134
202
  code: 'NOT_FOUND',
135
- message: `No handler found for route ${ pathOnly }`,
203
+ message: `No handler found for route ${ route }`,
136
204
  },
137
205
  });
138
206
  return;
@@ -141,19 +209,12 @@ const server = http.createServer(async (req, res) => {
141
209
  try {
142
210
  const body = await getRequestBody(req);
143
211
  const args = argsFromBody(body);
144
- const result = matchedHandler.usesFuncApi
145
- ? await matchedHandler.handler({
146
- req,
147
- res,
148
- body,
149
- args,
150
- })
151
- : await matchedHandler.handler(...args);
212
+ const result = handler.usesFuncApi
213
+ ? await handler.handler({ req, res, body, args })
214
+ : await handler.handler(...args);
152
215
 
153
216
  if (result === undefined) {
154
- respondJson(res, 200, {
155
- ok: true,
156
- });
217
+ respondJson(res, 200, { ok: true });
157
218
  return;
158
219
  }
159
220
 
@@ -170,25 +231,64 @@ const server = http.createServer(async (req, res) => {
170
231
  }
171
232
  });
172
233
 
173
- const startServer = ({
174
- port = process.env.PORT || 8000,
175
- } = {}) => {
176
- server.listen(port, () => {
177
- console.log(`Mineral server running on port ${ port }`);
178
- console.log('Registered routes:');
179
- for (const route of Array.from(handlers.keys()).sort()) {
180
- console.log(route);
181
- }
182
- });
234
+ // --- Start ---
235
+
236
+ let server = null;
237
+ let handlers = null;
238
+
239
+ const logStartup = ({
240
+ port,
241
+ workspace,
242
+ api_dirs,
243
+ handlers: routeHandlers,
244
+ }) => {
245
+ console.log(`Mineral server running on port ${ port }`);
246
+ console.log(`Workspace: ${ workspace }`);
247
+
248
+ if (api_dirs.length) {
249
+ console.log('Extra API dirs:', api_dirs.join(', '));
250
+ }
251
+
252
+ console.log('Registered routes:');
253
+ for (const route of [...routeHandlers.keys()].sort()) {
254
+ console.log(route);
255
+ }
256
+ };
257
+
258
+ const startServer = (options = {}) => {
259
+ const config = getConfig(options);
260
+
261
+ setWorkspace(config.workspace);
262
+ loadWorkspaceEnv();
263
+
264
+ handlers = loadHandlers(config);
265
+ server = createServer(handlers);
266
+
267
+ server.listen(config.port, () => logStartup({
268
+ ...config,
269
+ handlers,
270
+ }));
183
271
 
184
272
  return server;
185
273
  };
186
274
 
187
275
  module.exports = {
188
- server,
189
- handlers,
190
- loadHandlers,
191
276
  startServer,
277
+ loadHandlers,
278
+ get server() {
279
+ if (!server) {
280
+ startServer();
281
+ }
282
+
283
+ return server;
284
+ },
285
+ get handlers() {
286
+ if (!handlers) {
287
+ startServer();
288
+ }
289
+
290
+ return handlers;
291
+ },
192
292
  };
193
293
 
194
294
  if (require.main === module) {