@unchainedshop/api 5.0.0-alpha.3 → 5.0.0-alpha.5

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 (91) hide show
  1. package/README.md +2 -2
  2. package/lib/acl.js +4 -0
  3. package/lib/adminUiPlugins.d.ts +36 -1
  4. package/lib/adminUiPlugins.js +188 -26
  5. package/lib/auth.d.ts +0 -8
  6. package/lib/auth.js +0 -20
  7. package/lib/errors.d.ts +2 -0
  8. package/lib/errors.js +2 -0
  9. package/lib/events.d.ts +1 -0
  10. package/lib/events.js +1 -0
  11. package/lib/express/chatHandler.js +12 -1
  12. package/lib/express/index.d.ts +2 -4
  13. package/lib/express/index.js +63 -24
  14. package/lib/fastify/chatHandler.js +10 -1
  15. package/lib/fastify/index.d.ts +4 -4
  16. package/lib/fastify/index.js +83 -30
  17. package/lib/loaders/assortmentLinksLoader.js +24 -22
  18. package/lib/loaders/assortmentMediaTextLoader.js +1 -0
  19. package/lib/loaders/assortmentProductLoader.js +7 -4
  20. package/lib/loaders/assortmentTextLoader.js +1 -0
  21. package/lib/loaders/filterTextLoader.js +1 -0
  22. package/lib/loaders/productMediaTextLoader.js +1 -0
  23. package/lib/loaders/productTextLoader.js +1 -0
  24. package/lib/loaders/productVariationTextLoader.js +1 -0
  25. package/lib/mcp/tools/assortment/handlers/getAssortmentFilters.js +1 -1
  26. package/lib/mcp/tools/assortment/handlers/getAssortmentProducts.js +1 -1
  27. package/lib/mcp/tools/system/handlers/allocateWork.js +2 -2
  28. package/lib/mcp/tools/users/handlers/getUserPaymentCredentials.js +1 -0
  29. package/lib/mcp/tools/users/index.js +1 -1
  30. package/lib/mcp/utils/getNormalizedAssortmentDetails.js +2 -2
  31. package/lib/mcp/utils/getNormalizedUserDetails.js +1 -0
  32. package/lib/middleware/createAuthMiddleware.js +4 -39
  33. package/lib/resolvers/mutations/accounts/loginWithPassword.js +16 -2
  34. package/lib/resolvers/mutations/accounts/logoutAllSessions.d.ts +3 -1
  35. package/lib/resolvers/mutations/accounts/logoutAllSessions.js +10 -4
  36. package/lib/resolvers/mutations/bulk/bulkAssignProductsToAssortment.d.ts +10 -0
  37. package/lib/resolvers/mutations/bulk/bulkAssignProductsToAssortment.js +18 -0
  38. package/lib/resolvers/mutations/bulk/bulkOperation.d.ts +12 -0
  39. package/lib/resolvers/mutations/bulk/bulkOperation.js +14 -0
  40. package/lib/resolvers/mutations/bulk/bulkRemoveAssortments.d.ts +9 -0
  41. package/lib/resolvers/mutations/bulk/bulkRemoveAssortments.js +7 -0
  42. package/lib/resolvers/mutations/bulk/bulkRemoveFilters.d.ts +9 -0
  43. package/lib/resolvers/mutations/bulk/bulkRemoveFilters.js +9 -0
  44. package/lib/resolvers/mutations/bulk/bulkRemoveProducts.d.ts +9 -0
  45. package/lib/resolvers/mutations/bulk/bulkRemoveProducts.js +9 -0
  46. package/lib/resolvers/mutations/bulk/bulkRemoveUsers.d.ts +9 -0
  47. package/lib/resolvers/mutations/bulk/bulkRemoveUsers.js +7 -0
  48. package/lib/resolvers/mutations/bulk/bulkSetAssortmentActive.d.ts +10 -0
  49. package/lib/resolvers/mutations/bulk/bulkSetAssortmentActive.js +9 -0
  50. package/lib/resolvers/mutations/bulk/bulkSetFilterActive.d.ts +10 -0
  51. package/lib/resolvers/mutations/bulk/bulkSetFilterActive.js +10 -0
  52. package/lib/resolvers/mutations/bulk/bulkSetProductStatus.d.ts +10 -0
  53. package/lib/resolvers/mutations/bulk/bulkSetProductStatus.js +17 -0
  54. package/lib/resolvers/mutations/bulk/bulkSetUserRoles.d.ts +10 -0
  55. package/lib/resolvers/mutations/bulk/bulkSetUserRoles.js +14 -0
  56. package/lib/resolvers/mutations/bulk/bulkUpdateAssortmentTags.d.ts +11 -0
  57. package/lib/resolvers/mutations/bulk/bulkUpdateAssortmentTags.js +10 -0
  58. package/lib/resolvers/mutations/bulk/bulkUpdateProductTags.d.ts +11 -0
  59. package/lib/resolvers/mutations/bulk/bulkUpdateProductTags.js +7 -0
  60. package/lib/resolvers/mutations/bulk/bulkUpdateUserTags.d.ts +11 -0
  61. package/lib/resolvers/mutations/bulk/bulkUpdateUserTags.js +7 -0
  62. package/lib/resolvers/mutations/index.d.ts +12 -0
  63. package/lib/resolvers/mutations/index.js +25 -1
  64. package/lib/resolvers/mutations/orders/checkoutCart.js +1 -1
  65. package/lib/resolvers/mutations/users/removeUser.js +9 -2
  66. package/lib/resolvers/mutations/users/setRoles.js +9 -2
  67. package/lib/resolvers/mutations/worker/allocateWork.d.ts +1 -1
  68. package/lib/resolvers/mutations/worker/allocateWork.js +4 -3
  69. package/lib/resolvers/queries/index.d.ts +1 -0
  70. package/lib/resolvers/queries/index.js +2 -0
  71. package/lib/resolvers/queries/search/globalSearch.d.ts +36 -0
  72. package/lib/resolvers/queries/search/globalSearch.js +200 -0
  73. package/lib/resolvers/type/assortment/assortment-types.js +2 -2
  74. package/lib/resolvers/type/global-search-result-types.d.ts +3 -0
  75. package/lib/resolvers/type/global-search-result-types.js +15 -0
  76. package/lib/resolvers/type/index.d.ts +3 -0
  77. package/lib/resolvers/type/index.js +2 -0
  78. package/lib/resolvers/type/user-types.d.ts +1 -0
  79. package/lib/resolvers/type/user-types.js +36 -4
  80. package/lib/roles/all.js +12 -0
  81. package/lib/roles/index.js +12 -0
  82. package/lib/roles/loggedIn.js +1 -1
  83. package/lib/schema/inputTypes.js +5 -0
  84. package/lib/schema/mutation.js +74 -1
  85. package/lib/schema/query.js +15 -0
  86. package/lib/schema/types/common.js +7 -0
  87. package/lib/schema/types/search.js +36 -0
  88. package/lib/schema/types/user.js +4 -0
  89. package/package.json +31 -26
  90. package/lib/adminUiTheme.d.ts +0 -25
  91. package/lib/adminUiTheme.js +0 -34
