@foxtware/mineral 0.1.0

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 (73) hide show
  1. package/.creds.yml.sample +21 -0
  2. package/AGENTS.md +13 -0
  3. package/README.md +52 -0
  4. package/_build_scripts/copyCredsToEnv.js +45 -0
  5. package/_build_scripts/createNewFunction.js +361 -0
  6. package/_docs/standard_response_examples.md +67 -0
  7. package/api/_example.js +45 -0
  8. package/api/constants.js +3 -0
  9. package/api/linear/linear.constants.js +5 -0
  10. package/api/linear/linear.utils.js +43 -0
  11. package/api/linear/linearIssuesGet.js +106 -0
  12. package/api/peoplevox/.gitkeep +0 -0
  13. package/api/peoplevox/_example.js +49 -0
  14. package/api/peoplevox/peoplevox.constants.js +3 -0
  15. package/api/peoplevox/peoplevox.utils.js +191 -0
  16. package/api/peoplevox/peoplevoxAuthGet.js +99 -0
  17. package/api/peoplevox/peoplevoxGetSingle.js +119 -0
  18. package/api/peoplevox/peoplevoxOrderEdit.js +82 -0
  19. package/api/peoplevox/peoplevoxOrderGet.js +60 -0
  20. package/api/peoplevox/peoplevoxReportGet.js +76 -0
  21. package/api/shopify/.gitkeep +0 -0
  22. package/api/shopify/_example.create.js +69 -0
  23. package/api/shopify/_example.getsingle.js +55 -0
  24. package/api/shopify/_example.js +45 -0
  25. package/api/shopify/_example.mutation.js +58 -0
  26. package/api/shopify/shopify.constants.js +7 -0
  27. package/api/shopify/shopify.utils.js +123 -0
  28. package/api/shopify/shopifyCollectionGet.js +109 -0
  29. package/api/shopify/shopifyCustomerCreate.js +71 -0
  30. package/api/shopify/shopifyCustomerGet.js +112 -0
  31. package/api/shopify/shopifyCustomerMarketingConsentUpdateEmail.js +84 -0
  32. package/api/shopify/shopifyCustomerUpdate.js +75 -0
  33. package/api/shopify/shopifyCustomersGet.js +50 -0
  34. package/api/shopify/shopifyGet.js +309 -0
  35. package/api/shopify/shopifyGetSingle.js +106 -0
  36. package/api/shopify/shopifyGiftCardCreate.js +95 -0
  37. package/api/shopify/shopifyGiftCardDeactivate.js +58 -0
  38. package/api/shopify/shopifyMetafieldsSet.js +100 -0
  39. package/api/shopify/shopifyMutationDo.js +95 -0
  40. package/api/shopify/shopifyOrderGet.js +123 -0
  41. package/api/shopify/shopifyOrdersGet.js +80 -0
  42. package/api/shopify/shopifyPageDelete.js +59 -0
  43. package/api/shopify/shopifyPageGet.js +57 -0
  44. package/api/shopify/shopifyPageUpdate.js +79 -0
  45. package/api/shopify/shopifyTagsAdd.js +94 -0
  46. package/api/shopify/shopifyTagsRemove.js +94 -0
  47. package/api/shopify/shopifyThemeDelete.js +87 -0
  48. package/api/shopify/shopifyThemeDuplicate.js +70 -0
  49. package/api/shopify/shopifyThemeGet.js +59 -0
  50. package/api/shopify/shopifyThemeUpdate.js +71 -0
  51. package/api/shopify/shopifyThemesGet.js +63 -0
  52. package/api/utils.js +1166 -0
  53. package/api/validators.js +12 -0
  54. package/api/yotpo/yotpo.constants.js +7 -0
  55. package/api/yotpo/yotpo.utils.js +47 -0
  56. package/api/yotpo/yotpoCampaignsGet.js +75 -0
  57. package/api/yotpo/yotpoCustomerActionRecord.js +95 -0
  58. package/api/yotpo/yotpoCustomerAnniversaryGet.js +60 -0
  59. package/api/yotpo/yotpoCustomerAnniversarySet.js +78 -0
  60. package/api/yotpo/yotpoCustomerBirthdaySet.js +91 -0
  61. package/api/yotpo/yotpoCustomerGet.js +104 -0
  62. package/api/yotpo/yotpoCustomerPointsAdjust.js +89 -0
  63. package/api/yotpo/yotpoCustomerUpsert.js +91 -0
  64. package/api/yotpo/yotpoCustomersRecentGet.js +62 -0
  65. package/api/yotpo/yotpoRedemptionCreate.js +96 -0
  66. package/api/yotpo/yotpoRedemptionOptionsGet.js +78 -0
  67. package/api/yotpo/yotpoVipTiersGet.js +51 -0
  68. package/api/youtube/youtubeChannelVideosGet.js +249 -0
  69. package/bin/mineral.js +5 -0
  70. package/package.json +24 -0
  71. package/server.js +196 -0
  72. package/server.utils.js +228 -0
  73. package/test.js +19 -0
