@foxtware/mineral 0.1.22 → 0.1.23

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.
@@ -1,5 +1,6 @@
1
1
  const csvtojson = require('csvtojson');
2
2
  const xml2js = require('xml2js');
3
+ const { HOSTED } = require('../constants');
3
4
  const { resolveCreds } = require('../pipelineSteps');
4
5
  const {
5
6
  FetchClient,
@@ -69,7 +70,7 @@ const useSoapEnvelope = async (state) => {
69
70
  sessionId,
70
71
  });
71
72
 
72
- logDeep({ envelopeXml });
73
+ !HOSTED && logDeep({ envelopeXml });
73
74
 
74
75
  return {
75
76
  requestPayload: {
@@ -0,0 +1,127 @@
1
+ // https://shopify.dev/docs/api/admin-graphql/latest/mutations/metafieldDefinitionDelete
2
+
3
+ const { credsValidator } = require('../validators');
4
+ const { ArgsWarden, valueProvided, actionSingleOrMultiple, everyIfArray } = require('../utils');
5
+ const { shopifyMutationDo } = require('../shopify/shopifyMutationDo');
6
+
7
+ const metafieldDefinitionIdentifierValidator = (metafieldDefinitionIdentifier) => {
8
+ const {
9
+ id,
10
+ key,
11
+ namespace,
12
+ ownerType,
13
+ } = metafieldDefinitionIdentifier;
14
+ return (key && namespace && ownerType)
15
+ || valueProvided(id);
16
+ };
17
+
18
+ const argsWarden = new ArgsWarden([
19
+ ['credsPayload', (i) => everyIfArray(credsValidator, i)],
20
+ ['metafieldDefinitionIdentifier', (i) => everyIfArray(metafieldDefinitionIdentifierValidator, i)],
21
+ ]);
22
+
23
+ const shopifyMetafieldDefinitionDeleteSingle = async (
24
+ credsPayload,
25
+ metafieldDefinitionIdentifier,
26
+ {
27
+ apiVersion,
28
+ returnSchema = 'deletedDefinitionId',
29
+ deleteAllAssociatedMetafields = false,
30
+ } = {},
31
+ ) => {
32
+
33
+ const {
34
+ id,
35
+ key,
36
+ namespace,
37
+ ownerType,
38
+ } = metafieldDefinitionIdentifier;
39
+
40
+ return shopifyMutationDo(
41
+ credsPayload,
42
+ 'metafieldDefinitionDelete',
43
+ {
44
+ mutationVariables: {
45
+ ...(id && { id: {
46
+ type: 'ID!',
47
+ value: `gid://shopify/MetafieldDefinition/${ id }`,
48
+ } }),
49
+ ...((key && namespace && ownerType) && {
50
+ identifier: {
51
+ type: 'MetafieldDefinitionIdentifierInput!',
52
+ value: {
53
+ key,
54
+ namespace,
55
+ ownerType,
56
+ },
57
+ },
58
+ }),
59
+ deleteAllAssociatedMetafields: {
60
+ type: 'Boolean!',
61
+ value: deleteAllAssociatedMetafields,
62
+ },
63
+ },
64
+ returnSchema,
65
+ apiVersion,
66
+ },
67
+ );
68
+ };
69
+
70
+ const shopifyMetafieldDefinitionDelete = async (
71
+ credsPayload,
72
+ metafieldDefinitionIdentifier,
73
+ {
74
+ queueRunOptions,
75
+ ...options
76
+ } = {},
77
+ ) => {
78
+
79
+ const rejectResponse = await argsWarden.responseIfRejectingArgs({
80
+ credsPayload,
81
+ metafieldDefinitionIdentifier,
82
+ });
83
+ if (rejectResponse) {
84
+ return rejectResponse;
85
+ }
86
+
87
+ return actionSingleOrMultiple(
88
+ [credsPayload, metafieldDefinitionIdentifier],
89
+ shopifyMetafieldDefinitionDeleteSingle,
90
+ (credsPayloadItem, metafieldDefinitionIdentifierItem) => ({
91
+ args: [
92
+ credsPayloadItem,
93
+ metafieldDefinitionIdentifierItem,
94
+ options,
95
+ ],
96
+ }),
97
+ {
98
+ ...(queueRunOptions ? { queueRunOptions } : {}),
99
+ },
100
+ );
101
+ };
102
+
103
+ const funcApiConfig = {
104
+ argsWarden,
105
+ };
106
+
107
+ module.exports = {
108
+ shopifyMetafieldDefinitionDelete,
109
+ funcApiConfig,
110
+ };
111
+
112
+ /*
113
+ curl -X POST "http://localhost:8000/shopifyMetafieldDefinitionDelete" \
114
+ -H "Content-Type: application/json" \
115
+ -d '{
116
+ "credsPayload": [
117
+ { "credsPath": "shopify.au" },
118
+ { "credsPath": "shopify.us" },
119
+ { "credsPath": "shopify.uk" }
120
+ ],
121
+ "metafieldDefinitionIdentifier": {
122
+ "key": "associations",
123
+ "namespace": "merch",
124
+ "ownerType": "PRODUCT"
125
+ }
126
+ }'
127
+ */
@@ -1,4 +1,4 @@
1
- const { logDeep, ArgsWarden } = require('../utils');
1
+ const { ArgsWarden } = require('../utils');
2
2
  const { credsValidator } = require('../validators');
3
3
  const { shopifyClient } = require('./shopify.utils');
4
4
 
@@ -68,7 +68,6 @@ const shopifyOrdersGet = async (
68
68
  return response;
69
69
  }
70
70
 
71
- logDeep('response', response);
72
71
  return response;
73
72
  };
74
73
 
@@ -0,0 +1,109 @@
1
+ // https://shopify.dev/docs/api/admin-graphql/latest/queries/shopifyqlQuery
2
+ // Requires API version 2025-10+ and the read_reports access scope.
3
+
4
+ const { credsValidator } = require('../validators');
5
+ const { ArgsWarden } = require('../utils');
6
+ const { shopifyClient } = require('./shopify.utils');
7
+
8
+ const defaultAttrs = `
9
+ tableData {
10
+ columns {
11
+ name
12
+ dataType
13
+ displayName
14
+ }
15
+ rows
16
+ }
17
+ parseErrors
18
+ `.trim();
19
+
20
+ const argsWarden = new ArgsWarden([
21
+ ['credsPayload', credsValidator],
22
+ ['query'],
23
+ ]);
24
+
25
+ const shopifyShopifyqlQuery = async (
26
+ credsPayload,
27
+ query,
28
+ {
29
+ apiVersion,
30
+ attrs = defaultAttrs,
31
+ } = {},
32
+ ) => {
33
+
34
+ const rejectResponse = await argsWarden.responseIfRejectingArgs({
35
+ credsPayload,
36
+ query,
37
+ });
38
+ if (rejectResponse) {
39
+ return rejectResponse;
40
+ }
41
+
42
+ const response = await shopifyClient.fetch({
43
+ requestPayload: {
44
+ method: 'post',
45
+ body: {
46
+ query: `
47
+ query Shopifyql($query: String!) {
48
+ shopifyqlQuery(query: $query) {
49
+ ${ attrs }
50
+ }
51
+ }
52
+ `,
53
+ variables: {
54
+ query,
55
+ },
56
+ },
57
+ },
58
+ context: {
59
+ credsPayload,
60
+ apiVersion,
61
+ resultPath: 'data.shopifyqlQuery',
62
+ },
63
+ });
64
+
65
+ if (!response?.ok) {
66
+ return response;
67
+ }
68
+
69
+ const parseErrors = response?.data?.parseErrors;
70
+ if (Array.isArray(parseErrors) && parseErrors.length) {
71
+ return {
72
+ ok: false,
73
+ error: {
74
+ code: 'PARSE_ERROR',
75
+ message: parseErrors.join('; '),
76
+ details: parseErrors,
77
+ },
78
+ data: response.data,
79
+ };
80
+ }
81
+
82
+ return response;
83
+ };
84
+
85
+ const funcApiConfig = {
86
+ argsWarden,
87
+ };
88
+
89
+ module.exports = {
90
+ shopifyShopifyqlQuery,
91
+ funcApiConfig,
92
+ };
93
+
94
+ /*
95
+ curl -X POST "http://localhost:8000/shopifyShopifyqlQuery" \
96
+ -H "Content-Type: application/json" \
97
+ -d '{
98
+ "credsPayload": { "credsPath": "shopify.au" },
99
+ "query": "FROM sales SHOW total_sales, orders TIMESERIES day SINCE -7d ORDER BY day ASC"
100
+ }'
101
+
102
+ Example command to get at last restock date
103
+ curl -X POST "http://localhost:8000/shopifyShopifyqlQuery" \
104
+ -H "Content-Type: application/json" \
105
+ -d '{
106
+ "credsPayload": { "credsPath": "shopify.au" },
107
+ "query": "FROM inventory_adjustment_history SHOW inventory_adjustment_change, product_variant_title, product_variant_sku, day GROUP BY product_variant_title, product_variant_sku, day HAVING inventory_adjustment_change > 10 ORDER BY day DESC LIMIT 10"
108
+ }'
109
+ */
@@ -1,7 +1,7 @@
1
1
  # Tagalys
2
2
 
3
- - [Custom integration API (V1)](https://static.tagalys.com/docs/api/custom-integration/)
4
- - [Support: custom platform integration](https://support.tagalys.com/how-can-i-install-tagalys-to-my-custom-built-ecommerce-platform)
5
- - [Integration overview](https://support.tagalys.com/integration)
3
+ - [Storefront API (v2)](https://tagalys.notion.site/Storefront-API-v2-20eacdd38c2080d58d3fd0cf6f24e435)
6
4
 
7
- Region-specific HTTPS origins with POST JSON (and FormData for feeds); every call includes an `identification` object (`client_code`, `api_key`, `store_id`) using the private key for sync/config and the public key for storefront search and analytics.
5
+ Region-specific HTTPS origins (`https://api-r{n}.tagalys.com`) with REST-style JSON under `/v2`. Storefront reads use GET; analytics ingest uses `POST /v2/analytics/events` with a JSON body. Browser requests authenticate with the `shop_id` query param (the shop’s `*.myshopify.com` domain). Server-side integrations should also pass `storefront_api_key`.
6
+
7
+ Endpoints: `GET /v2/collections/:collection_id`, `GET /v2/search`, `GET /v2/search_suggestions`, `GET /v2/popular_searches`, `GET /v2/recommendations/:recommendation_id`, `POST /v2/analytics/events`. Collections and search support pagination (`page`, `per_page`, capped at 10,000 products), sorting (`sort`, `include[]=sort_options`), filtering (`filter[...]`, `include[]=filters`), and scope (`scope[...]`). Use `include[]=products` or `include[]=product_ids` to control product payloads. Optional request context on collections, search, and recommendations: `country`, `language`, `segment_tag`. Errors return JSON with `error.type`, `error.code`, and `error.message`.
package/api/utils.js CHANGED
@@ -1180,6 +1180,10 @@ class Getter extends EventEmitter {
1180
1180
  items = ensureArray(response);
1181
1181
  }
1182
1182
 
1183
+ if (!items) {
1184
+ logDeep({ response });
1185
+ }
1186
+
1183
1187
  if (this.limit) {
1184
1188
  const itemsLeft = this.limit - resultsCount;
1185
1189
  items = items.slice(0, itemsLeft);
@@ -1,6 +1,6 @@
1
1
  const http = require('http');
2
2
  const { createRequire } = require('module');
3
- const { respondJson, errorToReadable } = require('../server.utils');
3
+ const { respondJson, errorToReadable, listenOrOfferKillPort } = require('../server.utils');
4
4
  const { getWorkspace, setWorkspace, loadWorkspaceEnv, toAbsolutePath } = require('../api/workspace');
5
5
  const { getApiDirs, readCliFlag } = require('../cli');
6
6
  const { loadHandlers } = require('../server');
@@ -137,7 +137,7 @@ const startHostingPreview = (options = {}) => {
137
137
  const handlers = loadHostedRoutes(config);
138
138
  const server = createHostedPreviewServer(handlers);
139
139
 
140
- server.listen(config.port, () => {
140
+ listenOrOfferKillPort(server, config.port, () => {
141
141
  console.log(`Hosting preview running on port ${ config.port }`);
142
142
  console.log(`Workspace: ${ config.workspace }`);
143
143
  console.log('Hosted routes:');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxtware/mineral",
3
- "version": "0.1.22",
3
+ "version": "0.1.23",
4
4
  "bin": {
5
5
  "mineral": "bin/mineral.js"
6
6
  },
package/server.js CHANGED
@@ -1,6 +1,14 @@
1
1
  const fs = require('fs');
2
2
  const http = require('http');
3
- const { respondJson, errorToReadable, getRequestBody, argsFromBody, funcApi, statusCodeFromResult } = require('./server.utils');
3
+ const {
4
+ respondJson,
5
+ errorToReadable,
6
+ getRequestBody,
7
+ argsFromBody,
8
+ funcApi,
9
+ statusCodeFromResult,
10
+ listenOrOfferKillPort,
11
+ } = require('./server.utils');
4
12
  const { getWorkspace, setWorkspace, loadWorkspaceEnv, toAbsolutePath } = require('./api/workspace');
5
13
  const { getApiDirs, readCliFlag } = require('./cli');
6
14
  const { getFuncApiConfig } = require('./hosting/hosting.utils');
@@ -208,7 +216,7 @@ const startServer = (options = {}) => {
208
216
  handlers = loadHandlers(config);
209
217
  server = createServer(handlers);
210
218
 
211
- server.listen(config.port, () => logStartup({
219
+ listenOrOfferKillPort(server, config.port, () => logStartup({
212
220
  ...config,
213
221
  handlers,
214
222
  }));
package/server.utils.js CHANGED
@@ -1,4 +1,5 @@
1
- const { logDeep } = require('./api/utils');
1
+ const { execFileSync } = require('child_process');
2
+ const { logDeep, askQuestion } = require('./api/utils');
2
3
  const { HOSTED } = require('./api/constants');
3
4
  const { StringDecoder } = require('string_decoder');
4
5
 
@@ -277,6 +278,171 @@ const funcApi = (func, config = {}) => {
277
278
  };
278
279
  };
279
280
 
281
+ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
282
+
283
+ const isWatchMode = () => process.execArgv.some((arg) => arg.startsWith('--watch'));
284
+
285
+ const exitServer = (code = 0) => {
286
+ if (isWatchMode() && process.ppid && process.ppid !== 1) {
287
+ try {
288
+ process.kill(process.ppid, 'SIGTERM');
289
+ } catch (error) {
290
+ // Parent may already be gone.
291
+ }
292
+ }
293
+
294
+ process.exit(code);
295
+ };
296
+
297
+ const findPortListeners = (port) => {
298
+ try {
299
+ const stdout = execFileSync('lsof', [
300
+ '-nP',
301
+ `-iTCP:${ port }`,
302
+ '-sTCP:LISTEN',
303
+ '-Fpc',
304
+ ], {
305
+ encoding: 'utf8',
306
+ });
307
+
308
+ const listeners = [];
309
+ let pid = null;
310
+
311
+ for (const line of stdout.split('\n')) {
312
+ if (line.startsWith('p')) {
313
+ pid = Number(line.slice(1));
314
+ continue;
315
+ }
316
+
317
+ if (line.startsWith('c') && pid) {
318
+ listeners.push({
319
+ pid,
320
+ command: line.slice(1),
321
+ });
322
+ pid = null;
323
+ }
324
+ }
325
+
326
+ return listeners.filter((listener) => listener.pid && listener.pid !== process.pid);
327
+ } catch (error) {
328
+ return [];
329
+ }
330
+ };
331
+
332
+ const formatPortListeners = (listeners) => listeners
333
+ .map(({ pid, command }) => `${ command } (PID ${ pid })`)
334
+ .join(', ');
335
+
336
+ const killPortListeners = async (listeners) => {
337
+ for (const { pid } of listeners) {
338
+ try {
339
+ process.kill(pid, 'SIGTERM');
340
+ } catch (error) {
341
+ if (error.code !== 'ESRCH') {
342
+ throw error;
343
+ }
344
+ }
345
+ }
346
+
347
+ await wait(300);
348
+
349
+ for (const { pid } of listeners) {
350
+ try {
351
+ process.kill(pid, 0);
352
+ process.kill(pid, 'SIGKILL');
353
+ } catch (error) {
354
+ if (error.code !== 'ESRCH') {
355
+ throw error;
356
+ }
357
+ }
358
+ }
359
+
360
+ await wait(200);
361
+ };
362
+
363
+ const listenOrOfferKillPort = (server, port, onListening) => {
364
+ let recovering = false;
365
+
366
+ const tryListen = () => {
367
+ server.listen(port);
368
+ };
369
+
370
+ const askRecoveryQuestions = async () => {
371
+ const shouldFind = await askQuestion('Look up the process using it? ');
372
+
373
+ if (shouldFind === 'n') {
374
+ return false;
375
+ }
376
+
377
+ if (shouldFind !== '') {
378
+ console.error('not a valid input');
379
+ exitServer(1);
380
+ }
381
+
382
+ const listeners = findPortListeners(port);
383
+
384
+ if (!listeners.length) {
385
+ console.error(`Could not find a process listening on port ${ port }.`);
386
+ return false;
387
+ }
388
+
389
+ console.log('Port in use by:', formatPortListeners(listeners));
390
+
391
+ const shouldKill = await askQuestion('Kill it and retry? ');
392
+
393
+ if (shouldKill === 'n') {
394
+ return false;
395
+ }
396
+
397
+ if (shouldKill !== '') {
398
+ console.error('not a valid input');
399
+ exitServer(1);
400
+ }
401
+
402
+ await killPortListeners(listeners);
403
+ return true;
404
+ };
405
+
406
+ server.once('listening', onListening);
407
+
408
+ server.on('error', async (error) => {
409
+ if (error.code !== 'EADDRINUSE') {
410
+ console.error(error);
411
+ exitServer(1);
412
+ }
413
+
414
+ if (recovering) {
415
+ console.error(error);
416
+ exitServer(1);
417
+ }
418
+
419
+ console.error(error);
420
+
421
+ if (HOSTED) {
422
+ exitServer(1);
423
+ }
424
+
425
+ recovering = true;
426
+
427
+ try {
428
+ const shouldRetry = await askRecoveryQuestions();
429
+
430
+ if (!shouldRetry) {
431
+ exitServer(0);
432
+ }
433
+
434
+ tryListen();
435
+ } catch (recoveryError) {
436
+ console.error(recoveryError);
437
+ exitServer(1);
438
+ }
439
+ });
440
+
441
+ tryListen();
442
+
443
+ return server;
444
+ };
445
+
280
446
  module.exports = {
281
447
  respondJson,
282
448
  errorToReadable,
@@ -285,4 +451,6 @@ module.exports = {
285
451
  wrapFunction,
286
452
  statusCodeFromResult,
287
453
  funcApi,
454
+ findPortListeners,
455
+ listenOrOfferKillPort,
288
456
  };