@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.
- package/README.md +2 -2
- package/lib/acl.js +4 -0
- package/lib/adminUiPlugins.d.ts +36 -1
- package/lib/adminUiPlugins.js +188 -26
- package/lib/auth.d.ts +0 -8
- package/lib/auth.js +0 -20
- package/lib/errors.d.ts +2 -0
- package/lib/errors.js +2 -0
- package/lib/events.d.ts +1 -0
- package/lib/events.js +1 -0
- package/lib/express/chatHandler.js +12 -1
- package/lib/express/index.d.ts +2 -4
- package/lib/express/index.js +63 -24
- package/lib/fastify/chatHandler.js +10 -1
- package/lib/fastify/index.d.ts +4 -4
- package/lib/fastify/index.js +83 -30
- package/lib/loaders/assortmentLinksLoader.js +24 -22
- package/lib/loaders/assortmentMediaTextLoader.js +1 -0
- package/lib/loaders/assortmentProductLoader.js +7 -4
- package/lib/loaders/assortmentTextLoader.js +1 -0
- package/lib/loaders/filterTextLoader.js +1 -0
- package/lib/loaders/productMediaTextLoader.js +1 -0
- package/lib/loaders/productTextLoader.js +1 -0
- package/lib/loaders/productVariationTextLoader.js +1 -0
- package/lib/mcp/tools/assortment/handlers/getAssortmentFilters.js +1 -1
- package/lib/mcp/tools/assortment/handlers/getAssortmentProducts.js +1 -1
- package/lib/mcp/tools/system/handlers/allocateWork.js +2 -2
- package/lib/mcp/tools/users/handlers/getUserPaymentCredentials.js +1 -0
- package/lib/mcp/tools/users/index.js +1 -1
- package/lib/mcp/utils/getNormalizedAssortmentDetails.js +2 -2
- package/lib/mcp/utils/getNormalizedUserDetails.js +1 -0
- package/lib/middleware/createAuthMiddleware.js +4 -39
- package/lib/resolvers/mutations/accounts/loginWithPassword.js +16 -2
- package/lib/resolvers/mutations/accounts/logoutAllSessions.d.ts +3 -1
- package/lib/resolvers/mutations/accounts/logoutAllSessions.js +10 -4
- package/lib/resolvers/mutations/bulk/bulkAssignProductsToAssortment.d.ts +10 -0
- package/lib/resolvers/mutations/bulk/bulkAssignProductsToAssortment.js +18 -0
- package/lib/resolvers/mutations/bulk/bulkOperation.d.ts +12 -0
- package/lib/resolvers/mutations/bulk/bulkOperation.js +14 -0
- package/lib/resolvers/mutations/bulk/bulkRemoveAssortments.d.ts +9 -0
- package/lib/resolvers/mutations/bulk/bulkRemoveAssortments.js +7 -0
- package/lib/resolvers/mutations/bulk/bulkRemoveFilters.d.ts +9 -0
- package/lib/resolvers/mutations/bulk/bulkRemoveFilters.js +9 -0
- package/lib/resolvers/mutations/bulk/bulkRemoveProducts.d.ts +9 -0
- package/lib/resolvers/mutations/bulk/bulkRemoveProducts.js +9 -0
- package/lib/resolvers/mutations/bulk/bulkRemoveUsers.d.ts +9 -0
- package/lib/resolvers/mutations/bulk/bulkRemoveUsers.js +7 -0
- package/lib/resolvers/mutations/bulk/bulkSetAssortmentActive.d.ts +10 -0
- package/lib/resolvers/mutations/bulk/bulkSetAssortmentActive.js +9 -0
- package/lib/resolvers/mutations/bulk/bulkSetFilterActive.d.ts +10 -0
- package/lib/resolvers/mutations/bulk/bulkSetFilterActive.js +10 -0
- package/lib/resolvers/mutations/bulk/bulkSetProductStatus.d.ts +10 -0
- package/lib/resolvers/mutations/bulk/bulkSetProductStatus.js +17 -0
- package/lib/resolvers/mutations/bulk/bulkSetUserRoles.d.ts +10 -0
- package/lib/resolvers/mutations/bulk/bulkSetUserRoles.js +14 -0
- package/lib/resolvers/mutations/bulk/bulkUpdateAssortmentTags.d.ts +11 -0
- package/lib/resolvers/mutations/bulk/bulkUpdateAssortmentTags.js +10 -0
- package/lib/resolvers/mutations/bulk/bulkUpdateProductTags.d.ts +11 -0
- package/lib/resolvers/mutations/bulk/bulkUpdateProductTags.js +7 -0
- package/lib/resolvers/mutations/bulk/bulkUpdateUserTags.d.ts +11 -0
- package/lib/resolvers/mutations/bulk/bulkUpdateUserTags.js +7 -0
- package/lib/resolvers/mutations/index.d.ts +12 -0
- package/lib/resolvers/mutations/index.js +25 -1
- package/lib/resolvers/mutations/orders/checkoutCart.js +1 -1
- package/lib/resolvers/mutations/users/removeUser.js +9 -2
- package/lib/resolvers/mutations/users/setRoles.js +9 -2
- package/lib/resolvers/mutations/worker/allocateWork.d.ts +1 -1
- package/lib/resolvers/mutations/worker/allocateWork.js +4 -3
- package/lib/resolvers/queries/index.d.ts +1 -0
- package/lib/resolvers/queries/index.js +2 -0
- package/lib/resolvers/queries/search/globalSearch.d.ts +36 -0
- package/lib/resolvers/queries/search/globalSearch.js +200 -0
- package/lib/resolvers/type/assortment/assortment-types.js +2 -2
- package/lib/resolvers/type/global-search-result-types.d.ts +3 -0
- package/lib/resolvers/type/global-search-result-types.js +15 -0
- package/lib/resolvers/type/index.d.ts +3 -0
- package/lib/resolvers/type/index.js +2 -0
- package/lib/resolvers/type/user-types.d.ts +1 -0
- package/lib/resolvers/type/user-types.js +36 -4
- package/lib/roles/all.js +12 -0
- package/lib/roles/index.js +12 -0
- package/lib/roles/loggedIn.js +1 -1
- package/lib/schema/inputTypes.js +5 -0
- package/lib/schema/mutation.js +74 -1
- package/lib/schema/query.js +15 -0
- package/lib/schema/types/common.js +7 -0
- package/lib/schema/types/search.js +36 -0
- package/lib/schema/types/user.js +4 -0
- package/package.json +31 -26
- package/lib/adminUiTheme.d.ts +0 -25
- package/lib/adminUiTheme.js +0 -34
package/lib/express/index.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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',
|
|
50
|
+
.set('ETag', etag)
|
|
31
51
|
.type('text/css')
|
|
32
|
-
.send(
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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,
|
|
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(
|
|
66
|
+
const resourceContext = await buildChatResourceContext(unchainedContext);
|
|
58
67
|
const tools = {
|
|
59
68
|
...defaultUnchainedTools,
|
|
60
69
|
...additionalTools,
|
package/lib/fastify/index.d.ts
CHANGED
|
@@ -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 '
|
|
8
|
-
|
|
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, }: {
|
package/lib/fastify/index.js
CHANGED
|
@@ -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 {
|
|
11
|
-
import { join } from 'node:path';
|
|
19
|
+
import { readFileSync } from 'node:fs';
|
|
12
20
|
import { createBackchannelLogoutRoute } from "../handlers/createBackchannelLogoutHandler.js";
|
|
13
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
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
|
-
|
|
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',
|
|
170
|
+
.header('ETag', etag)
|
|
139
171
|
.type('text/css')
|
|
140
|
-
.send(
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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.
|
|
215
|
-
}
|
|
216
|
-
|
|
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 =
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
29
|
+
return parentAssortmentLinkMap.get(q.parentAssortmentId) || [];
|
|
28
30
|
}
|
|
29
31
|
else if (q.childAssortmentId) {
|
|
30
|
-
return childAssortmentLinkMap
|
|
32
|
+
return childAssortmentLinkMap.get(q.childAssortmentId) || [];
|
|
31
33
|
}
|
|
32
34
|
if (q.assortmentId) {
|
|
33
35
|
return [
|
|
34
|
-
...(parentAssortmentLinkMap
|
|
35
|
-
...(childAssortmentLinkMap
|
|
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
|
|
10
|
-
|
|
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
|
|
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.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);
|
|
@@ -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
|
|
5
|
+
const work = await WorkerDirector.allocateWork(context, {
|
|
6
6
|
types: options.types || [],
|
|
7
7
|
worker: options.worker || '',
|
|
8
8
|
});
|
|
@@ -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: '
|
|
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({
|