@@ -1,39 +1,59 @@
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
+ };
1
9
  import e from 'express';
2
10
  import cookieParser from 'cookie-parser';
3
11
  import { pluginRegistry } from '@unchainedshop/core';
4
12
  import { createHash } from 'node:crypto';
5
- import { existsSync } from 'node:fs';
6
- import { join } from 'node:path';
13
+ import { runWithAuditContext } from '@unchainedshop/events';
7
14
  import { getCurrentContextResolver } from "../context.js";
8
15
  import { createAuthContext } from "../middleware/createAuthMiddleware.js";
9
16
  import createMCPMiddleware from "./createMCPMiddleware.js";
10
17
  import { connectChat } from "./chatHandler.js";
11
18
  import { mountRoutes } from "./mountRoutes.js";
12
19
  import { createBackchannelLogoutRoute } from "../handlers/createBackchannelLogoutHandler.js";
13
- import { generateThemeCSS } from '@unchainedshop/admin-ui/theme';
14
- import { preparePluginAssets, resolveAdminUIPath } from "../adminUiPlugins.js";
20
+ import { preparePluginAssets, prepareAdminUIHTML, resolveAdminUIHTML, injectAdminUIImportMap, buildImportMapTag, resolveAdminUIPath, } from "../adminUiPlugins.js";
15
21
  export const adminUIRouter = (enabled = true, theme, plugins = []) => {
16
22
  const router = e.Router();
17
23
  const adminUIPath = resolveAdminUIPath();
18
24
  if (!adminUIPath)
19
25
  return router;
20
26
  if (enabled) {
21
- const themeCSS = generateThemeCSS(theme);
22
- const themeHash = createHash('sha256').update(themeCSS).digest('hex').slice(0, 8);
23
- const themeEtag = `"${themeHash}"`;
24
- router.get('/admin-ui-theme.css', (req, res) => {
25
- if (req.headers['if-none-match'] === themeEtag) {
27
+ let themePromise;
28
+ const resolveThemeCSS = async () => {
29
+ let css = '/* default theme */';
30
+ if (theme) {
31
+ try {
32
+ const themeModuleUrl = import.meta.resolve('@unchainedshop/admin-ui/theme');
33
+ const { generateThemeCSS } = await import(__rewriteRelativeImportExtension(themeModuleUrl));
34
+ css = generateThemeCSS(theme);
35
+ }
36
+ catch {
37
+ console.warn("npm dependency @unchainedshop/admin-ui is not installed, can't apply admin-ui theme");
38
+ }
39
+ }
40
+ const etag = `"${createHash('sha256').update(css).digest('hex').slice(0, 8)}"`;
41
+ return { css, etag };
42
+ };
43
+ router.get('/admin-ui-theme.css', async (req, res) => {
44
+ const { css, etag } = await (themePromise ||= resolveThemeCSS());
45
+ if (req.headers['if-none-match'] === etag) {
26
46
  return res.status(304).end();
27
47
  }
28
48
  res
29
49
  .set('Cache-Control', 'public, max-age=0, must-revalidate')
30
- .set('ETag', themeEtag)
50
+ .set('ETag', etag)
31
51
  .type('text/css')
32
- .send(themeCSS);
52
+ .send(css);
33
53
  });
34
54
  const log = { info: console.info, warn: console.warn };
35
55
  const devMode = process.env.NODE_ENV !== 'production';
36
- const { routes: pluginRoutes } = preparePluginAssets(plugins, log, { devMode });
56
+ const { routes: pluginRoutes, importMapTag, importMapJSON, } = preparePluginAssets(plugins, log, { devMode });
37
57
  for (const [path, asset] of pluginRoutes) {
38
58
  router.get(path, (req, res) => {
39
59
  if (asset.etag) {
@@ -51,16 +71,23 @@ export const adminUIRouter = (enabled = true, theme, plugins = []) => {
51
71
  .send(body);
52
72
  });
53
73
  }
54
- router.use(e.static(adminUIPath));
55
- const extHtmlPath = join(adminUIPath, 'ext', '[[...slug]]', 'index.html');
56
- const hasExtHtml = existsSync(extHtmlPath);
57
- router.get(/(.*)/, (req, res) => {
58
- const urlPath = req.path.replace(/\/+$/, '');
59
- if (hasExtHtml && (urlPath === '/ext' || urlPath.startsWith('/ext/'))) {
60
- return res.sendFile(extHtmlPath);
61
- }
62
- return res.sendFile(join(adminUIPath, 'index.html'));
63
- });
74
+ router.use(e.static(adminUIPath, importMapTag ? { index: false } : undefined));
75
+ if (importMapJSON) {
76
+ const preparedHTML = prepareAdminUIHTML(adminUIPath);
77
+ router.get(/(.*)/, (req, res) => {
78
+ if (devMode)
79
+ res.set('Cache-Control', 'no-cache');
80
+ const nonce = res.locals?.cspNonce;
81
+ const tag = nonce ? buildImportMapTag(importMapJSON, nonce) : importMapTag;
82
+ return res.type('text/html').send(injectAdminUIImportMap(preparedHTML, req.path, tag));
83
+ });
84
+ }
85
+ else {
86
+ const preparedHTML = prepareAdminUIHTML(adminUIPath);
87
+ router.get(/(.*)/, (req, res) => {
88
+ return res.type('text/html').send(resolveAdminUIHTML(preparedHTML, req.path));
89
+ });
90
+ }
64
91
  }
65
92
  return router;
66
93
  };
@@ -100,7 +127,7 @@ const createAddContextMiddleware = (authConfig, trustProxy = false) => async fun
100
127
  };
101
128
  const authContext = await createAuthContext(authContextParams, authConfig);
102
129
  const context = getCurrentContextResolver();
103
- req.unchainedContext = await context({
130
+ const unchainedContext = await context({
104
131
  setHeader,
105
132
  getHeader,
106
133
  remoteAddress,
@@ -112,6 +139,12 @@ const createAddContextMiddleware = (authConfig, trustProxy = false) => async fun
112
139
  impersonatorId: authContext.impersonatorId,
113
140
  tokenVersion: authContext.tokenVersion,
114
141
  }, req, res);
142
+ req.unchainedContext = unchainedContext;
143
+ req._auditContext = {
144
+ userId: unchainedContext.userId,
145
+ userName: unchainedContext.user?.username || unchainedContext.user?.emails?.[0]?.address,
146
+ remoteAddress,
147
+ };
115
148
  next();
116
149
  }
117
150
  catch (error) {
@@ -140,7 +173,13 @@ export const connect = async (expressApp, { graphqlHandler, unchainedAPI, }, { a
140
173
  }
141
174
  expressApp.use(cookieParser());
142
175
  expressApp.use(createAddContextMiddleware(authConfig, trustProxy || allowRemoteToLocalhostSecureCookies));
143
- expressApp.use(graphqlHandler.graphqlEndpoint, graphqlHandler.handle);
176
+ expressApp.use(graphqlHandler.graphqlEndpoint, (req, res) => {
177
+ const auditCtx = req._auditContext;
178
+ if (auditCtx) {
179
+ return runWithAuditContext(auditCtx, () => graphqlHandler.handle(req, res));
180
+ }
181
+ return graphqlHandler.handle(req, res);
182
+ });
144
183
  expressApp.use(MCP_API_PATH, e.json({ limit: '10mb' }));
145
184
  expressApp.use(MCP_API_PATH, createMCPMiddleware);
146
185
  if (chat) {
@@ -39,6 +39,14 @@ const setupMCPChatHandler = (chatConfiguration) => {
39
39
  });
40
40
  return res.status(200).send();
41
41
  }
42
+ const unchainedContext = req.unchainedContext;
43
+ const user = unchainedContext?.user;
44
+ if (!user) {
45
+ return res.status(401).send({ error: 'unauthorized' });
46
+ }
47
+ if (!(user.roles || []).includes('admin')) {
48
+ return res.status(403).send({ error: 'forbidden', message: 'Chat requires admin privileges' });
49
+ }
42
50
  let client;
43
51
  const lifecycle = createChatRequestLifecycle(res.raw, configuredAbortSignal);
44
52
  try {
@@ -48,13 +56,14 @@ const setupMCPChatHandler = (chatConfiguration) => {
48
56
  url: unchainedMCPUrl,
49
57
  headers: {
50
58
  Cookie: req.headers.cookie || '',
59
+ ...(req.headers.authorization ? { Authorization: req.headers.authorization } : {}),
51
60
  },
52
61
  },
53
62
  initializationOptions: { signal: lifecycle.signal },
54
63
  });
55
64
  lifecycle.setClientClose(() => client.close());
56
65
  const defaultUnchainedTools = await client.tools();
57
- const resourceContext = await buildChatResourceContext(req.unchainedContext);
66
+ const resourceContext = await buildChatResourceContext(unchainedContext);
58
67
  const tools = {
59
68
  ...defaultUnchainedTools,
60
69
  ...additionalTools,
@@ -4,15 +4,15 @@ import type { UnchainedCore } from '@unchainedshop/core';
4
4
  import type { FastifyBaseLogger, FastifyInstance, FastifyPluginAsync } from 'fastify';
5
5
  import { connectChat } from './chatHandler.ts';
6
6
  import type { ChatConfiguration } from '../chat/utils.ts';
7
- import { type AdminUIThemeConfig } from '@unchainedshop/admin-ui/theme';
8
- import { type AdminUIPluginConfig } from '../adminUiPlugins.ts';
9
- export type { AdminUIPluginConfig, AdminUIPluginEntityConfig, AdminUIPluginPageConfig, AdminUIPluginTabConfig, AdminUIPluginWidgetConfig, AdminUIPluginSlotConfig, } from '../adminUiPlugins.ts';
10
- export type { AdminUIThemeTokens, AdminUIThemeConfig } from '@unchainedshop/admin-ui/theme';
7
+ import { type AdminUIPluginConfig, type AdminUIThemeConfig } from '../adminUiPlugins.ts';
8
+ export type { AdminUIPluginConfig, AdminUIPluginEntityConfig, AdminUIPluginPageConfig, AdminUIPluginTabConfig, AdminUIPluginWidgetConfig, AdminUIPluginSlotConfig, AdminUIThemeTokens, AdminUIThemeConfig, } from '../adminUiPlugins.ts';
11
9
  export interface AdminUIRouterOptions {
12
10
  prefix?: string;
13
11
  enabled?: boolean;
14
12
  theme?: AdminUIThemeConfig;
15
13
  plugins?: AdminUIPluginConfig[];
14
+ importMapTag?: string | null;
15
+ importMapJSON?: string | null;
16
16
  }
17
17
  export declare const unchainedLogger: (prefix: string) => FastifyBaseLogger;
18
18
  export declare const connect: (fastify: FastifyInstance, { graphqlHandler, unchainedAPI, }: {
@@ -1,3 +1,12 @@
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
+ import { runWithAuditContext } from '@unchainedshop/events';
1
10
  import { getCurrentContextResolver } from "../context.js";
2
11
  import { createAuthContext } from "../middleware/createAuthMiddleware.js";
3
12
  import { pluginRegistry } from '@unchainedshop/core';
@@ -7,11 +16,9 @@ import mcpHandler from "./mcpHandler.js";
7
16
  import { connectChat } from "./chatHandler.js";
8
17
  import { mountRoutes } from "./mountRoutes.js";
9
18
  import { createHash } from 'node:crypto';
10
- import { existsSync, readFileSync } from 'node:fs';
11
- import { join } from 'node:path';
19
+ import { readFileSync } from 'node:fs';
12
20
  import { createBackchannelLogoutRoute } from "../handlers/createBackchannelLogoutHandler.js";
13
- import { generateThemeCSS } from '@unchainedshop/admin-ui/theme';
14
- import { preparePluginAssets, resolveAdminUIPath } from "../adminUiPlugins.js";
21
+ import { preparePluginAssets, prepareAdminUIHTML, resolveAdminUIHTML, injectAdminUIImportMap, buildImportMapTag, resolveAdminUIPath, } from "../adminUiPlugins.js";
15
22
  const resolveUserRemoteAddress = (req, trustProxy = false) => {
16
23
  let remoteAddress;
17
24
  if (trustProxy) {
@@ -47,7 +54,7 @@ const createMiddlewareHook = (authConfig, trustProxy = false) => async function
47
54
  };
48
55
  const authContext = await createAuthContext(authContextParams, authConfig);
49
56
  const context = getCurrentContextResolver();
50
- req.unchainedContext = await context({
57
+ const unchainedContext = await context({
51
58
  setHeader,
52
59
  getHeader,
53
60
  remoteAddress,
@@ -59,6 +66,12 @@ const createMiddlewareHook = (authConfig, trustProxy = false) => async function
59
66
  impersonatorId: authContext.impersonatorId,
60
67
  tokenVersion: authContext.tokenVersion,
61
68
  }, req, reply);
69
+ req.unchainedContext = unchainedContext;
70
+ req._auditContext = {
71
+ userId: unchainedContext.userId,
72
+ userName: unchainedContext.user?.username || unchainedContext.user?.emails?.[0]?.address,
73
+ remoteAddress,
74
+ };
62
75
  };
63
76
  export const unchainedLogger = (prefix) => {
64
77
  const logger = createLogger(prefix);
@@ -104,10 +117,15 @@ export const connect = async (fastify, { graphqlHandler, unchainedAPI, }, { allo
104
117
  url: graphqlHandler.graphqlEndpoint,
105
118
  method: ['GET', 'POST', 'OPTIONS'],
106
119
  handler: async (req, reply) => {
107
- return graphqlHandler.handleNodeRequestAndResponse(req, reply, {
120
+ const auditCtx = req._auditContext;
121
+ const handle = () => graphqlHandler.handleNodeRequestAndResponse(req, reply, {
108
122
  req,
109
123
  reply,
110
124
  });
125
+ if (auditCtx) {
126
+ return runWithAuditContext(auditCtx, handle);
127
+ }
128
+ return handle();
111
129
  },
112
130
  });
113
131
  fastify.route({
@@ -126,21 +144,35 @@ export const connect = async (fastify, { graphqlHandler, unchainedAPI, }, { allo
126
144
  if (adminUI) {
127
145
  const adminUIOptions = typeof adminUI === 'object' ? adminUI : undefined;
128
146
  const adminUIPlugins = adminUIOptions?.plugins || [];
129
- const themeCSS = generateThemeCSS(adminUIOptions?.theme);
130
- const themeHash = createHash('sha256').update(themeCSS).digest('hex').slice(0, 8);
131
- const themeEtag = `"${themeHash}"`;
147
+ let themePromise;
148
+ const resolveThemeCSS = async () => {
149
+ let css = '/* default theme */';
150
+ if (adminUIOptions?.theme) {
151
+ try {
152
+ const themeModuleUrl = import.meta.resolve('@unchainedshop/admin-ui/theme');
153
+ const { generateThemeCSS } = await import(__rewriteRelativeImportExtension(themeModuleUrl));
154
+ css = generateThemeCSS(adminUIOptions.theme);
155
+ }
156
+ catch {
157
+ fastify.log.warn("npm dependency @unchainedshop/admin-ui is not installed, can't apply admin-ui theme");
158
+ }
159
+ }
160
+ const etag = `"${createHash('sha256').update(css).digest('hex').slice(0, 8)}"`;
161
+ return { css, etag };
162
+ };
132
163
  fastify.get('/admin-ui-theme.css', async (request, reply) => {
133
- if (request.headers['if-none-match'] === themeEtag) {
164
+ const { css, etag } = await (themePromise ||= resolveThemeCSS());
165
+ if (request.headers['if-none-match'] === etag) {
134
166
  return reply.code(304).send();
135
167
  }
136
168
  return reply
137
169
  .header('Cache-Control', 'public, max-age=0, must-revalidate')
138
- .header('ETag', themeEtag)
170
+ .header('ETag', etag)
139
171
  .type('text/css')
140
- .send(themeCSS);
172
+ .send(css);
141
173
  });
142
174
  const devMode = process.env.NODE_ENV !== 'production';
143
- const { routes: pluginRoutes } = preparePluginAssets(adminUIPlugins, fastify.log, {
175
+ const { routes: pluginRoutes, importMapTag, importMapJSON, } = preparePluginAssets(adminUIPlugins, fastify.log, {
144
176
  devMode,
145
177
  });
146
178
  for (const [path, asset] of pluginRoutes) {
@@ -165,6 +197,8 @@ export const connect = async (fastify, { graphqlHandler, unchainedAPI, }, { allo
165
197
  enabled: true,
166
198
  prefix: adminUIOptions?.prefix || '/',
167
199
  plugins: adminUIPlugins,
200
+ importMapTag,
201
+ importMapJSON,
168
202
  });
169
203
  }
170
204
  };
@@ -197,24 +231,43 @@ export const adminUIRouter = async (fastify, opts) => {
197
231
  if (fastifyStatic) {
198
232
  const adminUIPath = resolveAdminUIPath();
199
233
  if (adminUIPath) {
200
- const indexHtml = readFileSync(join(adminUIPath, 'index.html'), 'utf-8');
201
- const extHtmlPath = join(adminUIPath, 'ext', '[[...slug]]', 'index.html');
202
- const extHtml = existsSync(extHtmlPath) ? readFileSync(extHtmlPath, 'utf-8') : null;
203
- await fastify.register(fastifyStatic, {
204
- root: adminUIPath,
205
- prefix: opts.prefix || '/',
206
- wildcard: false,
207
- });
208
- fastify.setNotFoundHandler(async (request, reply) => {
209
- if (request.method === 'GET' && !request.url.includes('.')) {
210
- const urlPath = request.url.split('?')[0].replace(/\/+$/, '');
211
- if (extHtml && (urlPath === '/ext' || urlPath.startsWith('/ext/'))) {
212
- return reply.type('text/html').send(extHtml);
234
+ if (opts.importMapJSON) {
235
+ const preparedHTML = prepareAdminUIHTML(adminUIPath);
236
+ await fastify.register(fastifyStatic, {
237
+ root: adminUIPath,
238
+ prefix: opts.prefix || '/',
239
+ wildcard: false,
240
+ index: false,
241
+ });
242
+ fastify.setNotFoundHandler(async (request, reply) => {
243
+ if (request.method === 'GET' && !request.url.includes('.')) {
244
+ if (process.env.NODE_ENV !== 'production')
245
+ reply.header('Cache-Control', 'no-cache');
246
+ const nonce = reply.cspNonce?.script;
247
+ const tag = nonce ? buildImportMapTag(opts.importMapJSON, nonce) : opts.importMapTag;
248
+ return reply
249
+ .type('text/html')
250
+ .send(injectAdminUIImportMap(preparedHTML, request.url, tag, opts.prefix));
213
251
  }
214
- return reply.type('text/html').send(indexHtml);
215
- }
216
- return reply.code(404).send({ error: 'Not Found' });
217
- });
252
+ return reply.code(404).send({ error: 'Not Found' });
253
+ });
254
+ }
255
+ else {
256
+ const preparedHTML = prepareAdminUIHTML(adminUIPath);
257
+ await fastify.register(fastifyStatic, {
258
+ root: adminUIPath,
259
+ prefix: opts.prefix || '/',
260
+ wildcard: false,
261
+ });
262
+ fastify.setNotFoundHandler(async (request, reply) => {
263
+ if (request.method === 'GET' && !request.url.includes('.')) {
264
+ return reply
265
+ .type('text/html')
266
+ .send(resolveAdminUIHTML(preparedHTML, request.url, opts.prefix));
267
+ }
268
+ return reply.code(404).send({ error: 'Not Found' });
269
+ });
270
+ }
218
271
  return;
219
272
  }
220
273
  }
@@ -1,38 +1,40 @@
1
1
  import DataLoader from 'dataloader';
2
2
  export default (unchainedAPI) => new DataLoader(async (queries) => {
3
- const parentAssortmentIds = queries.flatMap((q) => q.parentAssortmentId).filter(Boolean);
4
- const childAssortmentIds = queries.flatMap((q) => q.childAssortmentId).filter(Boolean);
5
- const assortmentIds = queries.flatMap((q) => q.assortmentId).filter(Boolean);
3
+ const parentAssortmentIds = [
4
+ ...new Set(queries.flatMap((q) => q.parentAssortmentId).filter(Boolean)),
5
+ ];
6
+ const childAssortmentIds = [
7
+ ...new Set(queries.flatMap((q) => q.childAssortmentId).filter(Boolean)),
8
+ ];
9
+ const assortmentIds = [
10
+ ...new Set(queries.flatMap((q) => q.assortmentId).filter(Boolean)),
11
+ ];
6
12
  const allLinks = await unchainedAPI.modules.assortments.links.findLinks({
7
- assortmentIds: [...new Set([...parentAssortmentIds, ...childAssortmentIds, ...assortmentIds])],
13
+ ...(parentAssortmentIds.length ? { parentAssortmentIds } : {}),
14
+ ...(childAssortmentIds.length ? { childAssortmentIds } : {}),
15
+ ...(assortmentIds.length ? { assortmentIds } : {}),
8
16
  });
9
- const parentAssortmentLinkMap = {};
10
- const childAssortmentLinkMap = {};
17
+ const parentAssortmentLinkMap = new Map();
18
+ const childAssortmentLinkMap = new Map();
11
19
  for (const link of allLinks) {
12
- if (!parentAssortmentLinkMap[link.parentAssortmentId]) {
13
- parentAssortmentLinkMap[link.parentAssortmentId] = [link];
14
- }
15
- else {
16
- parentAssortmentLinkMap[link.parentAssortmentId].push(link);
17
- }
18
- if (!childAssortmentLinkMap[link.childAssortmentId]) {
19
- childAssortmentLinkMap[link.childAssortmentId] = [link];
20
- }
21
- else {
22
- childAssortmentLinkMap[link.childAssortmentId].push(link);
23
- }
20
+ const parentLinks = parentAssortmentLinkMap.get(link.parentAssortmentId) || [];
21
+ parentLinks.push(link);
22
+ parentAssortmentLinkMap.set(link.parentAssortmentId, parentLinks);
23
+ const childLinks = childAssortmentLinkMap.get(link.childAssortmentId) || [];
24
+ childLinks.push(link);
25
+ childAssortmentLinkMap.set(link.childAssortmentId, childLinks);
24
26
  }
25
27
  return queries.map((q) => {
26
28
  if (q.parentAssortmentId) {
27
- return parentAssortmentLinkMap[q.parentAssortmentId] || [];
29
+ return parentAssortmentLinkMap.get(q.parentAssortmentId) || [];
28
30
  }
29
31
  else if (q.childAssortmentId) {
30
- return childAssortmentLinkMap[q.childAssortmentId] || [];
32
+ return childAssortmentLinkMap.get(q.childAssortmentId) || [];
31
33
  }
32
34
  if (q.assortmentId) {
33
35
  return [
34
- ...(parentAssortmentLinkMap[q.assortmentId] || []),
35
- ...(childAssortmentLinkMap[q.assortmentId] || []),
36
+ ...(parentAssortmentLinkMap.get(q.assortmentId) || []),
37
+ ...(childAssortmentLinkMap.get(q.assortmentId) || []),
36
38
  ];
37
39
  }
38
40
  return [];
@@ -6,6 +6,7 @@ export default (unchainedAPI) => new DataLoader(async (queries) => {
6
6
  const texts = await unchainedAPI.modules.assortments.media.texts.findMediaTexts({ assortmentMediaIds }, {
7
7
  sort: {
8
8
  assortmentMediaId: 1,
9
+ _id: 1,
9
10
  },
10
11
  });
11
12
  const localeMap = buildLocaleMap(queries, texts);
@@ -1,13 +1,16 @@
1
1
  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
+ const productIds = [...new Set(queries.map((q) => q.productId).filter(Boolean))];
4
5
  const assortmentProducts = await unchainedAPI.modules.assortments.products.findAssortmentProducts({
5
6
  assortmentIds,
7
+ productIds,
6
8
  });
7
- const assortmentProductMap = {};
9
+ const assortmentProductMap = new Map();
8
10
  for (const assortmentProduct of assortmentProducts) {
9
- assortmentProductMap[assortmentProduct.assortmentId + assortmentProduct.productId] =
10
- assortmentProduct;
11
+ const productsById = assortmentProductMap.get(assortmentProduct.assortmentId) || new Map();
12
+ productsById.set(assortmentProduct.productId, assortmentProduct);
13
+ assortmentProductMap.set(assortmentProduct.assortmentId, productsById);
11
14
  }
12
- return queries.map((q) => assortmentProductMap[q.assortmentId + q.productId]);
15
+ return queries.map((q) => assortmentProductMap.get(q.assortmentId)?.get(q.productId));
13
16
  });
@@ -6,6 +6,7 @@ export default (unchainedAPI) => new DataLoader(async (queries) => {
6
6
  const texts = await unchainedAPI.modules.assortments.texts.findTexts({ assortmentIds }, {
7
7
  sort: {
8
8
  assortmentId: 1,
9
+ _id: 1,
9
10
  },
10
11
  });
11
12
  const localeMap = buildLocaleMap(queries, texts);
@@ -6,6 +6,7 @@ export default (unchainedAPI) => new DataLoader(async (queries) => {
6
6
  const texts = await unchainedAPI.modules.filters.texts.findTexts({ filterIds }, {
7
7
  sort: {
8
8
  filterId: 1,
9
+ _id: 1,
9
10
  },
10
11
  });
11
12
  const localeMap = buildLocaleMap(queries, texts);
@@ -6,6 +6,7 @@ export default (unchainedAPI) => new DataLoader(async (queries) => {
6
6
  const texts = await unchainedAPI.modules.products.media.texts.findMediaTexts({ productMediaIds }, {
7
7
  sort: {
8
8
  productMediaId: 1,
9
+ _id: 1,
9
10
  },
10
11
  });
11
12
  const localeMap = buildLocaleMap(queries, texts);
@@ -6,6 +6,7 @@ export default (unchainedAPI) => new DataLoader(async (queries) => {
6
6
  const texts = await unchainedAPI.modules.products.texts.findTexts({ productIds }, {
7
7
  sort: {
8
8
  productId: 1,
9
+ _id: 1,
9
10
  },
10
11
  });
11
12
  const localeMap = buildLocaleMap(queries, texts);
@@ -8,6 +8,7 @@ export default (unchainedAPI) => new DataLoader(async (queries) => {
8
8
  }, {
9
9
  sort: {
10
10
  productVariationId: 1,
11
+ _id: 1,
11
12
  },
12
13
  });
13
14
  const localeMap = buildLocaleMap(queries, texts);
@@ -7,7 +7,7 @@ export default async function getAssortmentFilters(context, params) {
7
7
  const assortment = await getNormalizedAssortmentDetails({ assortmentId }, context);
8
8
  if (!assortment)
9
9
  throw new AssortmentNotFoundError({ assortmentId });
10
- const filters = await modules.assortments.filters.findFilters({ assortmentId }, { sort: { sortKey: 1 } });
10
+ const filters = await modules.assortments.filters.findFilters({ assortmentId }, { sort: { sortKey: 1, _id: 1 } });
11
11
  const filters_normalized = await Promise.all(filters?.map(async ({ filterId, ...rest }) => ({
12
12
  ...(await getNormalizedFilterDetails(filterId, context)),
13
13
  ...rest,
@@ -7,7 +7,7 @@ export default async function getAssortmentProducts(context, params) {
7
7
  const assortment = await getNormalizedAssortmentDetails({ assortmentId }, context);
8
8
  if (!assortment)
9
9
  throw new AssortmentNotFoundError({ assortmentId });
10
- const assortmentProducts = await modules.assortments.products.findAssortmentProducts({ assortmentId }, { sort: { sortKey: 1 } });
10
+ const assortmentProducts = await modules.assortments.products.findAssortmentProducts({ assortmentId }, { sort: { sortKey: 1, _id: 1 } });
11
11
  const products = await Promise.all(assortmentProducts?.map(async ({ productId, ...rest }) => ({
12
12
  ...(await getNormalizedProductDetails(productId, context)),
13
13
  ...rest,
@@ -1,8 +1,8 @@
1
1
  import { buildObfuscatedFieldsFilter } from '@unchainedshop/utils';
2
+ import { WorkerDirector } from '@unchainedshop/core';
2
3
  const allocateWork = async (context, options) => {
3
- const { modules } = context;
4
4
  const removePrivateFieldsFromWork = buildObfuscatedFieldsFilter(context.options.worker?.blacklistedVariables);
5
- const work = await modules.worker.allocateWork({
5
+ const work = await WorkerDirector.allocateWork(context, {
6
6
  types: options.types || [],
7
7
  worker: options.worker || '',
8
8
  });
@@ -4,6 +4,7 @@ export default async function getUserPaymentCredentials(context, params) {
4
4
  const credentials = await modules.payment.paymentCredentials.findPaymentCredentials({ userId }, {
5
5
  sort: {
6
6
  created: -1,
7
+ _id: -1,
7
8
  },
8
9
  });
9
10
  return { credentials };
@@ -1,7 +1,7 @@
1
1
  import { usersManagement, UsersManagementSchema } from "./usersManagement.js";
2
2
  export const registerUsersTools = (server, context) => {
3
3
  server.registerTool('users_management', {
4
- description: 'Comprehensive user management system for Unchained e-commerce platform. Core operations: LIST (paginated user listing with filters for guests, email verification, last login dates), COUNT (user count with same filters), GET (retrieve user by ID), CREATE (new user with optional profile), UPDATE (profile/metadata updates), REMOVE (soft delete with optional review cleanup), ENROLL (create user + send enrollment email). User administration: SET_ROLES (assign admin/editor roles), SET_TAGS (categorization tags), SET_PASSWORD (admin password reset), SET_USERNAME (username changes). Email management: ADD_EMAIL/REMOVE_EMAIL (multi-email support), SEND_ENROLLMENT_EMAIL (trigger enrollment), SEND_VERIFICATION_EMAIL (email verification). Data access: GET_ORDERS (user order history with cart inclusion), GET_ENROLLMENTS (subscription enrollments), GET_QUOTATIONS (price quotes), GET_BOOKMARKS (saved products), GET_CART (active user cart), GET_PAYMENT_CREDENTIALS (stored payment methods), GET_AVATAR (profile image), GET_REVIEWS/GET_REVIEWS_COUNT (product reviews), REMOVE_PRODUCT_REVIEWS (bulk review cleanup), GET_CURRENT_USER (current session logged in user). Supports pagination, sorting, filtering, date ranges, and proper validation for all operations.',
4
+ description: 'User management for Unchained e-commerce. Actions: LIST, COUNT, GET, CREATE, UPDATE, REMOVE, ENROLL (create + send enrollment email), SET_TAGS, SET_USERNAME, ADD_EMAIL, REMOVE_EMAIL, SEND_ENROLLMENT_EMAIL, SEND_VERIFICATION_EMAIL, GET_ORDERS, GET_ENROLLMENTS, GET_QUOTATIONS, GET_BOOKMARKS, GET_PAYMENT_CREDENTIALS, GET_AVATAR, GET_REVIEWS, GET_REVIEWS_COUNT, REMOVE_PRODUCT_REVIEWS, GET_CURRENT_USER. LIST supports pagination, sorting, search, and filters for guests, email verification, and last login; COUNT supports the same search and filter criteria.',
5
5
  inputSchema: UsersManagementSchema,
6
6
  }, async (params) => usersManagement(context, params));
7
7
  };
@@ -9,7 +9,7 @@ export async function getNormalizedAssortmentDetails({ assortmentId, slug }, con
9
9
  assortmentId: normalizedAssortmentId,
10
10
  locale,
11
11
  });
12
- const filters = await modules.assortments.filters.findFilters({ assortmentId: assortment._id }, { sort: { sortKey: 1 } });
12
+ const filters = await modules.assortments.filters.findFilters({ assortmentId: assortment._id }, { sort: { sortKey: 1, _id: 1 } });
13
13
  const assortmentMedias = await modules.assortments.media.findAssortmentMedias({
14
14
  assortmentId: normalizedAssortmentId,
15
15
  });
@@ -19,7 +19,7 @@ export async function getNormalizedAssortmentDetails({ assortmentId, slug }, con
19
19
  const links = (await loaders.assortmentLinksLoader.load({
20
20
  assortmentId: assortment._id,
21
21
  })) || [];
22
- const products = await modules.assortments.products.findAssortmentProducts({ assortmentId: assortment._id }, { sort: { sortKey: 1 } });
22
+ const products = await modules.assortments.products.findAssortmentProducts({ assortmentId: assortment._id }, { sort: { sortKey: 1, _id: 1 } });
23
23
  const assortmentIds = assortmentChildLinks.map(({ childAssortmentId }) => childAssortmentId);
24
24
  const childrenCount = await modules.assortments.count({
25
25
  assortmentIds,
@@ -38,6 +38,7 @@ export async function getNormalizedUserDetails(userId, context) {
38
38
  const paymentCredentials = await modules.payment.paymentCredentials.findPaymentCredentials({ userId: user._id }, {
39
39
  sort: {
40
40
  created: -1,
41
+ _id: -1,
41
42
  },
42
43
  });
43
44
  const quotations = await modules.quotations.findQuotations({