@foxtware/mineral 0.1.26 → 0.1.27

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 CHANGED
@@ -115,10 +115,11 @@ google:
115
115
 
116
116
  tagalys:
117
117
  store:
118
- BASE_URL: ____________________________ # Server URL
119
- CLIENT_CODE: _________________________
120
- API_KEY: _____________________________
121
- STORE_ID: ____________________________
118
+ BASE_URL: ____________________________
119
+ CLIENT_CODE: _________________________ # unused but surfaced in dashboard
120
+ API_KEY: _____________________________ # unused but surfaced in dashboard
121
+ STOREFRONT_API_KEY: __________________
122
+ STORE_HANDLE: ________________________
122
123
 
123
124
  tableau:
124
125
  PAT_NAME: ______________________________
@@ -2,6 +2,6 @@
2
2
 
3
3
  - [Storefront API (v2)](https://tagalys.notion.site/Storefront-API-v2-20eacdd38c2080d58d3fd0cf6f24e435)
4
4
 
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`.
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 pass `api_key`.
6
6
 
7
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`.
@@ -0,0 +1,156 @@
1
+ // https://tagalys.notion.site/Storefront-API-v2-20eacdd38c2080d58d3fd0cf6f24e435
2
+
3
+ const { credsValidator } = require('../validators');
4
+ const { ArgsWarden, logDeep } = require('../utils');
5
+ const { resolveCreds } = require('../pipelineSteps');
6
+ const {
7
+ FetchClient,
8
+ fetchClientCommonSteps,
9
+ } = require('../utils');
10
+
11
+ // NOTE: the Storefront API v2 docs authenticate with `shop_id` (the
12
+ // Shopify myshopify.com domain) + an optional `storefront_api_key`, and
13
+ // don't mention a client_code param on this endpoint at all -- that's a
14
+ // holdover from Tagalys' older v1 API. Mapped below as best guess:
15
+ // BASE_URL -> used directly (docs say it's region-specific,
16
+ // e.g. https://api-r1.tagalys.com -- assumed already
17
+ // resolved to the right region in the creds store)
18
+ // STORE_HANDLE -> expanded to the `shop_id` myshopify domain
19
+ // STOREFRONT_API_KEY -> sent as `api_key`
20
+ // CLIENT_CODE -> not used by /v2/search per the docs; left unused below.
21
+ // Flag if your account actually needs it sent somewhere.
22
+ const DEFAULT_INCLUDE = ['products', 'total_count'];
23
+
24
+ // Recursively flattens nested params into Rails/PHP-style bracket query
25
+ // params, e.g. { filter: { color: ['red'] } } -> filter[color][]=red
26
+ // and { filter: { price: { selected_min: 100 } } } -> filter[price][selected_min]=100
27
+ const appendParams = (searchParams, key, value) => {
28
+ if (value === undefined || value === null) {
29
+ return;
30
+ }
31
+ if (Array.isArray(value)) {
32
+ value.forEach((item) => appendParams(searchParams, `${ key }[]`, item));
33
+ } else if (typeof value === 'object') {
34
+ Object.entries(value).forEach(([subKey, subValue]) => {
35
+ appendParams(searchParams, `${ key }[${ subKey }]`, subValue);
36
+ });
37
+ } else {
38
+ searchParams.append(key, value);
39
+ }
40
+ };
41
+
42
+ const useUrlAndQuery = async (state) => {
43
+ const { requestPayload, context } = state;
44
+ const { creds } = context;
45
+
46
+ const {
47
+ BASE_URL,
48
+ STORE_HANDLE,
49
+ STOREFRONT_API_KEY,
50
+ } = creds;
51
+ const shopId = `${ STORE_HANDLE }.myshopify.com`;
52
+ const storefrontApiKey = STOREFRONT_API_KEY;
53
+
54
+ const searchParams = new URLSearchParams();
55
+ searchParams.append('shop_id', shopId);
56
+ if (storefrontApiKey) {
57
+ searchParams.append('api_key', storefrontApiKey);
58
+ }
59
+ Object.entries(requestPayload.query || {}).forEach(([key, value]) => {
60
+ appendParams(searchParams, key, value);
61
+ });
62
+
63
+ return {
64
+ requestPayload: {
65
+ ...requestPayload,
66
+ url: `${ BASE_URL }${ requestPayload.url }?${ searchParams.toString() }`,
67
+ headers: {
68
+ Accept: 'application/json',
69
+ ...requestPayload.headers,
70
+ },
71
+ },
72
+ };
73
+ };
74
+
75
+ const tagalysClient = new FetchClient({
76
+ pipeline: [
77
+ resolveCreds,
78
+ useUrlAndQuery,
79
+ 'fetch',
80
+ fetchClientCommonSteps.exitEarlyOnNotOk,
81
+ ],
82
+ });
83
+
84
+ const argsWarden = new ArgsWarden([
85
+ ['credsPayload', credsValidator],
86
+ ['query'],
87
+ ]);
88
+
89
+ const tagalysSearch = async (
90
+ credsPayload,
91
+ query,
92
+ {
93
+ include = DEFAULT_INCLUDE, // e.g. ['products', 'filters', 'sort_options', 'total_count']
94
+ filter, // e.g. { color: ['red'], price: { selected_min: 100, selected_max: 200 } }
95
+ scope, // e.g. { gender: ['female'] }
96
+ sort, // e.g. 'price-asc'
97
+ page,
98
+ perPage,
99
+ } = {},
100
+ ) => {
101
+ const rejectResponse = await argsWarden.responseIfRejectingArgs({
102
+ credsPayload,
103
+ query,
104
+ });
105
+ if (rejectResponse) {
106
+ return rejectResponse;
107
+ }
108
+
109
+ const response = await tagalysClient.fetch({
110
+ context: {
111
+ credsPayload,
112
+ },
113
+ requestPayload: {
114
+ url: '/v2/search',
115
+ method: 'get',
116
+ query: {
117
+ query,
118
+ include,
119
+ ...(filter ? { filter } : {}),
120
+ ...(scope ? { scope } : {}),
121
+ ...(sort ? { sort } : {}),
122
+ ...(page ? { page } : {}),
123
+ ...(perPage ? { per_page: perPage } : {}),
124
+ },
125
+ },
126
+ });
127
+
128
+ const { ok, data, error } = response;
129
+ if (!ok) {
130
+ logDeep({ error });
131
+ return { ok: false, error };
132
+ }
133
+
134
+ // A configured redirect takes precedence over the rest of the response --
135
+ // surface it plainly rather than making callers dig for it.
136
+ if (data.redirect_url) {
137
+ return { ok: true, redirected: true, redirectUrl: data.redirect_url, data };
138
+ }
139
+
140
+ return { ok: true, data };
141
+ };
142
+
143
+ const funcApiConfig = {
144
+ argsWarden,
145
+ };
146
+
147
+ module.exports = {
148
+ tagalysSearch,
149
+ tagalysClient,
150
+ funcApiConfig,
151
+ };
152
+
153
+ /*
154
+ curl -X POST "http://localhost:8000/tagalysSearch" \
155
+ -d '{ "credsPayload": { "credsPath": "tagalys.au" }, "query": "gold" }'
156
+ */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxtware/mineral",
3
- "version": "0.1.26",
3
+ "version": "0.1.27",
4
4
  "bin": {
5
5
  "mineral": "bin/mineral.js"
6
6
  },