@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
@@ -0,0 +1,21 @@
1
+ shopify:
2
+ au:
3
+ STORE_HANDLE: ________________________
4
+ API_KEY: shpat________________________
5
+
6
+ peoplevox:
7
+ CLIENT_ID: _____________________________
8
+ USERNAME: ______________________________
9
+ PASSWORD: ______________________________
10
+
11
+ youtube:
12
+ API_KEY: _______________________________
13
+
14
+ yotpo:
15
+ au:
16
+ API_KEY: _____________________________
17
+ GUID: ________________________________
18
+ MERCHANT_ID: _________________________
19
+
20
+ linear:
21
+ API_KEY: _______________________________
package/AGENTS.md ADDED
@@ -0,0 +1,13 @@
1
+ # Mineral — agent instructions
2
+
3
+ Notes for AI assistants working in this directory. **Read this file at the start of mineral tasks** and follow anything here that isn't overridden by the user in chat.
4
+
5
+ ## Misc
6
+ - When working in shopify, refer to gids (gid://shopify/Page/12345678) explicitly as "gids", and use "id" to refer to the number itself (12345678). "id" may still be required for use of the GraphQL API but semantically we should make this distinction.
7
+ - funcApiConfig should not list `options` in `argNames` — it is optional. When present on the request body, `funcApi` appends it after the named args.
8
+ - When implementing API functions that mirror official functions, include a link to the documentation at the top of the file as a comment.
9
+ - Don't add to this file unless asked.
10
+ - Use snake_case for package.json commands, with colon if appropriate.
11
+ - Use newlines and trailing commas where appropriate to minimise future diffs, e.g. in function args.
12
+ - If you make a new .env or .creds.yml variable, update the correlating .sample file.
13
+ - In Shopify, don't normalise and take gids as inputs if you can just take the id - the non-gid number - instead.
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # mineral
2
+
3
+ The [bedrock](https://github.com/GorgonFreeman/bedrock) middleware, refactored for compatibility with other projects and more consistent standards.
4
+
5
+ ## Problems with bedrock
6
+
7
+ - **Inconsistent response format**
8
+ - `success: true` vs `success: false` for calls that succeed but return no data
9
+ - No distinction between technical failures and valid business errors
10
+ - Mixed use of `error` vs `errors` in responses
11
+ - **Portability** — `.creds.yml` and related structures resist reuse outside the repo
12
+ - **Auth** — helpers can fail without retrying, especially when using creds from Upstash
13
+ - **Architecture** — boundaries between core and private functions could be firmer
14
+
15
+ ## The plan
16
+
17
+ - **The response format to rule them all**
18
+ ```typescript
19
+ interface Response {
20
+ ok: boolean
21
+ data?: unknown
22
+ error?: {
23
+ code: string
24
+ message: string
25
+ details?: unknown
26
+ }
27
+ meta?: {
28
+ page?: number
29
+ pageSize?: number
30
+ total?: number
31
+ [key: string]: unknown
32
+ }
33
+ results?: Response[]
34
+ }
35
+ ```
36
+ - `ok` — `true` if the intent executed; `false` otherwise
37
+ - `data` — any info returned
38
+ - `error` — when present, multiple errors go in `error.details`
39
+ - `meta` — pagination, total items, etc.
40
+ - `results` — array of responses from a queue or batch of calls
41
+ - [Examples](_docs/standard_response_examples.md)
42
+ - **BYO Creds**
43
+ ```
44
+ {
45
+ credsPath,
46
+ shopifyCredsPath,
47
+ credsObject,
48
+ credsProvider, // function that returns a credsObject
49
+ }
50
+ ```
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. In the serve functions, allow it to operate from a different directory, finding .hosting.yml, .creds.yml, etc. within it.
@@ -0,0 +1,45 @@
1
+ const fs = require('fs').promises;
2
+ const path = require('path');
3
+ const yaml = require('yaml');
4
+
5
+ const mineralRoot = path.join(__dirname, '..');
6
+ const credsYmlPath = path.join(mineralRoot, '.creds.yml');
7
+ const envPath = path.join(mineralRoot, '.env');
8
+
9
+ (async () => {
10
+ const credsText = await fs.readFile(credsYmlPath, 'utf8');
11
+ const credsFromYml = yaml.parse(credsText);
12
+ const credsJsonString = JSON.stringify(credsFromYml);
13
+ const newCredsLine = `CREDS=${ credsJsonString }`;
14
+
15
+ let envFileContents = '';
16
+ try {
17
+ envFileContents = await fs.readFile(envPath, 'utf8');
18
+ } catch (err) {
19
+ if (err.code !== 'ENOENT') {
20
+ throw err;
21
+ }
22
+ }
23
+
24
+ if (!envFileContents) {
25
+ await fs.writeFile(envPath, `${ newCredsLine }\n`);
26
+ console.log('Created .env with CREDS');
27
+ return;
28
+ }
29
+
30
+ if (/^CREDS=/m.test(envFileContents)) {
31
+ const updatedFileContents = envFileContents.replace(/^CREDS=.*$/m, newCredsLine);
32
+
33
+ if (updatedFileContents === envFileContents) {
34
+ console.log('CREDS already up to date');
35
+ return;
36
+ }
37
+
38
+ await fs.writeFile(envPath, updatedFileContents);
39
+ console.log('Updated CREDS in .env');
40
+ return;
41
+ }
42
+
43
+ await fs.appendFile(envPath, `\n\n${ newCredsLine }\n`);
44
+ console.log('Appended CREDS to .env');
45
+ })();
@@ -0,0 +1,361 @@
1
+ const fs = require('fs').promises;
2
+ const path = require('path');
3
+ const { spawn } = require('child_process');
4
+ const { askQuestion, capitaliseString } = require('../api/utils');
5
+
6
+ const apiDirectory = path.join(__dirname, '../api');
7
+ const rootExampleJsPath = path.join(apiDirectory, '_example.js');
8
+
9
+ const excludedDirs = new Set([
10
+ 'node_modules',
11
+ ]);
12
+
13
+ const printHelp = () => {
14
+ console.log(`
15
+ Usage:
16
+ npm run new
17
+ npm run new -- --dir <apiSubdir> --name <name> [--template <exampleTemplate>] [--commit]
18
+
19
+ Options:
20
+ --dir, -d api subdirectory (e.g. peoplevox, shopify)
21
+ --name, -n function name suffix (e.g. orderGet → peoplevoxOrderGet)
22
+ --template, -t example template to copy (default: _example.js in dir, else api/_example.js)
23
+ examples: _example.js, getsingle, _example.getsingle.js
24
+ --commit auto-commit the stub after creation
25
+ --help, -h show this help
26
+
27
+ Examples:
28
+ npm run new -- --dir peoplevox --name orderGet
29
+ npm run new -- --dir peoplevox --name orderGet --template _example.js
30
+ npm run new -- --dir shopify --name pageGet --template getsingle
31
+ `);
32
+ };
33
+
34
+ const parseCliArgs = (argv) => {
35
+ const args = {
36
+ dir: null,
37
+ name: null,
38
+ template: null,
39
+ commit: false,
40
+ help: false,
41
+ };
42
+
43
+ for (let i = 0; i < argv.length; i++) {
44
+ const arg = argv[i];
45
+
46
+ if (arg === 'commit') {
47
+ args.commit = true;
48
+ continue;
49
+ }
50
+
51
+ if (arg === '--help' || arg === '-h') {
52
+ args.help = true;
53
+ continue;
54
+ }
55
+
56
+ if (arg === '--commit') {
57
+ args.commit = true;
58
+ continue;
59
+ }
60
+
61
+ if (arg === '--dir' || arg === '-d') {
62
+ args.dir = argv[++i];
63
+ continue;
64
+ }
65
+
66
+ if (arg === '--name' || arg === '-n') {
67
+ args.name = argv[++i];
68
+ continue;
69
+ }
70
+
71
+ if (arg === '--template' || arg === '-t') {
72
+ args.template = argv[++i];
73
+ continue;
74
+ }
75
+
76
+ if (arg.startsWith('--dir=')) {
77
+ args.dir = arg.slice('--dir='.length);
78
+ continue;
79
+ }
80
+
81
+ if (arg.startsWith('--name=')) {
82
+ args.name = arg.slice('--name='.length);
83
+ continue;
84
+ }
85
+
86
+ if (arg.startsWith('--template=')) {
87
+ args.template = arg.slice('--template='.length);
88
+ continue;
89
+ }
90
+ }
91
+
92
+ return args;
93
+ };
94
+
95
+ const gitCommitAll = (message) => {
96
+ console.log('Committing in background (2 second delay)...');
97
+
98
+ const commitProcess = spawn('sh', ['-c', `sleep 2 && git add . && git commit -m '${ message }'`], {
99
+ detached: true,
100
+ stdio: 'ignore',
101
+ });
102
+
103
+ commitProcess.unref();
104
+ };
105
+
106
+ const findExampleFiles = async (dirPath) => {
107
+ try {
108
+ const files = await fs.readdir(dirPath);
109
+ const exampleFiles = files.filter(file => file.startsWith('_example'));
110
+
111
+ return exampleFiles.map(file => {
112
+ const match = file.match(/^_example(?:\.(.+))?\.js$/);
113
+ if (match) {
114
+ return {
115
+ filename: file,
116
+ displayName: match[1] || '_example.js',
117
+ fullPath: path.join(dirPath, file),
118
+ };
119
+ }
120
+ return null;
121
+ }).filter(Boolean);
122
+ } catch (err) {
123
+ return [];
124
+ }
125
+ };
126
+
127
+ const sortExampleFiles = (exampleFiles) => {
128
+ return [...exampleFiles].sort((a, b) => {
129
+ if (a.displayName === '_example.js') return -1;
130
+ if (b.displayName === '_example.js') return 1;
131
+ return a.displayName.localeCompare(b.displayName);
132
+ });
133
+ };
134
+
135
+ const normalizeTemplateArg = (templateArg) => {
136
+ if (!templateArg) {
137
+ return null;
138
+ }
139
+
140
+ if (templateArg.endsWith('.js')) {
141
+ return templateArg;
142
+ }
143
+
144
+ if (templateArg.startsWith('_example')) {
145
+ return `${ templateArg }.js`;
146
+ }
147
+
148
+ return `_example.${ templateArg }.js`;
149
+ };
150
+
151
+ const resolveTemplateFromArg = async (exampleFiles, templateArg, dir) => {
152
+ if (!templateArg) {
153
+ const defaultInDir = exampleFiles.find(file => file.displayName === '_example.js');
154
+ if (defaultInDir) {
155
+ return defaultInDir.fullPath;
156
+ }
157
+
158
+ await fs.access(rootExampleJsPath);
159
+ return rootExampleJsPath;
160
+ }
161
+
162
+ const normalizedFilename = normalizeTemplateArg(templateArg);
163
+
164
+ const matchInDir = exampleFiles.find(file => {
165
+ return file.filename === normalizedFilename
166
+ || file.displayName === templateArg
167
+ || file.displayName === normalizedFilename;
168
+ });
169
+
170
+ if (matchInDir) {
171
+ return matchInDir.fullPath;
172
+ }
173
+
174
+ if (normalizedFilename === '_example.js') {
175
+ await fs.access(rootExampleJsPath);
176
+ return rootExampleJsPath;
177
+ }
178
+
179
+ throw new Error(`Template not found: ${ templateArg } (looked for ${ normalizedFilename } in api/${ dir })`);
180
+ };
181
+
182
+ const scriptFileContents = async (name, selectedTemplate) => {
183
+ const exampleFileContents = await fs.readFile(selectedTemplate, 'utf-8');
184
+ return exampleFileContents.replace(/FUNC/g, name);
185
+ };
186
+
187
+ const getDirs = async () => {
188
+ const dirents = await fs.readdir(apiDirectory, { withFileTypes: true });
189
+ return dirents
190
+ .filter(dirent => dirent.isDirectory())
191
+ .map(dirent => dirent.name)
192
+ .filter(name => name[0] !== '_' && name[0] !== '.')
193
+ .filter(name => !excludedDirs.has(name));
194
+ };
195
+
196
+ const buildFuncName = (dir, name) => {
197
+ const hasUnderscore = dir?.includes('_');
198
+ const separator = hasUnderscore ? '_' : '';
199
+ const formattedName = hasUnderscore ? name : capitaliseString(name);
200
+ return dir ? `${ dir }${ separator }${ formattedName }` : name;
201
+ };
202
+
203
+ const selectDirInteractive = async (dirs) => {
204
+ const dirIndex = await askQuestion(`Where does your new function live? \n${
205
+ dirs.map((dir, index) => {
206
+ return `[${ index + 1 }] ${ dir }`;
207
+ }).join('\n')
208
+ }\n`);
209
+
210
+ const dir = dirs[dirIndex - 1];
211
+
212
+ if (!dir) {
213
+ throw new Error(`${ dirIndex } not a valid option.`);
214
+ }
215
+
216
+ return dir;
217
+ };
218
+
219
+ const selectTemplateInteractive = async (exampleFiles, dir) => {
220
+ if (exampleFiles.length === 0) {
221
+ await fs.access(rootExampleJsPath);
222
+ console.log('\nUsing template: api/_example.js');
223
+ return rootExampleJsPath;
224
+ }
225
+
226
+ if (exampleFiles.length === 1) {
227
+ console.log(`\nUsing template: ${ exampleFiles[0].displayName }`);
228
+ return exampleFiles[0].fullPath;
229
+ }
230
+
231
+ console.log(`\nFound ${ exampleFiles.length } template(s) in api/${ dir }:`);
232
+ const templateIndex = await askQuestion(`Which template would you like to use? (press enter for _example.js) \n${
233
+ exampleFiles.map((file, index) => {
234
+ return `[${ index + 1 }] ${ file.displayName }`;
235
+ }).join('\n')
236
+ }\n`);
237
+
238
+ if (!templateIndex || templateIndex.trim() === '') {
239
+ const defaultFile = exampleFiles.find(file => file.displayName === '_example.js');
240
+ if (defaultFile) {
241
+ return defaultFile.fullPath;
242
+ }
243
+
244
+ await fs.access(rootExampleJsPath);
245
+ console.log('\nUsing template: api/_example.js');
246
+ return rootExampleJsPath;
247
+ }
248
+
249
+ const selectedFile = exampleFiles[templateIndex - 1];
250
+ if (!selectedFile) {
251
+ throw new Error(`${ templateIndex } not a valid option.`);
252
+ }
253
+
254
+ return selectedFile.fullPath;
255
+ };
256
+
257
+ const selectNameInteractive = async (dir) => {
258
+ const promptSuffix = dir?.includes('_') ? `${ dir }_` : dir;
259
+ const name = await askQuestion(`What do you want to call it? ${ promptSuffix }`);
260
+
261
+ if (!name) {
262
+ throw new Error('Error getting script name');
263
+ }
264
+
265
+ return name;
266
+ };
267
+
268
+ const writeNewFunction = async ({
269
+ dir,
270
+ name,
271
+ selectedTemplate,
272
+ shouldAutoCommit,
273
+ }) => {
274
+ const funcName = buildFuncName(dir, name);
275
+ const outputPath = path.join(apiDirectory, dir, `${ funcName }.js`);
276
+
277
+ try {
278
+ await fs.access(outputPath);
279
+ throw new Error(`File already exists: ${ outputPath }`);
280
+ } catch (err) {
281
+ if (err.code !== 'ENOENT') {
282
+ throw err;
283
+ }
284
+ }
285
+
286
+ const script = await scriptFileContents(funcName, selectedTemplate);
287
+ await fs.writeFile(outputPath, script);
288
+
289
+ console.log(`\nCreated ${ outputPath }`);
290
+
291
+ if (shouldAutoCommit) {
292
+ gitCommitAll(`${ funcName } stub`);
293
+ }
294
+
295
+ return outputPath;
296
+ };
297
+
298
+ const createNewFunction = async () => {
299
+ const cliArgs = parseCliArgs(process.argv.slice(2));
300
+
301
+ if (cliArgs.help) {
302
+ printHelp();
303
+ return;
304
+ }
305
+
306
+ const dirs = await getDirs();
307
+ const nonInteractive = Boolean(cliArgs.dir || cliArgs.name);
308
+
309
+ if (nonInteractive) {
310
+ if (!cliArgs.dir || !cliArgs.name) {
311
+ console.error('Non-interactive mode requires both --dir and --name.');
312
+ printHelp();
313
+ process.exitCode = 1;
314
+ return;
315
+ }
316
+
317
+ if (!dirs.includes(cliArgs.dir)) {
318
+ console.error(`Invalid --dir "${ cliArgs.dir }". Available: ${ dirs.join(', ') }`);
319
+ process.exitCode = 1;
320
+ return;
321
+ }
322
+
323
+ const exampleFiles = sortExampleFiles(await findExampleFiles(path.join(apiDirectory, cliArgs.dir)));
324
+
325
+ try {
326
+ const selectedTemplate = await resolveTemplateFromArg(exampleFiles, cliArgs.template, cliArgs.dir);
327
+ console.log(`Using template: ${ path.relative(path.join(__dirname, '..'), selectedTemplate) }`);
328
+
329
+ await writeNewFunction({
330
+ dir: cliArgs.dir,
331
+ name: cliArgs.name,
332
+ selectedTemplate,
333
+ shouldAutoCommit: process.env.AUTO_COMMIT_STUBS === 'true' || cliArgs.commit,
334
+ });
335
+ } catch (err) {
336
+ console.error(err.message || err);
337
+ process.exitCode = 1;
338
+ }
339
+
340
+ return;
341
+ }
342
+
343
+ try {
344
+ const dir = await selectDirInteractive(dirs);
345
+ const exampleFiles = sortExampleFiles(await findExampleFiles(path.join(apiDirectory, dir)));
346
+ const selectedTemplate = await selectTemplateInteractive(exampleFiles, dir);
347
+ const name = await selectNameInteractive(dir);
348
+
349
+ await writeNewFunction({
350
+ dir,
351
+ name,
352
+ selectedTemplate,
353
+ shouldAutoCommit: process.env.AUTO_COMMIT_STUBS === 'true' || cliArgs.commit,
354
+ });
355
+ } catch (err) {
356
+ console.error(err.message || err);
357
+ process.exitCode = 1;
358
+ }
359
+ };
360
+
361
+ createNewFunction();
@@ -0,0 +1,67 @@
1
+ # Response examples
2
+
3
+ Examples of the [mineral response format](../README.md#the-plan).
4
+
5
+ ```typescript
6
+ // Fetch product by ID — found
7
+ { ok: true, data: { id: ... } }
8
+
9
+ // Fetch product by ID — not found
10
+ {
11
+ ok: false,
12
+ error: {
13
+ code: "NOT_FOUND",
14
+ message: "Product sku-999 does not exist",
15
+ },
16
+ }
17
+
18
+ // Fetch collection — results
19
+ {
20
+ ok: true,
21
+ data: [ { id: "sku-001" }, { id: "sku-002" } ],
22
+ meta: { total: 2 },
23
+ }
24
+
25
+ // Fetch collection — none found
26
+ {
27
+ ok: true,
28
+ data: [],
29
+ meta: { total: 0 },
30
+ }
31
+
32
+ // Delete product — success
33
+ {
34
+ ok: true,
35
+ }
36
+
37
+ // Mutate single product — validation failure
38
+ {
39
+ ok: false,
40
+ error: {
41
+ code: "INVALID_PRICE",
42
+ message: "Price cannot be negative",
43
+ field: "price",
44
+ },
45
+ }
46
+
47
+ // Batch mutate — mixed results
48
+ {
49
+ ok: false,
50
+ results: [
51
+ {
52
+ ok: true,
53
+ data: { id: "sku-001", price: "49.95" },
54
+ },
55
+ {
56
+ ok: false,
57
+ meta: { id: "sku-002" },
58
+ error: {
59
+ code: "INVALID_PRICE",
60
+ message: "Price cannot be negative",
61
+ field: "price",
62
+ },
63
+ },
64
+ ],
65
+ meta: { total: 2, succeeded: 1, failed: 1 },
66
+ }
67
+ ```
@@ -0,0 +1,45 @@
1
+ const { credsValidator } = require('./validators');
2
+ const { ArgsWarden } = require('./utils');
3
+
4
+ const argsWarden = new ArgsWarden([
5
+ ['credsPayload', credsValidator],
6
+ ['arg'],
7
+ ]);
8
+
9
+ const FUNC = async (
10
+ credsPayload,
11
+ arg,
12
+ options = {},
13
+ ) => {
14
+
15
+ const rejectResponse = await argsWarden.responseIfRejectingArgs({ credsPayload, arg });
16
+ if (rejectResponse) {
17
+ return rejectResponse;
18
+ }
19
+
20
+ return {
21
+ ok: true,
22
+ data: {
23
+ arg,
24
+ options,
25
+ },
26
+ };
27
+ };
28
+
29
+ const funcApiConfig = {
30
+ argsWarden,
31
+ };
32
+
33
+ module.exports = {
34
+ FUNC,
35
+ funcApiConfig,
36
+ };
37
+
38
+ /*
39
+ curl -X POST "http://localhost:8000/FUNC" \
40
+ -H "Content-Type: application/json" \
41
+ -d '{
42
+ "credsPayload": { "credsPath": "shopify.au" },
43
+ "arg": "1234"
44
+ }'
45
+ */
@@ -0,0 +1,3 @@
1
+ module.exports = {
2
+ HOSTED: !!process.env.HOSTED,
3
+ };
@@ -0,0 +1,5 @@
1
+ const LINEAR_GRAPHQL_URL = 'https://api.linear.app/graphql';
2
+
3
+ module.exports = {
4
+ LINEAR_GRAPHQL_URL,
5
+ };
@@ -0,0 +1,43 @@
1
+ const { LINEAR_GRAPHQL_URL } = require('../linear/linear.constants');
2
+ const {
3
+ FetchClient,
4
+ Chain,
5
+ fetchClientCommonSteps,
6
+ } = require('../utils');
7
+
8
+ const addUrlAndAuthHeaders = async (state) => {
9
+ const { requestPayload, context } = state;
10
+ const { creds } = context;
11
+ const { API_KEY } = creds;
12
+
13
+ return {
14
+ requestPayload: {
15
+ ...requestPayload,
16
+ url: requestPayload.url || LINEAR_GRAPHQL_URL,
17
+ method: requestPayload.method || 'post',
18
+ headers: {
19
+ 'Content-Type': 'application/json',
20
+ Authorization: API_KEY,
21
+ ...requestPayload.headers,
22
+ },
23
+ },
24
+ };
25
+ };
26
+
27
+ const linearClientRequestPreparer = new Chain([
28
+ addUrlAndAuthHeaders,
29
+ ]);
30
+
31
+ const linearClientResponseInterpreter = new Chain([
32
+ fetchClientCommonSteps.exitEarlyOnNotOk,
33
+ fetchClientCommonSteps.exitEarlyOnGraphqlErrors,
34
+ ]);
35
+
36
+ const linearClient = new FetchClient({
37
+ requestPreparer: linearClientRequestPreparer,
38
+ responseInterpreter: linearClientResponseInterpreter,
39
+ });
40
+
41
+ module.exports = {
42
+ linearClient,
43
+ };