package/server.js ADDED
@@ -0,0 +1,196 @@
1
+ require('dotenv').config();
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const http = require('http');
6
+ const { respondJson, errorToReadable, getRequestBody, argsFromBody, funcApi } = require('./server.utils');
7
+
8
+ const apiDirectory = path.join(__dirname, 'api');
9
+
10
+ const shouldSkipApiFile = (fileName) => {
11
+ if (!fileName.endsWith('.js')) {
12
+ return true;
13
+ }
14
+
15
+ if (fileName.startsWith('_')) {
16
+ return true;
17
+ }
18
+
19
+ if (fileName.endsWith('.utils.js')) {
20
+ return true;
21
+ }
22
+
23
+ const skippedFiles = new Set([
24
+ 'utils.js',
25
+ 'validators.js',
26
+ ]);
27
+
28
+ return skippedFiles.has(fileName);
29
+ };
30
+
31
+ const walkFilesRecursive = (directory) => {
32
+ const entries = fs.readdirSync(directory, { withFileTypes: true });
33
+ const files = [];
34
+
35
+ for (const entry of entries) {
36
+ const entryPath = path.join(directory, entry.name);
37
+ if (entry.isDirectory()) {
38
+ files.push(...walkFilesRecursive(entryPath));
39
+ continue;
40
+ }
41
+
42
+ if (entry.isFile()) {
43
+ files.push(entryPath);
44
+ }
45
+ }
46
+
47
+ return files;
48
+ };
49
+
50
+ const loadHandlers = () => {
51
+ const files = walkFilesRecursive(apiDirectory).filter((filePath) => !shouldSkipApiFile(path.basename(filePath)));
52
+ const routeToHandler = new Map();
53
+
54
+ for (const filePath of files) {
55
+ const moduleExports = require(filePath);
56
+ if (!moduleExports || typeof moduleExports !== 'object') {
57
+ continue;
58
+ }
59
+
60
+ const fileBaseName = path.basename(filePath, '.js');
61
+ const exportedValue = moduleExports[fileBaseName];
62
+
63
+ if (typeof exportedValue !== 'function') {
64
+ continue;
65
+ }
66
+
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
+ }
102
+
103
+ routeToHandler.set(routePath, {
104
+ filePath,
105
+ exportName: fileBaseName,
106
+ handler,
107
+ usesFuncApi: Boolean(exportFuncApiConfig),
108
+ });
109
+ }
110
+
111
+ return routeToHandler;
112
+ };
113
+
114
+ const handlers = loadHandlers();
115
+
116
+ const server = http.createServer(async (req, res) => {
117
+ if (req.method === 'GET' && req.url === '/') {
118
+ const routes = Array.from(handlers.keys()).sort();
119
+ respondJson(res, 200, {
120
+ ok: true,
121
+ data: {
122
+ routes,
123
+ },
124
+ });
125
+ return;
126
+ }
127
+
128
+ const pathOnly = (req.url || '/').split('?')[0];
129
+ const matchedHandler = handlers.get(pathOnly);
130
+ if (!matchedHandler) {
131
+ respondJson(res, 404, {
132
+ ok: false,
133
+ error: {
134
+ code: 'NOT_FOUND',
135
+ message: `No handler found for route ${ pathOnly }`,
136
+ },
137
+ });
138
+ return;
139
+ }
140
+
141
+ try {
142
+ const body = await getRequestBody(req);
143
+ 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);
152
+
153
+ if (result === undefined) {
154
+ respondJson(res, 200, {
155
+ ok: true,
156
+ });
157
+ return;
158
+ }
159
+
160
+ respondJson(res, 200, result);
161
+ } catch (error) {
162
+ respondJson(res, 500, {
163
+ ok: false,
164
+ error: {
165
+ code: 'UNHANDLED_ERROR',
166
+ message: 'Unhandled server error.',
167
+ details: errorToReadable(error),
168
+ },
169
+ });
170
+ }
171
+ });
172
+
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
+ });
183
+
184
+ return server;
185
+ };
186
+
187
+ module.exports = {
188
+ server,
189
+ handlers,
190
+ loadHandlers,
191
+ startServer,
192
+ };
193
+
194
+ if (require.main === module) {
195
+ startServer();
196
+ }
@@ -0,0 +1,228 @@
1
+ const { logDeep } = require('./api/utils');
2
+ const { StringDecoder } = require('string_decoder');
3
+
4
+ const respondJson = (res, statusCode, payload) => {
5
+ logDeep(payload);
6
+ const body = JSON.stringify(payload);
7
+ res.writeHead(statusCode, {
8
+ 'Content-Type': 'application/json',
9
+ 'Content-Length': Buffer.byteLength(body),
10
+ });
11
+ res.end(body);
12
+ };
13
+
14
+ const errorToReadable = (error) => {
15
+ if (!error) {
16
+ return {
17
+ message: 'Unknown error',
18
+ };
19
+ }
20
+
21
+ if (typeof error === 'string') {
22
+ return {
23
+ message: error,
24
+ };
25
+ }
26
+
27
+ return {
28
+ name: error.name,
29
+ message: error.message,
30
+ stack: error.stack,
31
+ };
32
+ };
33
+
34
+ const getRequestBody = async (req) => {
35
+ if (req.method === 'GET' || req.method === 'HEAD') {
36
+ return undefined;
37
+ }
38
+
39
+ const decoder = new StringDecoder('utf8');
40
+
41
+ return await new Promise((resolve, reject) => {
42
+ let buffer = '';
43
+
44
+ req.on('data', (chunk) => {
45
+ buffer += decoder.write(chunk);
46
+ });
47
+
48
+ req.on('end', () => {
49
+ buffer += decoder.end();
50
+
51
+ if (!buffer.trim()) {
52
+ resolve(undefined);
53
+ return;
54
+ }
55
+
56
+ try {
57
+ resolve(JSON.parse(buffer));
58
+ } catch (error) {
59
+ reject(new Error(`Invalid JSON body: ${ error.message }`));
60
+ }
61
+ });
62
+
63
+ req.on('error', reject);
64
+ });
65
+ };
66
+
67
+ const argsFromBody = (body) => {
68
+ if (Array.isArray(body)) {
69
+ return body;
70
+ }
71
+
72
+ if (Array.isArray(body?.args)) {
73
+ return body.args;
74
+ }
75
+
76
+ if (body === undefined) {
77
+ return [];
78
+ }
79
+
80
+ return [body];
81
+ };
82
+
83
+ const mergeRequestContext = (requestContext, update) => {
84
+ if (!update || typeof update !== 'object') {
85
+ return requestContext;
86
+ }
87
+
88
+ return {
89
+ ...requestContext,
90
+ ...update,
91
+ };
92
+ };
93
+
94
+ const runRequestHandler = async (requestHandler, requestContext) => {
95
+ if (!requestHandler) {
96
+ return requestContext;
97
+ }
98
+
99
+ if (Array.isArray(requestHandler)) {
100
+ let updatedRequestContext = requestContext;
101
+ for (const requestHandlerStep of requestHandler) {
102
+ if (typeof requestHandlerStep !== 'function') {
103
+ throw new Error('requestHandler array only supports functions');
104
+ }
105
+ const stepOutput = await requestHandlerStep(updatedRequestContext);
106
+ updatedRequestContext = mergeRequestContext(updatedRequestContext, stepOutput);
107
+ }
108
+ return updatedRequestContext;
109
+ }
110
+
111
+ if (typeof requestHandler?.run !== 'function' && typeof requestHandler !== 'function') {
112
+ throw new Error('requestHandler must be a function, array of functions, or a Chain');
113
+ }
114
+
115
+ const handlerOutput = await requestHandler?.run(requestContext) || await requestHandler(requestContext);
116
+ return mergeRequestContext(requestContext, handlerOutput);
117
+ };
118
+
119
+ const funcApi = (func, config = {}) => {
120
+ const {
121
+ requestHandler,
122
+ argsWarden,
123
+ validators = [],
124
+ requestVerifiers = [],
125
+ bodyModifiers = [],
126
+ passThroughReq = false,
127
+ passThroughBody = false,
128
+ } = config;
129
+
130
+ const argNames = argsWarden?.argNames();
131
+
132
+ return async ({
133
+ req,
134
+ res,
135
+ body,
136
+ args,
137
+ }) => {
138
+ let requestContext = {
139
+ req,
140
+ res,
141
+ body,
142
+ args,
143
+ };
144
+
145
+ requestContext = await runRequestHandler(requestHandler, requestContext);
146
+ if (requestContext?.response !== undefined) {
147
+ return requestContext.response;
148
+ }
149
+
150
+ for (const requestVerifier of requestVerifiers) {
151
+ const verified = await requestVerifier(
152
+ requestContext.req,
153
+ requestContext.res,
154
+ requestContext.body,
155
+ requestContext,
156
+ );
157
+ if (!verified) {
158
+ return {
159
+ ok: false,
160
+ error: {
161
+ code: 'REQUEST_NOT_VERIFIED',
162
+ message: 'Request verification failed.',
163
+ },
164
+ };
165
+ }
166
+ }
167
+
168
+ let modifiedBody = requestContext.body;
169
+ for (const bodyModifier of bodyModifiers) {
170
+ modifiedBody = await bodyModifier(modifiedBody, requestContext.req, requestContext.res, requestContext);
171
+ }
172
+ requestContext.body = modifiedBody;
173
+
174
+ if (argsWarden) {
175
+ const rejectResponse = await argsWarden.responseIfRejectingArgs(
176
+ Object.fromEntries(argNames.map((argName) => [argName, modifiedBody?.[argName]])),
177
+ { ...modifiedBody },
178
+ );
179
+ if (rejectResponse) {
180
+ return rejectResponse;
181
+ }
182
+ }
183
+
184
+ for (const validator of validators) {
185
+ const valid = await validator(
186
+ modifiedBody,
187
+ requestContext.req,
188
+ requestContext.res,
189
+ requestContext,
190
+ );
191
+ if (!valid) {
192
+ return {
193
+ ok: false,
194
+ error: {
195
+ code: 'INVALID_BODY',
196
+ message: 'Request body failed validation.',
197
+ },
198
+ };
199
+ }
200
+ }
201
+
202
+ let callArgs = requestContext.args;
203
+ if (passThroughReq) {
204
+ callArgs = [requestContext.req];
205
+ } else if (passThroughBody) {
206
+ callArgs = [modifiedBody];
207
+ } else if (argNames?.length) {
208
+ callArgs = argNames.map((argName) => modifiedBody?.[argName]);
209
+
210
+ if (
211
+ !argNames.includes('options')
212
+ && modifiedBody?.options !== undefined
213
+ ) {
214
+ callArgs.push(modifiedBody.options);
215
+ }
216
+ }
217
+
218
+ return await func(...callArgs);
219
+ };
220
+ };
221
+
222
+ module.exports = {
223
+ respondJson,
224
+ errorToReadable,
225
+ getRequestBody,
226
+ argsFromBody,
227
+ funcApi,
228
+ };
package/test.js ADDED
@@ -0,0 +1,19 @@
1
+ const { shopifyOrdersGet } = require('./api/shopify/shopifyOrdersGet');
2
+
3
+ const [
4
+ storeHandle,
5
+ apiKey,
6
+ ] = process.argv.slice(2);
7
+
8
+ shopifyOrdersGet(
9
+ {
10
+ credsObject: {
11
+ STORE_HANDLE: storeHandle,
12
+ API_KEY: apiKey,
13
+ },
14
+ },
15
+ ).then((result) => {
16
+ console.log('result', result);
17
+ });
18
+
19
+ // node test.js a-b-c shpat_111111 1111