@unchainedshop/api 4.4.0 → 4.6.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.
- package/README.md +204 -0
- package/lib/api-index.js +3 -0
- package/lib/chat/generateImageHandler.d.ts +7 -1
- package/lib/chat/generateImageHandler.js +4 -5
- package/lib/context.js +2 -13
- package/lib/errors.d.ts +1 -0
- package/lib/errors.js +2 -1
- package/lib/events.js +0 -2
- package/lib/express/chatHandler.js +4 -4
- package/lib/express/createERCMetadataMiddleware.js +1 -1
- package/lib/express/index.js +23 -9
- package/lib/fastify/chatHandler.js +4 -4
- package/lib/fastify/ercMetadataHandler.js +1 -1
- package/lib/fastify/index.js +9 -3
- package/lib/loaders/assortmentMediaTextLoader.js +1 -1
- package/lib/loaders/assortmentMediasLoader.js +1 -1
- package/lib/loaders/assortmentTextLoader.js +1 -1
- package/lib/loaders/countryLoader.js +1 -1
- package/lib/loaders/currencyLoader.js +1 -1
- package/lib/loaders/deliveryProviderLoader.js +1 -1
- package/lib/loaders/fileLoader.js +1 -1
- package/lib/loaders/filterTextLoader.js +1 -1
- package/lib/loaders/languageLoader.js +1 -1
- package/lib/loaders/orderLoader.js +1 -1
- package/lib/loaders/paymentProviderLoader.js +1 -1
- package/lib/loaders/productBySKULoader.js +2 -4
- package/lib/loaders/productLoader.js +1 -3
- package/lib/loaders/productMediaTextLoader.js +1 -1
- package/lib/loaders/productMediasLoader.js +1 -1
- package/lib/loaders/productProxiesLoader.js +2 -10
- package/lib/loaders/productTextLoader.js +1 -1
- package/lib/loaders/productVariationByKeyLoader.js +1 -1
- package/lib/loaders/productVariationLoader.js +1 -1
- package/lib/loaders/productVariationTextLoader.js +1 -1
- package/lib/loaders/quotationLoader.js +1 -1
- package/lib/loaders/userLoader.js +1 -1
- package/lib/loaders/warehousingProviderLoader.js +1 -1
- package/lib/mcp/tools/assortment/handlers/addAssortmentFilter.d.ts +0 -1
- package/lib/mcp/tools/assortment/handlers/addAssortmentLink.d.ts +0 -1
- package/lib/mcp/tools/assortment/handlers/addAssortmentMedia.d.ts +0 -1
- package/lib/mcp/tools/assortment/handlers/addAssortmentProduct.d.ts +0 -1
- package/lib/mcp/tools/assortment/handlers/createAssortment.d.ts +0 -1
- package/lib/mcp/tools/assortment/handlers/getAssortment.d.ts +0 -1
- package/lib/mcp/tools/assortment/handlers/getAssortmentChildren.d.ts +0 -2
- package/lib/mcp/tools/assortment/handlers/getAssortmentFilters.d.ts +0 -1
- package/lib/mcp/tools/assortment/handlers/getAssortmentLinks.d.ts +0 -2
- package/lib/mcp/tools/assortment/handlers/getAssortmentMedia.d.ts +0 -1
- package/lib/mcp/tools/assortment/handlers/getAssortmentProducts.d.ts +0 -1
- package/lib/mcp/tools/assortment/handlers/index.d.ts +0 -2
- package/lib/mcp/tools/assortment/handlers/index.js +0 -2
- package/lib/mcp/tools/assortment/handlers/listAssortments.d.ts +0 -1
- package/lib/mcp/tools/assortment/handlers/reorderAssortmentLinks.d.ts +0 -1
- package/lib/mcp/tools/assortment/handlers/searchAssortmentProducts.d.ts +0 -1
- package/lib/mcp/tools/assortment/handlers/updateAssortment.d.ts +0 -1
- package/lib/mcp/tools/assortment/handlers/updateAssortmentStatus.d.ts +0 -1
- package/lib/mcp/tools/assortment/schemas.d.ts +0 -5
- package/lib/mcp/tools/assortment/schemas.js +0 -4
- package/lib/mcp/tools/order/handlers/confirmOrder.d.ts +1 -1
- package/lib/mcp/tools/order/handlers/deliverOrder.d.ts +1 -1
- package/lib/mcp/tools/order/handlers/getOrder.d.ts +1 -1
- package/lib/mcp/tools/order/handlers/getTopCustomers.d.ts +6 -6
- package/lib/mcp/tools/order/handlers/getTopCustomers.js +5 -55
- package/lib/mcp/tools/order/handlers/getTopProducts.d.ts +3 -3
- package/lib/mcp/tools/order/handlers/getTopProducts.js +4 -43
- package/lib/mcp/tools/order/handlers/getUserCart.d.ts +1 -1
- package/lib/mcp/tools/order/handlers/listOrders.d.ts +1 -1
- package/lib/mcp/tools/order/handlers/payOrder.d.ts +1 -1
- package/lib/mcp/tools/order/handlers/rejectOrder.d.ts +1 -1
- package/lib/mcp/tools/provider/handlers/listProviders.js +4 -8
- package/lib/mcp/tools/quotation/handlers/getQuotation.d.ts +2 -2
- package/lib/mcp/tools/quotation/handlers/listQuotations.d.ts +2 -2
- package/lib/mcp/tools/quotation/handlers/makeQuotationProposal.d.ts +2 -2
- package/lib/mcp/tools/quotation/handlers/rejectQuotation.d.ts +2 -2
- package/lib/mcp/tools/quotation/handlers/rejectQuotation.js +1 -1
- package/lib/mcp/tools/quotation/handlers/verifyQuotation.d.ts +2 -2
- package/lib/mcp/tools/quotation/index.js +1 -1
- package/lib/mcp/tools/quotation/schemas.d.ts +5 -5
- package/lib/mcp/tools/quotation/schemas.js +3 -3
- package/lib/mcp/tools/system/schemas.d.ts +6 -6
- package/lib/mcp/tools/system/schemas.js +14 -7
- package/lib/mcp/tools/users/handlers/addUserEmail.d.ts +1 -1
- package/lib/mcp/tools/users/handlers/createUser.d.ts +1 -1
- package/lib/mcp/tools/users/handlers/enrollUser.d.ts +1 -1
- package/lib/mcp/tools/users/handlers/getCurrentUser.d.ts +1 -1
- package/lib/mcp/tools/users/handlers/getUser.d.ts +1 -1
- package/lib/mcp/tools/users/handlers/getUserOrders.d.ts +1 -1
- package/lib/mcp/tools/users/handlers/getUserQuotations.d.ts +1 -1
- package/lib/mcp/tools/users/handlers/listUsers.d.ts +1 -1
- package/lib/mcp/tools/users/handlers/setUserTags.d.ts +1 -1
- package/lib/mcp/tools/users/handlers/setUserUsername.d.ts +1 -1
- package/lib/mcp/tools/users/handlers/updateUser.d.ts +1 -1
- package/lib/mcp/utils/getNormalizedAssortmentDetails.d.ts +0 -1
- package/lib/mcp/utils/getNormalizedOrderDetails.d.ts +1 -1
- package/lib/mcp/utils/getNormalizedQuotationDetails.d.ts +2 -2
- package/lib/mcp/utils/getNormalizedUserDetails.d.ts +1 -1
- package/lib/mongo-store.d.ts +9 -5
- package/lib/mongo-store.js +31 -25
- package/lib/resolvers/mutations/accounts/addWeb3Address.d.ts +2 -2
- package/lib/resolvers/mutations/accounts/addWeb3Address.js +1 -14
- package/lib/resolvers/mutations/accounts/addWebAuthnCredentials.d.ts +2 -2
- package/lib/resolvers/mutations/accounts/addWebAuthnCredentials.js +2 -6
- package/lib/resolvers/mutations/accounts/createUser.js +3 -1
- package/lib/resolvers/mutations/accounts/createWebAuthnCredentialCreationOptions.d.ts +1 -4
- package/lib/resolvers/mutations/accounts/createWebAuthnCredentialRequestOptions.d.ts +1 -4
- package/lib/resolvers/mutations/accounts/forgotPassword.js +4 -4
- package/lib/resolvers/mutations/accounts/loginAsGuest.js +1 -3
- package/lib/resolvers/mutations/accounts/removeWeb3Address.d.ts +1 -1
- package/lib/resolvers/mutations/accounts/removeWeb3Address.js +3 -7
- package/lib/resolvers/mutations/accounts/removeWebAuthnCredentials.d.ts +1 -1
- package/lib/resolvers/mutations/accounts/removeWebAuthnCredentials.js +1 -5
- package/lib/resolvers/mutations/accounts/verifyWeb3Address.d.ts +2 -2
- package/lib/resolvers/mutations/accounts/verifyWeb3Address.js +8 -89
- package/lib/resolvers/mutations/filters/removeFilter.d.ts +2 -2
- package/lib/resolvers/mutations/filters/removeFilter.js +3 -5
- package/lib/resolvers/mutations/index.d.ts +0 -1
- package/lib/resolvers/mutations/index.js +0 -2
- package/lib/resolvers/mutations/orders/addMultipleCartProducts.js +23 -31
- package/lib/resolvers/mutations/orders/removeCartDiscount.d.ts +2 -2
- package/lib/resolvers/mutations/orders/removeCartDiscount.js +9 -14
- package/lib/resolvers/mutations/orders/updateCart.js +2 -8
- package/lib/resolvers/mutations/quotations/rejectQuotation.js +1 -1
- package/lib/resolvers/mutations/users/removeUser.d.ts +1 -1
- package/lib/resolvers/mutations/users/removeUser.js +2 -3
- package/lib/resolvers/mutations/warehousing/exportToken.js +9 -17
- package/lib/resolvers/queries/payment/paymentProvidersCount.d.ts +3 -3
- package/lib/resolvers/queries/payment/paymentProvidersCount.js +1 -1
- package/lib/resolvers/queries/shopInfo.js +4 -36
- package/lib/resolvers/type/delivery-provider-interface.d.ts +1 -7
- package/lib/resolvers/type/delivery-provider-interface.js +10 -20
- package/lib/resolvers/type/delivery-provider-pickup-types.d.ts +1 -7
- package/lib/resolvers/type/delivery-provider-shipping-types.d.ts +1 -7
- package/lib/resolvers/type/delivery-provider-types.d.ts +1 -7
- package/lib/resolvers/type/index.d.ts +33 -54
- package/lib/resolvers/type/media-types.d.ts +1 -1
- package/lib/resolvers/type/media-types.js +7 -5
- package/lib/resolvers/type/order/order-item-types.d.ts +2 -9
- package/lib/resolvers/type/order/order-item-types.js +2 -37
- package/lib/resolvers/type/order/order-statistics-types.d.ts +10 -10
- package/lib/resolvers/type/order/order-statistics-types.js +10 -64
- package/lib/resolvers/type/payment/payment-provider-types.d.ts +1 -7
- package/lib/resolvers/type/payment/payment-provider-types.js +7 -17
- package/lib/resolvers/type/price-types.d.ts +0 -1
- package/lib/resolvers/type/product/product-bundle-types.d.ts +1 -1
- package/lib/resolvers/type/product/product-configurable-types.d.ts +1 -1
- package/lib/resolvers/type/product/product-configurable-types.js +9 -32
- package/lib/resolvers/type/product/product-plan-types.d.ts +1 -1
- package/lib/resolvers/type/product/product-simple-types.d.ts +1 -1
- package/lib/resolvers/type/product/product-tokenized-types.d.ts +1 -1
- package/lib/resolvers/type/product/product-types.d.ts +1 -1
- package/lib/resolvers/type/product/product-types.js +7 -17
- package/lib/resolvers/type/shop-types.js +2 -5
- package/lib/resolvers/type/token-types.d.ts +3 -3
- package/lib/resolvers/type/token-types.js +4 -25
- package/lib/resolvers/type/user-types.js +1 -4
- package/lib/resolvers/type/webauthn-credentials-types.d.ts +10 -1
- package/lib/roles/index.d.ts +2 -1
- package/lib/roles/index.js +7 -1
- package/lib/schema/mutation.js +1 -7
- package/lib/schema/types/assortment.js +0 -1
- package/lib/schema/types/order/order.js +2 -2
- package/lib/schema/types/quotation.js +2 -2
- package/lib/utils/getConfiguredTags.d.ts +1 -0
- package/lib/utils/getConfiguredTags.js +9 -0
- package/package.json +14 -23
- package/lib/mcp/tools/assortment/handlers/setAssortmentBase.d.ts +0 -28
- package/lib/mcp/tools/assortment/handlers/setAssortmentBase.js +0 -11
- package/lib/resolvers/mutations/assortments/setBaseAssortment.d.ts +0 -4
- package/lib/resolvers/mutations/assortments/setBaseAssortment.js +0 -11
package/README.md
CHANGED
|
@@ -149,6 +149,210 @@ import { createMCPServer } from '@unchainedshop/api/mcp';
|
|
|
149
149
|
const mcpServer = createMCPServer(unchainedCore);
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
+
## Security
|
|
153
|
+
|
|
154
|
+
The API layer implements comprehensive security controls.
|
|
155
|
+
|
|
156
|
+
### Access Control
|
|
157
|
+
|
|
158
|
+
- **128+ permission actions** covering all API operations
|
|
159
|
+
- **Role-Based Access Control (RBAC)** with built-in and custom roles
|
|
160
|
+
- **ACL enforcement** on all GraphQL mutations
|
|
161
|
+
- **Ownership validation** ensuring users can only access their resources
|
|
162
|
+
|
|
163
|
+
### Session Security
|
|
164
|
+
|
|
165
|
+
| Variable | Purpose | Default |
|
|
166
|
+
|----------|---------|---------|
|
|
167
|
+
| `UNCHAINED_TOKEN_SECRET` | Session encryption (min 32 chars) | Required |
|
|
168
|
+
| `UNCHAINED_COOKIE_NAME` | Cookie name | `unchained_token` |
|
|
169
|
+
| `UNCHAINED_COOKIE_SAMESITE` | SameSite attribute | `none` |
|
|
170
|
+
| `UNCHAINED_COOKIE_INSECURE` | Disable secure flag | `false` |
|
|
171
|
+
|
|
172
|
+
Cookies are `httpOnly` and `secure` by default.
|
|
173
|
+
|
|
174
|
+
### Error Handling
|
|
175
|
+
|
|
176
|
+
Errors are designed to prevent information leakage:
|
|
177
|
+
- Generic authentication error messages
|
|
178
|
+
- No distinction between "invalid" vs "expired" tokens
|
|
179
|
+
- Permission errors don't reveal action details
|
|
180
|
+
|
|
181
|
+
### CORS Configuration
|
|
182
|
+
|
|
183
|
+
CORS behavior depends on `NODE_ENV`:
|
|
184
|
+
|
|
185
|
+
| Environment | Default Behavior |
|
|
186
|
+
|-------------|------------------|
|
|
187
|
+
| `development` | Permissive CORS (reflects any origin) + trust proxy headers |
|
|
188
|
+
| `production` | No CORS headers (reverse proxy should handle it) |
|
|
189
|
+
|
|
190
|
+
#### Environment Variable
|
|
191
|
+
|
|
192
|
+
| Variable | Purpose | Default |
|
|
193
|
+
|----------|---------|---------|
|
|
194
|
+
| `UNCHAINED_CORS_ORIGINS` | Allowed CORS origins | Auto (see above) |
|
|
195
|
+
|
|
196
|
+
#### Programmatic Configuration
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
// Auto behavior (recommended) - permissive in dev, none in prod
|
|
200
|
+
connect(app, { graphqlHandler, db, unchainedAPI });
|
|
201
|
+
|
|
202
|
+
// Explicit whitelist (production)
|
|
203
|
+
connect(app, { graphqlHandler, db, unchainedAPI }, {
|
|
204
|
+
corsOrigins: "https://shop.example.com,https://admin.example.com",
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Force permissive (not recommended in production)
|
|
208
|
+
connect(app, { graphqlHandler, db, unchainedAPI }, {
|
|
209
|
+
corsOrigins: true,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Disable CORS entirely
|
|
213
|
+
connect(app, { graphqlHandler, db, unchainedAPI }, {
|
|
214
|
+
corsOrigins: false,
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Deployment Scenarios
|
|
219
|
+
|
|
220
|
+
#### Scenario 1: Direct TLS (No Reverse Proxy)
|
|
221
|
+
|
|
222
|
+
For simple deployments where the Node.js server handles TLS directly:
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
import express from 'express';
|
|
226
|
+
import https from 'https';
|
|
227
|
+
import fs from 'fs';
|
|
228
|
+
import { connect } from '@unchainedshop/api/express';
|
|
229
|
+
|
|
230
|
+
const app = express();
|
|
231
|
+
|
|
232
|
+
// Configure CORS for your frontend origins
|
|
233
|
+
connect(app, { graphqlHandler, db, unchainedAPI }, {
|
|
234
|
+
corsOrigins: "https://shop.example.com,https://admin.example.com",
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Create HTTPS server with your certificates
|
|
238
|
+
https.createServer({
|
|
239
|
+
key: fs.readFileSync('/path/to/privkey.pem'),
|
|
240
|
+
cert: fs.readFileSync('/path/to/fullchain.pem'),
|
|
241
|
+
}, app).listen(443);
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Environment:
|
|
245
|
+
```bash
|
|
246
|
+
NODE_ENV=production
|
|
247
|
+
UNCHAINED_CORS_ORIGINS=https://shop.example.com,https://admin.example.com
|
|
248
|
+
UNCHAINED_TOKEN_SECRET=your-32-char-secret-here
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
#### Scenario 2: Behind Reverse Proxy (Recommended)
|
|
252
|
+
|
|
253
|
+
For production deployments behind nginx, Caddy, or a cloud load balancer:
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
import express from 'express';
|
|
257
|
+
import { connect } from '@unchainedshop/api/express';
|
|
258
|
+
|
|
259
|
+
const app = express();
|
|
260
|
+
|
|
261
|
+
// Trust the reverse proxy for client IP headers
|
|
262
|
+
app.set('trust proxy', 1);
|
|
263
|
+
|
|
264
|
+
// Let the proxy handle CORS, or configure here
|
|
265
|
+
connect(app, { graphqlHandler, db, unchainedAPI }, {
|
|
266
|
+
corsOrigins: "https://shop.example.com,https://admin.example.com",
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
app.listen(4010); // Internal port, not exposed
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**Nginx configuration:**
|
|
273
|
+
```nginx
|
|
274
|
+
server {
|
|
275
|
+
listen 443 ssl http2;
|
|
276
|
+
server_name api.example.com;
|
|
277
|
+
|
|
278
|
+
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
|
|
279
|
+
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
|
|
280
|
+
|
|
281
|
+
location / {
|
|
282
|
+
# Pass client IP to the app
|
|
283
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
284
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
285
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
286
|
+
proxy_set_header Host $host;
|
|
287
|
+
|
|
288
|
+
proxy_pass http://localhost:4010;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
**Caddy configuration:**
|
|
294
|
+
```caddyfile
|
|
295
|
+
api.example.com {
|
|
296
|
+
reverse_proxy localhost:4010
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
#### Scenario 3: Proxy Handles CORS
|
|
301
|
+
|
|
302
|
+
For complex multi-origin setups, let the reverse proxy manage CORS:
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
// Don't set corsOrigins - let proxy handle it
|
|
306
|
+
app.set('trust proxy', 1);
|
|
307
|
+
connect(app, { graphqlHandler, db, unchainedAPI });
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
**Nginx with CORS:**
|
|
311
|
+
```nginx
|
|
312
|
+
location / {
|
|
313
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
314
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
315
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
316
|
+
|
|
317
|
+
# CORS whitelist
|
|
318
|
+
set $cors "";
|
|
319
|
+
if ($http_origin ~* "^https://(shop|admin)\.example\.com$") {
|
|
320
|
+
set $cors $http_origin;
|
|
321
|
+
}
|
|
322
|
+
add_header 'Access-Control-Allow-Origin' $cors always;
|
|
323
|
+
add_header 'Access-Control-Allow-Credentials' 'true' always;
|
|
324
|
+
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
|
|
325
|
+
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' always;
|
|
326
|
+
|
|
327
|
+
if ($request_method = 'OPTIONS') { return 204; }
|
|
328
|
+
|
|
329
|
+
proxy_pass http://localhost:4010;
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Trust Proxy
|
|
334
|
+
|
|
335
|
+
In development mode (`NODE_ENV=development`), trust proxy is automatically enabled.
|
|
336
|
+
|
|
337
|
+
In production, configure it on your Express/Fastify app before calling `connect()`:
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
// Express
|
|
341
|
+
app.set('trust proxy', 1); // Trust first hop
|
|
342
|
+
app.set('trust proxy', 'loopback'); // Trust loopback addresses
|
|
343
|
+
app.set('trust proxy', '10.0.0.0/8'); // Trust specific CIDR
|
|
344
|
+
|
|
345
|
+
// Fastify
|
|
346
|
+
const fastify = Fastify({ trustProxy: true });
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
**SECURITY**: Only enable trust proxy if your reverse proxy:
|
|
350
|
+
1. Strips incoming `X-Real-IP` and `X-Forwarded-For` headers from clients
|
|
351
|
+
2. Sets `X-Real-IP` to the actual client IP
|
|
352
|
+
3. Is the only way to reach your API server
|
|
353
|
+
|
|
354
|
+
See [SECURITY.md](../../SECURITY.md) for complete security documentation including FIPS 140-3 mode.
|
|
355
|
+
|
|
152
356
|
## License
|
|
153
357
|
|
|
154
358
|
EUPL-1.2
|
package/lib/api-index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { registerEvents } from '@unchainedshop/events';
|
|
1
2
|
import createGraphQLServer, {} from "./createGraphQLServer.js";
|
|
2
3
|
import { createContextResolver, setCurrentContextResolver, getCurrentContextResolver, } from "./context.js";
|
|
4
|
+
import { API_EVENTS } from "./events.js";
|
|
3
5
|
export * from "./events.js";
|
|
4
6
|
export * from "./context.js";
|
|
5
7
|
export * from "./locale-context.js";
|
|
@@ -12,6 +14,7 @@ import { buildDefaultTypeDefs } from "./schema/index.js";
|
|
|
12
14
|
import resolvers from "./resolvers/index.js";
|
|
13
15
|
import { actions } from "./roles/index.js";
|
|
14
16
|
export const startAPIServer = async (options) => {
|
|
17
|
+
registerEvents(Object.keys(API_EVENTS));
|
|
15
18
|
const { unchainedAPI, context: customContext, roles, adminUiConfig = {}, ...serverOptions } = options;
|
|
16
19
|
const contextResolver = createContextResolver(unchainedAPI, {
|
|
17
20
|
roles,
|
|
@@ -2,5 +2,11 @@ import type * as aiTypes from 'ai';
|
|
|
2
2
|
declare const generateImageHandler: (req: any) => ({ model, uploadUrl, }: {
|
|
3
3
|
model: aiTypes.ImageModel;
|
|
4
4
|
uploadUrl?: string;
|
|
5
|
-
}) => aiTypes.Tool<
|
|
5
|
+
}) => aiTypes.Tool<{
|
|
6
|
+
prompt: string;
|
|
7
|
+
size?: "512x512" | "768x768" | "1024x1024" | "512x896" | "640x1120" | "768x1344" | "1024x1792" | "896x512" | "1120x640" | "1344x768" | "1792x1024" | undefined;
|
|
8
|
+
}, string | {
|
|
9
|
+
imageUrl: any;
|
|
10
|
+
prompt: string;
|
|
11
|
+
}>;
|
|
6
12
|
export default generateImageHandler;
|
|
@@ -2,12 +2,12 @@ import { z } from 'zod';
|
|
|
2
2
|
import mime from 'mime/lite';
|
|
3
3
|
import { createLogger } from '@unchainedshop/logger';
|
|
4
4
|
const logger = createLogger('unchained:api:mcp');
|
|
5
|
-
let
|
|
5
|
+
let generateImage;
|
|
6
6
|
let tool;
|
|
7
7
|
try {
|
|
8
8
|
const aiTools = await import('ai');
|
|
9
9
|
tool = aiTools.tool;
|
|
10
|
-
|
|
10
|
+
generateImage = aiTools.generateImage;
|
|
11
11
|
}
|
|
12
12
|
catch {
|
|
13
13
|
}
|
|
@@ -33,11 +33,10 @@ const inputSchema = z.object({
|
|
|
33
33
|
const generateImageHandler = (req) => ({ model, uploadUrl = `${process.env.ROOT_URL}/temp-upload`, }) => {
|
|
34
34
|
return tool({
|
|
35
35
|
description: 'Generate an image based on the prompt',
|
|
36
|
-
inputSchema
|
|
37
|
-
name: 'generate_image',
|
|
36
|
+
inputSchema,
|
|
38
37
|
execute: async ({ prompt, size = '1024x1024' }) => {
|
|
39
38
|
try {
|
|
40
|
-
const { image } = await
|
|
39
|
+
const { image } = await generateImage({
|
|
41
40
|
model,
|
|
42
41
|
prompt,
|
|
43
42
|
size: size,
|
package/lib/context.js
CHANGED
|
@@ -1,22 +1,11 @@
|
|
|
1
|
-
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
-
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
-
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
-
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
-
});
|
|
6
|
-
}
|
|
7
|
-
return path;
|
|
8
|
-
};
|
|
9
1
|
import instantiateLoaders, {} from "./loaders/index.js";
|
|
10
2
|
import { getLocaleContext } from "./locale-context.js";
|
|
3
|
+
const { npm_package_version, UNCHAINED_API_VERSION } = process.env;
|
|
11
4
|
let context;
|
|
12
5
|
export const getCurrentContextResolver = () => context;
|
|
13
6
|
export const setCurrentContextResolver = (newContext) => {
|
|
14
7
|
context = newContext;
|
|
15
8
|
};
|
|
16
|
-
const { default: packageJson } = await import(__rewriteRelativeImportExtension(`${import.meta.dirname}/../package.json`), {
|
|
17
|
-
with: { type: 'json' },
|
|
18
|
-
});
|
|
19
|
-
const { UNCHAINED_API_VERSION = packageJson?.version || process.env.npm_package_version || 'n/a' } = process.env;
|
|
20
9
|
export const createContextResolver = (unchainedAPI, unchainedConfig) => async ({ getHeader, setHeader, remoteAddress, remotePort, userId, impersonatorId, accessToken, login, logout, }) => {
|
|
21
10
|
const abstractHttpServerContext = { remoteAddress, remotePort, getHeader, setHeader };
|
|
22
11
|
const loaders = instantiateLoaders(unchainedAPI);
|
|
@@ -43,6 +32,6 @@ export const createContextResolver = (unchainedAPI, unchainedConfig) => async ({
|
|
|
43
32
|
...userContext,
|
|
44
33
|
...abstractHttpServerContext,
|
|
45
34
|
loaders,
|
|
46
|
-
version: UNCHAINED_API_VERSION,
|
|
35
|
+
version: UNCHAINED_API_VERSION || npm_package_version || 'n/a',
|
|
47
36
|
};
|
|
48
37
|
};
|
package/lib/errors.d.ts
CHANGED
|
@@ -66,6 +66,7 @@ export declare const UsernameAlreadyExistsError: any;
|
|
|
66
66
|
export declare const UsernameOrEmailRequiredError: any;
|
|
67
67
|
export declare const PasswordInvalidError: any;
|
|
68
68
|
export declare const PasswordOrWebAuthnPublicKeyRequiredError: any;
|
|
69
|
+
export declare const WebAuthnVerificationFailedError: any;
|
|
69
70
|
export declare const InvalidCredentialsError: any;
|
|
70
71
|
export declare const InvalidResetTokenError: any;
|
|
71
72
|
export declare const InvalidEmailVerificationTokenError: any;
|
package/lib/errors.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { GraphQLError } from 'graphql';
|
|
2
2
|
export const createError = (code, message) => class extends GraphQLError {
|
|
3
|
-
constructor({ message: explicitMessage, ...data }) {
|
|
3
|
+
constructor({ message: explicitMessage, ...data } = {}) {
|
|
4
4
|
super(explicitMessage || message, {
|
|
5
5
|
extensions: {
|
|
6
6
|
code,
|
|
@@ -76,6 +76,7 @@ export const UsernameAlreadyExistsError = createError('UsernameAlreadyExists', '
|
|
|
76
76
|
export const UsernameOrEmailRequiredError = createError('UsernameOrEmailRequired', 'No username or email is provided.');
|
|
77
77
|
export const PasswordInvalidError = createError('PasswordInvalidError', 'The provided password is invalid, maybe too insecure');
|
|
78
78
|
export const PasswordOrWebAuthnPublicKeyRequiredError = createError('PasswordOrWebAuthnPublicKeyRequired', 'A password or a WebAuthn public key is required');
|
|
79
|
+
export const WebAuthnVerificationFailedError = createError('WebAuthnVerificationFailedError', 'WebAuthn credential verification failed');
|
|
79
80
|
export const InvalidCredentialsError = createError('InvalidCredentials', 'Invalid credentials provided');
|
|
80
81
|
export const InvalidResetTokenError = createError('InvalidResetTokenError', 'Token invalid or expired');
|
|
81
82
|
export const InvalidEmailVerificationTokenError = createError('InvalidEmailVerificationTokenError', 'Token invalid or expired');
|
package/lib/events.js
CHANGED
|
@@ -21,7 +21,7 @@ try {
|
|
|
21
21
|
convertToModelMessages = aiTools.convertToModelMessages;
|
|
22
22
|
stepCountIs = aiTools.stepCountIs;
|
|
23
23
|
streamText = aiTools.streamText;
|
|
24
|
-
createMCPClient = mcpTools.
|
|
24
|
+
createMCPClient = mcpTools.createMCPClient;
|
|
25
25
|
}
|
|
26
26
|
catch {
|
|
27
27
|
logger.warn(`optional peer npm packages 'ai', '@ai-sdk/mcp' and '@modelcontextprotocol/sdk' not installed, chat will not work`);
|
|
@@ -76,7 +76,7 @@ const setupMCPChatHandler = (chatConfiguration) => {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
catch (e) {
|
|
79
|
-
logger.error(`Failed to read resource ${resource.uri}
|
|
79
|
+
logger.error(`Failed to read resource ${resource.uri}: ${e.message}`);
|
|
80
80
|
}
|
|
81
81
|
return null;
|
|
82
82
|
}));
|
|
@@ -85,7 +85,7 @@ const setupMCPChatHandler = (chatConfiguration) => {
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
catch (e) {
|
|
88
|
-
logger.error(
|
|
88
|
+
logger.error(`Failed to fetch MCP resources: ${e.message}`);
|
|
89
89
|
}
|
|
90
90
|
const tools = {
|
|
91
91
|
...defaultUnchainedTools,
|
|
@@ -111,7 +111,7 @@ const setupMCPChatHandler = (chatConfiguration) => {
|
|
|
111
111
|
anthropic: { cacheControl: { type: 'ephemeral' } },
|
|
112
112
|
},
|
|
113
113
|
};
|
|
114
|
-
const normalizedMessages = convertToModelMessages(messages, { tools: cacheControlledTools });
|
|
114
|
+
const normalizedMessages = await convertToModelMessages(messages, { tools: cacheControlledTools });
|
|
115
115
|
if (normalizedMessages.length > 0) {
|
|
116
116
|
const lastIndex = normalizedMessages.length - 1;
|
|
117
117
|
normalizedMessages[lastIndex] = {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createLogger } from '@unchainedshop/logger';
|
|
2
2
|
const logger = createLogger('unchained:erc-metadata');
|
|
3
|
-
const { ROOT_URL = 'http://localhost:4010' } = process.env;
|
|
4
3
|
const methodWrongHandler = (res) => {
|
|
5
4
|
logger.error('Method not supported, return 404');
|
|
6
5
|
res.status(404).end();
|
|
@@ -11,6 +10,7 @@ const ercMetadataMiddleware = async (req, res) => {
|
|
|
11
10
|
if (req.method !== 'GET') {
|
|
12
11
|
return methodWrongHandler(res);
|
|
13
12
|
}
|
|
13
|
+
const { ROOT_URL = 'http://localhost:4010' } = process.env;
|
|
14
14
|
const { services, modules, loaders, locale } = req.unchainedContext;
|
|
15
15
|
const url = new URL(req.url, ROOT_URL);
|
|
16
16
|
const pathname = url.pathname;
|
package/lib/express/index.js
CHANGED
|
@@ -64,19 +64,33 @@ const addContext = async function middlewareWithContext(req, res, next) {
|
|
|
64
64
|
const { user } = req;
|
|
65
65
|
if (!user)
|
|
66
66
|
return false;
|
|
67
|
+
const currentSessionId = req.sessionID;
|
|
68
|
+
const targetSessionId = sessionId || currentSessionId;
|
|
67
69
|
const tokenObject = {
|
|
68
|
-
_id:
|
|
70
|
+
_id: targetSessionId,
|
|
69
71
|
userId: user._id,
|
|
70
72
|
};
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
if (sessionId && sessionId !== currentSessionId) {
|
|
74
|
+
await new Promise((resolve, reject) => {
|
|
75
|
+
req.sessionStore.destroy(sessionId, (error) => {
|
|
76
|
+
if (error) {
|
|
77
|
+
return reject(error);
|
|
78
|
+
}
|
|
79
|
+
return resolve();
|
|
80
|
+
});
|
|
78
81
|
});
|
|
79
|
-
}
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
await new Promise((resolve, reject) => {
|
|
85
|
+
req.logout((error, result) => {
|
|
86
|
+
if (error) {
|
|
87
|
+
return reject(error);
|
|
88
|
+
}
|
|
89
|
+
req.session.impersonatorId = null;
|
|
90
|
+
return resolve(result);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
80
94
|
await emit(API_EVENTS.API_LOGOUT, tokenObject);
|
|
81
95
|
return true;
|
|
82
96
|
};
|
|
@@ -20,7 +20,7 @@ try {
|
|
|
20
20
|
convertToModelMessages = aiTools.convertToModelMessages;
|
|
21
21
|
stepCountIs = aiTools.stepCountIs;
|
|
22
22
|
streamText = aiTools.streamText;
|
|
23
|
-
createMCPClient = mcpTools.
|
|
23
|
+
createMCPClient = mcpTools.createMCPClient;
|
|
24
24
|
}
|
|
25
25
|
catch {
|
|
26
26
|
logger.warn(`optional peer npm packages 'ai' and '@ai-sdk/mcp' not installed, chat will not work`);
|
|
@@ -73,7 +73,7 @@ const setupMCPChatHandler = (chatConfiguration) => {
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
catch (e) {
|
|
76
|
-
logger.error(`Failed to read resource ${resource.uri}
|
|
76
|
+
logger.error(`Failed to read resource ${resource.uri}: ${e.message}`);
|
|
77
77
|
}
|
|
78
78
|
return null;
|
|
79
79
|
}));
|
|
@@ -82,7 +82,7 @@ const setupMCPChatHandler = (chatConfiguration) => {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
catch (e) {
|
|
85
|
-
logger.error(
|
|
85
|
+
logger.error(`Failed to fetch MCP resources: ${e.message}`);
|
|
86
86
|
}
|
|
87
87
|
const tools = {
|
|
88
88
|
...defaultUnchainedTools,
|
|
@@ -107,7 +107,7 @@ const setupMCPChatHandler = (chatConfiguration) => {
|
|
|
107
107
|
anthropic: { cacheControl: { type: 'ephemeral' } },
|
|
108
108
|
},
|
|
109
109
|
};
|
|
110
|
-
const normalizedMessages = convertToModelMessages(messages, { tools: cacheControlledTools });
|
|
110
|
+
const normalizedMessages = await convertToModelMessages(messages, { tools: cacheControlledTools });
|
|
111
111
|
if (normalizedMessages.length > 0) {
|
|
112
112
|
const lastIndex = normalizedMessages.length - 1;
|
|
113
113
|
normalizedMessages[lastIndex] = {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { createLogger } from '@unchainedshop/logger';
|
|
2
2
|
import { ProductContractStandard } from '@unchainedshop/core-products';
|
|
3
3
|
const logger = createLogger('unchained:api:erc-metadata');
|
|
4
|
-
const { ROOT_URL = 'http://localhost:4010' } = process.env;
|
|
5
4
|
const notFoundHandler = (res) => {
|
|
6
5
|
logger.error('Method not supported, return 404');
|
|
7
6
|
res.status(404);
|
|
@@ -9,6 +8,7 @@ const notFoundHandler = (res) => {
|
|
|
9
8
|
};
|
|
10
9
|
const ercMetadataHandler = async (req, res) => {
|
|
11
10
|
try {
|
|
11
|
+
const { ROOT_URL = 'http://localhost:4010' } = process.env;
|
|
12
12
|
const { services, modules, locale, loaders } = req.unchainedContext;
|
|
13
13
|
const url = new URL(req.url, ROOT_URL);
|
|
14
14
|
if (!url.pathname.toLowerCase().endsWith('.json'))
|
package/lib/fastify/index.js
CHANGED
|
@@ -189,9 +189,15 @@ export const connect = (fastify, { graphqlHandler, db, unchainedAPI, }, { allowR
|
|
|
189
189
|
};
|
|
190
190
|
const fallbackLandingPageHandler = (request, reply) => {
|
|
191
191
|
if (request.raw.method === 'GET') {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
192
|
+
try {
|
|
193
|
+
const staticURL = import.meta.resolve('@unchainedshop/api/index.html');
|
|
194
|
+
const staticPath = new URL(staticURL).pathname;
|
|
195
|
+
return reply.type('text/html').send(readFileSync(staticPath));
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
const fallbackPath = new URL('../../index.html', import.meta.url).pathname;
|
|
199
|
+
return reply.type('text/html').send(readFileSync(fallbackPath));
|
|
200
|
+
}
|
|
195
201
|
}
|
|
196
202
|
else {
|
|
197
203
|
return reply.status(404).send();
|
|
@@ -3,7 +3,7 @@ import buildTextMap from "./buildTextMap.js";
|
|
|
3
3
|
import buildLocaleMap from "./buildLocaleMap.js";
|
|
4
4
|
export default (unchainedAPI) => new DataLoader(async (queries) => {
|
|
5
5
|
const assortmentMediaIds = [...new Set(queries.map((q) => q.assortmentMediaId).filter(Boolean))];
|
|
6
|
-
const texts = await unchainedAPI.modules.assortments.media.texts.findMediaTexts({
|
|
6
|
+
const texts = await unchainedAPI.modules.assortments.media.texts.findMediaTexts({ assortmentMediaIds }, {
|
|
7
7
|
sort: {
|
|
8
8
|
assortmentMediaId: 1,
|
|
9
9
|
},
|
|
@@ -2,7 +2,7 @@ import DataLoader from 'dataloader';
|
|
|
2
2
|
export default (unchainedAPI) => new DataLoader(async (queries) => {
|
|
3
3
|
const assortmentIds = [...new Set(queries.map((q) => q.assortmentId).filter(Boolean))];
|
|
4
4
|
const assortmentMediaItems = await unchainedAPI.modules.assortments.media.findAssortmentMedias({
|
|
5
|
-
|
|
5
|
+
assortmentIds,
|
|
6
6
|
});
|
|
7
7
|
const assortmentMediaMap = {};
|
|
8
8
|
for (const assortmentMedia of assortmentMediaItems) {
|
|
@@ -3,7 +3,7 @@ import buildTextMap from "./buildTextMap.js";
|
|
|
3
3
|
import buildLocaleMap from "./buildLocaleMap.js";
|
|
4
4
|
export default (unchainedAPI) => new DataLoader(async (queries) => {
|
|
5
5
|
const assortmentIds = [...new Set(queries.map((q) => q.assortmentId).filter(Boolean))];
|
|
6
|
-
const texts = await unchainedAPI.modules.assortments.texts.findTexts({
|
|
6
|
+
const texts = await unchainedAPI.modules.assortments.texts.findTexts({ assortmentIds }, {
|
|
7
7
|
sort: {
|
|
8
8
|
assortmentId: 1,
|
|
9
9
|
},
|
|
@@ -2,7 +2,7 @@ import DataLoader from 'dataloader';
|
|
|
2
2
|
export default (unchainedAPI) => new DataLoader(async (queries) => {
|
|
3
3
|
const isoCodes = [...new Set(queries.map((q) => q.isoCode).filter(Boolean))];
|
|
4
4
|
const countries = await unchainedAPI.modules.countries.findCountries({
|
|
5
|
-
|
|
5
|
+
isoCodes,
|
|
6
6
|
includeInactive: true,
|
|
7
7
|
});
|
|
8
8
|
const countryMap = {};
|
|
@@ -2,7 +2,7 @@ import DataLoader from 'dataloader';
|
|
|
2
2
|
export default (unchainedAPI) => new DataLoader(async (queries) => {
|
|
3
3
|
const isoCodes = [...new Set(queries.map((q) => q.isoCode).filter(Boolean))];
|
|
4
4
|
const currencies = await unchainedAPI.modules.currencies.findCurrencies({
|
|
5
|
-
|
|
5
|
+
isoCodes,
|
|
6
6
|
includeInactive: true,
|
|
7
7
|
});
|
|
8
8
|
const currencyMap = {};
|
|
@@ -2,7 +2,7 @@ import DataLoader from 'dataloader';
|
|
|
2
2
|
export default (unchainedAPI) => new DataLoader(async (queries) => {
|
|
3
3
|
const deliveryProviderIds = [...new Set(queries.map((q) => q.deliveryProviderId).filter(Boolean))];
|
|
4
4
|
const deliveryProviders = await unchainedAPI.modules.delivery.findProviders({
|
|
5
|
-
|
|
5
|
+
deliveryProviderIds,
|
|
6
6
|
includeDeleted: true,
|
|
7
7
|
});
|
|
8
8
|
const deliveryProviderMap = {};
|
|
@@ -2,7 +2,7 @@ import DataLoader from 'dataloader';
|
|
|
2
2
|
export default (unchainedAPI) => new DataLoader(async (queries) => {
|
|
3
3
|
const fileIds = [...new Set(queries.map((q) => q.fileId).filter(Boolean))];
|
|
4
4
|
const files = await unchainedAPI.modules.files.findFiles({
|
|
5
|
-
|
|
5
|
+
fileIds,
|
|
6
6
|
});
|
|
7
7
|
const fileMap = {};
|
|
8
8
|
for (const file of files) {
|
|
@@ -3,7 +3,7 @@ import buildLocaleMap from "./buildLocaleMap.js";
|
|
|
3
3
|
import buildTextMap from "./buildTextMap.js";
|
|
4
4
|
export default (unchainedAPI) => new DataLoader(async (queries) => {
|
|
5
5
|
const filterIds = [...new Set(queries.map((q) => q.filterId).filter(Boolean))];
|
|
6
|
-
const texts = await unchainedAPI.modules.filters.texts.findTexts({
|
|
6
|
+
const texts = await unchainedAPI.modules.filters.texts.findTexts({ filterIds }, {
|
|
7
7
|
sort: {
|
|
8
8
|
filterId: 1,
|
|
9
9
|
},
|
|
@@ -2,7 +2,7 @@ import DataLoader from 'dataloader';
|
|
|
2
2
|
export default (unchainedAPI) => new DataLoader(async (queries) => {
|
|
3
3
|
const isoCodes = [...new Set(queries.map((q) => q.isoCode).filter(Boolean))];
|
|
4
4
|
const languages = await unchainedAPI.modules.languages.findLanguages({
|
|
5
|
-
|
|
5
|
+
isoCodes,
|
|
6
6
|
includeInactive: true,
|
|
7
7
|
});
|
|
8
8
|
const languageMap = {};
|
|
@@ -3,7 +3,7 @@ export default (unchainedAPI) => new DataLoader(async (queries) => {
|
|
|
3
3
|
const orderIds = [...new Set(queries.map((q) => q.orderId).filter(Boolean))];
|
|
4
4
|
const orders = await unchainedAPI.modules.orders.findOrders({
|
|
5
5
|
includeCarts: true,
|
|
6
|
-
|
|
6
|
+
orderIds,
|
|
7
7
|
});
|
|
8
8
|
const orderMap = {};
|
|
9
9
|
for (const order of orders) {
|
|
@@ -2,7 +2,7 @@ import DataLoader from 'dataloader';
|
|
|
2
2
|
export default (unchainedAPI) => new DataLoader(async (queries) => {
|
|
3
3
|
const paymentProviderIds = [...new Set(queries.map((q) => q.paymentProviderId).filter(Boolean))];
|
|
4
4
|
const paymentProviders = await unchainedAPI.modules.payment.paymentProviders.findProviders({
|
|
5
|
-
|
|
5
|
+
paymentProviderIds,
|
|
6
6
|
includeDeleted: true,
|
|
7
7
|
});
|
|
8
8
|
const paymentProviderMap = {};
|
|
@@ -2,10 +2,8 @@ import DataLoader from 'dataloader';
|
|
|
2
2
|
export default (unchainedAPI) => new DataLoader(async (queries) => {
|
|
3
3
|
const skus = [...new Set(queries.map((q) => q.sku).filter(Boolean))];
|
|
4
4
|
const products = await unchainedAPI.modules.products.findProducts({
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
status: { $exists: true },
|
|
8
|
-
},
|
|
5
|
+
skus,
|
|
6
|
+
includeDeleted: true,
|
|
9
7
|
});
|
|
10
8
|
const productMap = {};
|
|
11
9
|
for (const product of products) {
|
|
@@ -3,9 +3,7 @@ export default (unchainedAPI) => new DataLoader(async (queries) => {
|
|
|
3
3
|
const productIds = [...new Set(queries.map((q) => q.productId).filter(Boolean))];
|
|
4
4
|
const products = await unchainedAPI.modules.products.findProducts({
|
|
5
5
|
productIds,
|
|
6
|
-
|
|
7
|
-
status: { $exists: true },
|
|
8
|
-
},
|
|
6
|
+
includeDeleted: true,
|
|
9
7
|
});
|
|
10
8
|
const productMap = {};
|
|
11
9
|
for (const product of products) {
|
|
@@ -3,7 +3,7 @@ import buildLocaleMap from "./buildLocaleMap.js";
|
|
|
3
3
|
import buildTextMap from "./buildTextMap.js";
|
|
4
4
|
export default (unchainedAPI) => new DataLoader(async (queries) => {
|
|
5
5
|
const productMediaIds = [...new Set(queries.map((q) => q.productMediaId).filter(Boolean))];
|
|
6
|
-
const texts = await unchainedAPI.modules.products.media.texts.findMediaTexts({
|
|
6
|
+
const texts = await unchainedAPI.modules.products.media.texts.findMediaTexts({ productMediaIds }, {
|
|
7
7
|
sort: {
|
|
8
8
|
productMediaId: 1,
|
|
9
9
|
},
|