@unchainedshop/api 5.0.0-alpha.2 → 5.0.0-alpha.4
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 +8 -6
- package/lib/adminUiPlugins.d.ts +97 -0
- package/lib/adminUiPlugins.js +164 -0
- package/lib/auth.d.ts +0 -8
- package/lib/auth.js +0 -20
- package/lib/chat/generateImageHandler.d.ts +273 -7
- package/lib/chat/utils.d.ts +8 -0
- package/lib/chat/utils.js +54 -2
- package/lib/createGraphQLServer.js +6 -0
- package/lib/express/chatHandler.js +63 -61
- package/lib/express/createMCPMiddleware.js +19 -78
- package/lib/express/index.d.ts +6 -2
- package/lib/express/index.js +67 -7
- package/lib/fastify/chatHandler.js +61 -61
- package/lib/fastify/index.d.ts +5 -1
- package/lib/fastify/index.js +70 -11
- package/lib/fastify/mcpHandler.js +24 -75
- 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/cartLoader.d.ts +13 -0
- package/lib/loaders/cartLoader.js +8 -0
- package/lib/loaders/filterTextLoader.js +1 -0
- package/lib/loaders/index.d.ts +9 -0
- package/lib/loaders/index.js +2 -0
- package/lib/loaders/productMediaTextLoader.js +1 -0
- package/lib/loaders/productTextLoader.js +1 -0
- package/lib/loaders/productVariationTextLoader.js +1 -0
- package/lib/locale-context.d.ts +2 -4
- package/lib/locale-context.js +2 -1
- package/lib/mcp/handleMcpHttpRequest.d.ts +2 -0
- package/lib/mcp/handleMcpHttpRequest.js +83 -0
- package/lib/mcp/index.d.ts +1 -1
- package/lib/mcp/nodeHttpBridge.d.ts +3 -0
- package/lib/mcp/nodeHttpBridge.js +64 -0
- package/lib/mcp/resources/localization.d.ts +5 -1
- package/lib/mcp/resources/localization.js +93 -69
- package/lib/mcp/tools/assortment/handlers/getAssortmentFilters.js +1 -1
- package/lib/mcp/tools/assortment/handlers/getAssortmentProducts.js +1 -1
- package/lib/mcp/tools/assortment/index.d.ts +1 -1
- package/lib/mcp/tools/assortment/index.js +4 -1
- package/lib/mcp/tools/assortment/schemas.d.ts +2 -2
- package/lib/mcp/tools/filter/handlers/createFilter.js +2 -4
- package/lib/mcp/tools/filter/handlers/createFilterOption.js +2 -4
- package/lib/mcp/tools/filter/handlers/removeFilterOption.js +2 -4
- package/lib/mcp/tools/filter/handlers/updateFilter.js +2 -2
- package/lib/mcp/tools/filter/index.d.ts +1 -1
- package/lib/mcp/tools/filter/index.js +4 -1
- package/lib/mcp/tools/filter/schemas.d.ts +2 -2
- package/lib/mcp/tools/localization/index.d.ts +1 -1
- package/lib/mcp/tools/localization/index.js +4 -1
- package/lib/mcp/tools/localization/schemas.d.ts +2 -2
- package/lib/mcp/tools/order/index.d.ts +1 -1
- package/lib/mcp/tools/order/index.js +4 -1
- package/lib/mcp/tools/order/schemas.d.ts +2 -2
- package/lib/mcp/tools/product/index.d.ts +1 -1
- package/lib/mcp/tools/product/index.js +4 -1
- package/lib/mcp/tools/product/schemas.d.ts +7 -7
- package/lib/mcp/tools/product/schemas.js +3 -2
- package/lib/mcp/tools/provider/index.d.ts +1 -1
- package/lib/mcp/tools/provider/index.js +4 -1
- package/lib/mcp/tools/provider/schemas.d.ts +2 -2
- package/lib/mcp/tools/quotation/index.d.ts +1 -1
- package/lib/mcp/tools/quotation/index.js +4 -1
- package/lib/mcp/tools/quotation/schemas.d.ts +2 -2
- package/lib/mcp/tools/system/index.d.ts +1 -1
- package/lib/mcp/tools/system/index.js +4 -1
- package/lib/mcp/tools/users/handlers/getUserPaymentCredentials.js +1 -0
- package/lib/mcp/tools/users/index.d.ts +1 -1
- package/lib/mcp/tools/users/index.js +4 -1
- package/lib/mcp/tools/users/schemas.d.ts +2 -2
- package/lib/mcp/utils/getNormalizedAssortmentDetails.js +2 -2
- package/lib/mcp/utils/getNormalizedUserDetails.js +1 -0
- package/lib/mcp/utils/sharedSchemas.d.ts +1 -0
- package/lib/mcp/utils/sharedSchemas.js +1 -0
- package/lib/middleware/createAuthMiddleware.js +2 -39
- package/lib/resolvers/index.js +2 -1
- package/lib/resolvers/mutations/bookmarks/createBookmark.d.ts +1 -1
- package/lib/resolvers/mutations/bookmarks/createBookmark.js +4 -3
- package/lib/resolvers/mutations/filters/createFilter.js +3 -4
- package/lib/resolvers/mutations/filters/createFilterOption.d.ts +1 -1
- package/lib/resolvers/mutations/filters/createFilterOption.js +3 -4
- package/lib/resolvers/mutations/filters/removeFilterOption.d.ts +1 -1
- package/lib/resolvers/mutations/filters/removeFilterOption.js +2 -8
- package/lib/resolvers/mutations/filters/updateFilter.d.ts +1 -1
- package/lib/resolvers/mutations/filters/updateFilter.js +2 -5
- package/lib/resolvers/mutations/index.js +3 -3
- package/lib/resolvers/mutations/orders/checkoutCart.js +1 -1
- package/lib/resolvers/mutations/products/updateProductCommerce.js +10 -1
- package/lib/resolvers/scalars/LowerCaseString.d.ts +1 -1
- package/lib/resolvers/scalars/index.d.ts +1 -1
- package/lib/resolvers/type/assortment/assortment-types.js +2 -2
- package/lib/resolvers/type/filter/loaded-filter-types.d.ts +1 -1
- package/lib/resolvers/type/filter/loaded-filter-types.js +1 -1
- package/lib/resolvers/type/index.d.ts +9 -5
- package/lib/resolvers/type/product/product-bundle-types.d.ts +1 -1
- 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/quotation-types.d.ts +4 -0
- package/lib/resolvers/type/quotation-types.js +1 -0
- package/lib/resolvers/type/user-types.js +3 -2
- package/lib/roles/index.js +2 -0
- package/lib/roles/loggedIn.js +11 -0
- package/lib/schema/inputTypes.js +3 -3
- package/lib/schema/scalars.js +1 -0
- package/lib/schema/types/filter.js +2 -2
- package/lib/schema/types/product/product.js +1 -1
- package/lib/schema/types/quotation.js +5 -0
- package/lib/utils/optionalPeerError.d.ts +1 -0
- package/lib/utils/optionalPeerError.js +6 -0
- package/package.json +44 -15
|
@@ -1,89 +1,69 @@
|
|
|
1
1
|
import generateImageHandler from "../chat/generateImageHandler.js";
|
|
2
2
|
import defaultSystemPrompt from "../chat/defaultSystemPrompt.js";
|
|
3
3
|
import normalizeToolsIndex from "../chat/normalizeToolsIndex.js";
|
|
4
|
-
import { errorHandler } from "../chat/utils.js";
|
|
4
|
+
import { chatErrorStatus, createChatRequestLifecycle, errorHandler, logOptionalPeerLoadError, } from "../chat/utils.js";
|
|
5
|
+
import { buildChatResourceContext } from "../mcp/resources/localization.js";
|
|
5
6
|
import { createLogger } from '@unchainedshop/logger';
|
|
6
7
|
const logger = createLogger('unchained:api:chat');
|
|
7
8
|
let convertToModelMessages;
|
|
8
9
|
let stepCountIs;
|
|
9
10
|
let streamText;
|
|
10
11
|
let createMCPClient;
|
|
11
|
-
let StreamableHTTPClientTransport;
|
|
12
|
-
let Client;
|
|
13
12
|
try {
|
|
14
13
|
const aiTools = await import('ai');
|
|
15
|
-
const mcpTools = await import('@ai-sdk/mcp');
|
|
16
|
-
const mcpSDKClientLibrary = await import('@modelcontextprotocol/sdk/client/streamableHttp.js');
|
|
17
|
-
const mcpSDKClient = await import('@modelcontextprotocol/sdk/client/index.js');
|
|
18
|
-
StreamableHTTPClientTransport = mcpSDKClientLibrary.StreamableHTTPClientTransport;
|
|
19
|
-
Client = mcpSDKClient.Client;
|
|
20
14
|
convertToModelMessages = aiTools.convertToModelMessages;
|
|
21
15
|
stepCountIs = aiTools.stepCountIs;
|
|
22
16
|
streamText = aiTools.streamText;
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
logOptionalPeerLoadError('ai', error);
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const mcpTools = await import('@ai-sdk/mcp');
|
|
23
23
|
createMCPClient = mcpTools.createMCPClient;
|
|
24
24
|
}
|
|
25
|
-
catch {
|
|
26
|
-
|
|
25
|
+
catch (error) {
|
|
26
|
+
logOptionalPeerLoadError('@ai-sdk/mcp', error);
|
|
27
27
|
}
|
|
28
28
|
const setupMCPChatHandler = (chatConfiguration) => {
|
|
29
29
|
if (!chatConfiguration?.model) {
|
|
30
30
|
throw new Error('Model is required');
|
|
31
31
|
}
|
|
32
|
-
const { tools: additionalTools = {}, unchainedMCPUrl = `${process.env.ROOT_URL}/mcp`, imageGenerationTool, ...restChatConfig } = chatConfiguration;
|
|
32
|
+
const { tools: additionalTools = {}, unchainedMCPUrl = `${process.env.ROOT_URL}/mcp`, model, imageGenerationTool, abortSignal: configuredAbortSignal, onAbort: configuredOnAbort, onEnd: configuredOnEnd, onFinish: configuredOnFinish, ...restChatConfig } = chatConfiguration;
|
|
33
33
|
const system = chatConfiguration.system ?? defaultSystemPrompt;
|
|
34
34
|
const mcpChatHandler = async (req, res) => {
|
|
35
|
+
if (req.method === 'OPTIONS') {
|
|
36
|
+
res.headers({
|
|
37
|
+
'access-control-allow-credentials': 'true',
|
|
38
|
+
'access-control-allow-private-network': 'true',
|
|
39
|
+
});
|
|
40
|
+
return res.status(200).send();
|
|
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
|
+
}
|
|
35
50
|
let client;
|
|
51
|
+
const lifecycle = createChatRequestLifecycle(res.raw, configuredAbortSignal);
|
|
36
52
|
try {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
});
|
|
42
|
-
return res.status(200).send();
|
|
43
|
-
}
|
|
44
|
-
const resourceTransport = new StreamableHTTPClientTransport(new URL(unchainedMCPUrl), {
|
|
45
|
-
requestInit: {
|
|
46
|
-
headers: {
|
|
47
|
-
Cookie: req.headers.cookie || '',
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
|
-
const sdkClient = new Client({ name: 'unchained-chat-client', version: '1.0.0' });
|
|
52
|
-
await sdkClient.connect(resourceTransport);
|
|
53
|
-
const transport = new StreamableHTTPClientTransport(new URL(unchainedMCPUrl), {
|
|
54
|
-
requestInit: {
|
|
53
|
+
client = await createMCPClient({
|
|
54
|
+
transport: {
|
|
55
|
+
type: 'http',
|
|
56
|
+
url: unchainedMCPUrl,
|
|
55
57
|
headers: {
|
|
56
58
|
Cookie: req.headers.cookie || '',
|
|
59
|
+
...(req.headers.authorization ? { Authorization: req.headers.authorization } : {}),
|
|
57
60
|
},
|
|
58
61
|
},
|
|
62
|
+
initializationOptions: { signal: lifecycle.signal },
|
|
59
63
|
});
|
|
60
|
-
|
|
61
|
-
transport,
|
|
62
|
-
});
|
|
64
|
+
lifecycle.setClientClose(() => client.close());
|
|
63
65
|
const defaultUnchainedTools = await client.tools();
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
const resources = await sdkClient.listResources();
|
|
67
|
-
if (resources?.resources) {
|
|
68
|
-
const resourceTexts = await Promise.all(resources.resources.map(async (resource) => {
|
|
69
|
-
try {
|
|
70
|
-
const content = await sdkClient.readResource({ uri: resource.uri });
|
|
71
|
-
if (content?.contents?.[0]?.text) {
|
|
72
|
-
return `${resource.name}:\n${content.contents[0].text}`;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
catch (e) {
|
|
76
|
-
logger.error(`Failed to read resource ${resource.uri}: ${e.message}`);
|
|
77
|
-
}
|
|
78
|
-
return null;
|
|
79
|
-
}));
|
|
80
|
-
resourceContext =
|
|
81
|
-
'\n\nAVAILABLE SHOP CONFIGURATION:\n' + resourceTexts.filter(Boolean).join('\n\n');
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
catch (e) {
|
|
85
|
-
logger.error(`Failed to fetch MCP resources: ${e.message}`);
|
|
86
|
-
}
|
|
66
|
+
const resourceContext = await buildChatResourceContext(unchainedContext);
|
|
87
67
|
const tools = {
|
|
88
68
|
...defaultUnchainedTools,
|
|
89
69
|
...additionalTools,
|
|
@@ -92,6 +72,7 @@ const setupMCPChatHandler = (chatConfiguration) => {
|
|
|
92
72
|
tools.generateImage = generateImageHandler(req)(imageGenerationTool);
|
|
93
73
|
}
|
|
94
74
|
if (req.method === 'GET') {
|
|
75
|
+
await lifecycle.close();
|
|
95
76
|
return res.status(200).send({
|
|
96
77
|
tools: normalizeToolsIndex(tools),
|
|
97
78
|
cached: false,
|
|
@@ -127,14 +108,28 @@ const setupMCPChatHandler = (chatConfiguration) => {
|
|
|
127
108
|
const messagesToInclude = normalizedMessages.slice(startIndex);
|
|
128
109
|
const result = streamText({
|
|
129
110
|
stopWhen: stepCountIs(500),
|
|
130
|
-
temperature: 0.2,
|
|
131
111
|
maxRetries: 3,
|
|
132
112
|
...restChatConfig,
|
|
113
|
+
abortSignal: lifecycle.signal,
|
|
133
114
|
messages: messagesToInclude,
|
|
134
115
|
system: system + resourceContext,
|
|
116
|
+
model,
|
|
135
117
|
tools: cacheControlledTools,
|
|
136
|
-
|
|
137
|
-
|
|
118
|
+
onEnd: async (event) => {
|
|
119
|
+
try {
|
|
120
|
+
await (configuredOnEnd ?? configuredOnFinish)?.(event);
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
await lifecycle.close();
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
onAbort: async (event) => {
|
|
127
|
+
try {
|
|
128
|
+
await configuredOnAbort?.(event);
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
await lifecycle.close();
|
|
132
|
+
}
|
|
138
133
|
},
|
|
139
134
|
providerOptions: {
|
|
140
135
|
anthropic: {
|
|
@@ -144,21 +139,26 @@ const setupMCPChatHandler = (chatConfiguration) => {
|
|
|
144
139
|
},
|
|
145
140
|
},
|
|
146
141
|
});
|
|
142
|
+
void result.finishReason.then(undefined, async () => {
|
|
143
|
+
await lifecycle.close();
|
|
144
|
+
});
|
|
147
145
|
return res.send(result.toUIMessageStreamResponse({
|
|
148
146
|
onError: errorHandler,
|
|
149
147
|
}));
|
|
150
148
|
}
|
|
151
149
|
catch (err) {
|
|
152
150
|
logger.error(err);
|
|
153
|
-
await
|
|
154
|
-
res.
|
|
151
|
+
await lifecycle.close();
|
|
152
|
+
if (lifecycle.signal.aborted || res.raw.destroyed)
|
|
153
|
+
return res;
|
|
154
|
+
res.status(chatErrorStatus(err));
|
|
155
155
|
return res.send({ error: errorHandler(err) });
|
|
156
156
|
}
|
|
157
157
|
};
|
|
158
158
|
return mcpChatHandler;
|
|
159
159
|
};
|
|
160
160
|
export const connectChat = (app, chatConfiguration) => {
|
|
161
|
-
if (!createMCPClient) {
|
|
161
|
+
if (!createMCPClient || !streamText) {
|
|
162
162
|
logger.warn('Optional dependencies for AI SDK Chat Handler are not installed. Please install @ai-sdk/mcp and ai packages to use this feature.');
|
|
163
163
|
return;
|
|
164
164
|
}
|
package/lib/fastify/index.d.ts
CHANGED
|
@@ -4,9 +4,13 @@ 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 AdminUIPluginConfig, type AdminUIThemeConfig } from '../adminUiPlugins.ts';
|
|
8
|
+
export type { AdminUIPluginConfig, AdminUIPluginEntityConfig, AdminUIPluginPageConfig, AdminUIPluginTabConfig, AdminUIPluginWidgetConfig, AdminUIPluginSlotConfig, AdminUIThemeTokens, AdminUIThemeConfig, } from '../adminUiPlugins.ts';
|
|
7
9
|
export interface AdminUIRouterOptions {
|
|
8
|
-
prefix
|
|
10
|
+
prefix?: string;
|
|
9
11
|
enabled?: boolean;
|
|
12
|
+
theme?: AdminUIThemeConfig;
|
|
13
|
+
plugins?: AdminUIPluginConfig[];
|
|
10
14
|
}
|
|
11
15
|
export declare const unchainedLogger: (prefix: string) => FastifyBaseLogger;
|
|
12
16
|
export declare const connect: (fastify: FastifyInstance, { graphqlHandler, unchainedAPI, }: {
|
package/lib/fastify/index.js
CHANGED
|
@@ -6,8 +6,11 @@ import { createLogger } from '@unchainedshop/logger';
|
|
|
6
6
|
import mcpHandler from "./mcpHandler.js";
|
|
7
7
|
import { connectChat } from "./chatHandler.js";
|
|
8
8
|
import { mountRoutes } from "./mountRoutes.js";
|
|
9
|
-
import {
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
10
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
10
12
|
import { createBackchannelLogoutRoute } from "../handlers/createBackchannelLogoutHandler.js";
|
|
13
|
+
import { preparePluginAssets, resolveAdminUIPath, } from "../adminUiPlugins.js";
|
|
11
14
|
const resolveUserRemoteAddress = (req, trustProxy = false) => {
|
|
12
15
|
let remoteAddress;
|
|
13
16
|
if (trustProxy) {
|
|
@@ -120,9 +123,60 @@ export const connect = async (fastify, { graphqlHandler, unchainedAPI, }, { allo
|
|
|
120
123
|
}
|
|
121
124
|
mountRoutes(fastify, unchainedAPI, routes);
|
|
122
125
|
if (adminUI) {
|
|
126
|
+
const adminUIOptions = typeof adminUI === 'object' ? adminUI : undefined;
|
|
127
|
+
const adminUIPlugins = adminUIOptions?.plugins || [];
|
|
128
|
+
let themePromise;
|
|
129
|
+
const resolveThemeCSS = async () => {
|
|
130
|
+
let css = '/* default theme */';
|
|
131
|
+
if (adminUIOptions?.theme) {
|
|
132
|
+
try {
|
|
133
|
+
const { generateThemeCSS } = await import('@unchainedshop/admin-ui/theme');
|
|
134
|
+
css = generateThemeCSS(adminUIOptions.theme);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
fastify.log.warn("npm dependency @unchainedshop/admin-ui is not installed, can't apply admin-ui theme");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const etag = `"${createHash('sha256').update(css).digest('hex').slice(0, 8)}"`;
|
|
141
|
+
return { css, etag };
|
|
142
|
+
};
|
|
143
|
+
fastify.get('/admin-ui-theme.css', async (request, reply) => {
|
|
144
|
+
const { css, etag } = await (themePromise ||= resolveThemeCSS());
|
|
145
|
+
if (request.headers['if-none-match'] === etag) {
|
|
146
|
+
return reply.code(304).send();
|
|
147
|
+
}
|
|
148
|
+
return reply
|
|
149
|
+
.header('Cache-Control', 'public, max-age=0, must-revalidate')
|
|
150
|
+
.header('ETag', etag)
|
|
151
|
+
.type('text/css')
|
|
152
|
+
.send(css);
|
|
153
|
+
});
|
|
154
|
+
const devMode = process.env.NODE_ENV !== 'production';
|
|
155
|
+
const { routes: pluginRoutes } = preparePluginAssets(adminUIPlugins, fastify.log, {
|
|
156
|
+
devMode,
|
|
157
|
+
});
|
|
158
|
+
for (const [path, asset] of pluginRoutes) {
|
|
159
|
+
fastify.get(path, async (request, reply) => {
|
|
160
|
+
if (asset.etag) {
|
|
161
|
+
const ifNoneMatch = request.headers['if-none-match'];
|
|
162
|
+
if (ifNoneMatch === asset.etag) {
|
|
163
|
+
return reply.code(304).send();
|
|
164
|
+
}
|
|
165
|
+
reply.header('ETag', asset.etag);
|
|
166
|
+
}
|
|
167
|
+
const body = typeof asset.content === 'function' ? asset.content() : asset.content;
|
|
168
|
+
return reply
|
|
169
|
+
.header('Cache-Control', asset.cacheControl)
|
|
170
|
+
.header('X-Content-Type-Options', 'nosniff')
|
|
171
|
+
.header('Access-Control-Allow-Origin', '*')
|
|
172
|
+
.type(asset.contentType)
|
|
173
|
+
.send(body);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
123
176
|
fastify.register(adminUIRouter, {
|
|
124
177
|
enabled: true,
|
|
125
|
-
prefix:
|
|
178
|
+
prefix: adminUIOptions?.prefix || '/',
|
|
179
|
+
plugins: adminUIPlugins,
|
|
126
180
|
});
|
|
127
181
|
}
|
|
128
182
|
};
|
|
@@ -142,15 +196,6 @@ const fallbackLandingPageHandler = (request, reply) => {
|
|
|
142
196
|
return reply.status(404).send();
|
|
143
197
|
}
|
|
144
198
|
};
|
|
145
|
-
const resolveAdminUIPath = () => {
|
|
146
|
-
try {
|
|
147
|
-
const staticURL = import.meta.resolve('@unchainedshop/admin-ui');
|
|
148
|
-
return new URL(staticURL).pathname.split('/').slice(0, -1).join('/');
|
|
149
|
-
}
|
|
150
|
-
catch {
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
};
|
|
154
199
|
export const adminUIRouter = async (fastify, opts) => {
|
|
155
200
|
try {
|
|
156
201
|
let fastifyStatic;
|
|
@@ -164,9 +209,23 @@ export const adminUIRouter = async (fastify, opts) => {
|
|
|
164
209
|
if (fastifyStatic) {
|
|
165
210
|
const adminUIPath = resolveAdminUIPath();
|
|
166
211
|
if (adminUIPath) {
|
|
212
|
+
const indexHtml = readFileSync(join(adminUIPath, 'index.html'), 'utf-8');
|
|
213
|
+
const extHtmlPath = join(adminUIPath, 'ext', '[[...slug]]', 'index.html');
|
|
214
|
+
const extHtml = existsSync(extHtmlPath) ? readFileSync(extHtmlPath, 'utf-8') : null;
|
|
167
215
|
await fastify.register(fastifyStatic, {
|
|
168
216
|
root: adminUIPath,
|
|
169
217
|
prefix: opts.prefix || '/',
|
|
218
|
+
wildcard: false,
|
|
219
|
+
});
|
|
220
|
+
fastify.setNotFoundHandler(async (request, reply) => {
|
|
221
|
+
if (request.method === 'GET' && !request.url.includes('.')) {
|
|
222
|
+
const urlPath = request.url.split('?')[0].replace(/\/+$/, '');
|
|
223
|
+
if (extHtml && (urlPath === '/ext' || urlPath.startsWith('/ext/'))) {
|
|
224
|
+
return reply.type('text/html').send(extHtml);
|
|
225
|
+
}
|
|
226
|
+
return reply.type('text/html').send(indexHtml);
|
|
227
|
+
}
|
|
228
|
+
return reply.code(404).send({ error: 'Not Found' });
|
|
170
229
|
});
|
|
171
230
|
return;
|
|
172
231
|
}
|
|
@@ -1,83 +1,32 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
2
|
import { createLogger } from '@unchainedshop/logger';
|
|
3
|
+
import handleMcpHttpRequest from "../mcp/handleMcpHttpRequest.js";
|
|
4
|
+
import { toWebRequest } from "../mcp/nodeHttpBridge.js";
|
|
3
5
|
const logger = createLogger('unchained:api:mcp');
|
|
4
|
-
let StreamableHTTPServerTransport;
|
|
5
|
-
let isInitializeRequest;
|
|
6
|
-
let McpServer;
|
|
7
|
-
try {
|
|
8
|
-
const mcpSDKServerLibrary = await import('@modelcontextprotocol/sdk/server/streamableHttp.js');
|
|
9
|
-
const mcpSDKClient = await import('@modelcontextprotocol/sdk/types.js');
|
|
10
|
-
const mcpSDKServer = await import('@modelcontextprotocol/sdk/server/mcp.js');
|
|
11
|
-
StreamableHTTPServerTransport = mcpSDKServerLibrary.StreamableHTTPServerTransport;
|
|
12
|
-
isInitializeRequest = mcpSDKClient.isInitializeRequest;
|
|
13
|
-
McpServer = mcpSDKServer.McpServer;
|
|
14
|
-
}
|
|
15
|
-
catch {
|
|
16
|
-
logger.warn(`optional peer npm package '@modelcontextprotocol/sdk' not installed, mcp will not work`);
|
|
17
|
-
}
|
|
18
|
-
const transports = {};
|
|
19
6
|
const mcpHandler = async (req, res) => {
|
|
20
|
-
if (!req.unchainedContext.user) {
|
|
21
|
-
res.status(401);
|
|
22
|
-
res.header('WWW-Authenticate', `Bearer realm="Unchained MCP", error="invalid_token", resource="${process.env.ROOT_URL || 'http://localhost:4010'}",`);
|
|
23
|
-
return res.send(JSON.stringify({
|
|
24
|
-
error: 'invalid_token',
|
|
25
|
-
resource_metadata: `${process.env.ROOT_URL || 'http://localhost:4010'}/.well-known/oauth-protected-resource`,
|
|
26
|
-
}));
|
|
27
|
-
}
|
|
28
7
|
try {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (transport.sessionId) {
|
|
44
|
-
delete transports[transport.sessionId];
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
const roles = req.unchainedContext.user.roles || [];
|
|
48
|
-
const server = initMCPServer(new McpServer({
|
|
49
|
-
name: 'Unchained MCP Server',
|
|
50
|
-
version: '1.0.0',
|
|
51
|
-
}), req.unchainedContext, roles);
|
|
52
|
-
await server.connect(transport);
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
return res.status(400).send(JSON.stringify({
|
|
56
|
-
jsonrpc: '2.0',
|
|
57
|
-
error: {
|
|
58
|
-
code: -32000,
|
|
59
|
-
message: 'Bad Request: No valid session ID provided',
|
|
60
|
-
},
|
|
61
|
-
id: null,
|
|
62
|
-
}));
|
|
63
|
-
}
|
|
64
|
-
await transport.handleRequest(req.raw, res.raw, req.body);
|
|
65
|
-
return res;
|
|
66
|
-
}
|
|
67
|
-
else if (req.method === 'GET' || req.method === 'DELETE') {
|
|
68
|
-
const sessionId = req.headers['mcp-session-id'];
|
|
69
|
-
if (!sessionId || !transports[sessionId]) {
|
|
70
|
-
return res.status(400).send('Invalid or missing session ID');
|
|
71
|
-
}
|
|
72
|
-
const transport = transports[sessionId];
|
|
73
|
-
await transport.handleRequest(req.raw, res.raw);
|
|
74
|
-
return res;
|
|
75
|
-
}
|
|
76
|
-
return res.status(405).send('Method Not Allowed');
|
|
8
|
+
const bodyText = req.method === 'POST' && req.body !== undefined ? JSON.stringify(req.body) : undefined;
|
|
9
|
+
const response = await handleMcpHttpRequest(req.unchainedContext, toWebRequest(req.raw, res.raw, bodyText), req.method === 'POST' ? req.body : undefined);
|
|
10
|
+
res.status(response.status);
|
|
11
|
+
response.headers.forEach((value, key) => {
|
|
12
|
+
if (key === 'set-cookie')
|
|
13
|
+
return;
|
|
14
|
+
res.header(key, value);
|
|
15
|
+
});
|
|
16
|
+
const setCookie = response.headers.getSetCookie();
|
|
17
|
+
if (setCookie.length)
|
|
18
|
+
res.header('set-cookie', setCookie);
|
|
19
|
+
if (!response.body)
|
|
20
|
+
return res.send();
|
|
21
|
+
return res.send(Readable.fromWeb(response.body));
|
|
77
22
|
}
|
|
78
|
-
catch (
|
|
79
|
-
logger.error(
|
|
80
|
-
|
|
23
|
+
catch (error) {
|
|
24
|
+
logger.error(error);
|
|
25
|
+
if (!res.sent) {
|
|
26
|
+
return res.status(500).send({ error: 'Internal Server Error' });
|
|
27
|
+
}
|
|
28
|
+
res.raw.destroy();
|
|
29
|
+
return res;
|
|
81
30
|
}
|
|
82
31
|
};
|
|
83
32
|
export default mcpHandler;
|
|
@@ -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
|
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { UnchainedCore } from '@unchainedshop/core';
|
|
2
|
+
import type { Order } from '@unchainedshop/core-orders';
|
|
3
|
+
import DataLoader from 'dataloader';
|
|
4
|
+
declare const _default: (unchainedAPI: UnchainedCore) => DataLoader<{
|
|
5
|
+
userId: string;
|
|
6
|
+
countryCode?: string;
|
|
7
|
+
orderNumber?: string;
|
|
8
|
+
}, Order | null, {
|
|
9
|
+
userId: string;
|
|
10
|
+
countryCode?: string;
|
|
11
|
+
orderNumber?: string;
|
|
12
|
+
}>;
|
|
13
|
+
export default _default;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import DataLoader from 'dataloader';
|
|
2
|
+
export default (unchainedAPI) => new DataLoader(async (queries) => {
|
|
3
|
+
const userIds = [...new Set(queries.map((q) => q.userId).filter(Boolean))];
|
|
4
|
+
const carts = await unchainedAPI.modules.orders.findCarts({ userIds });
|
|
5
|
+
return queries.map((q) => carts.find((cart) => cart.userId === q.userId &&
|
|
6
|
+
(!q.countryCode || cart.countryCode === q.countryCode) &&
|
|
7
|
+
(!q.orderNumber || cart.orderNumber === q.orderNumber)) || null);
|
|
8
|
+
});
|
package/lib/loaders/index.d.ts
CHANGED
|
@@ -166,6 +166,15 @@ declare const loaders: (unchainedAPI: UnchainedCore) => {
|
|
|
166
166
|
}, import("@unchainedshop/core-orders").Order, {
|
|
167
167
|
orderId: string;
|
|
168
168
|
}>;
|
|
169
|
+
cartLoader: import("dataloader")<{
|
|
170
|
+
userId: string;
|
|
171
|
+
countryCode?: string;
|
|
172
|
+
orderNumber?: string;
|
|
173
|
+
}, import("@unchainedshop/core-orders").Order | null, {
|
|
174
|
+
userId: string;
|
|
175
|
+
countryCode?: string;
|
|
176
|
+
orderNumber?: string;
|
|
177
|
+
}>;
|
|
169
178
|
quotationLoader: import("dataloader")<{
|
|
170
179
|
quotationId: string;
|
|
171
180
|
}, import("@unchainedshop/core-quotations").Quotation | null, {
|
package/lib/loaders/index.js
CHANGED
|
@@ -26,6 +26,7 @@ import deliveryProviderLoader from "./deliveryProviderLoader.js";
|
|
|
26
26
|
import paymentProviderLoader from "./paymentProviderLoader.js";
|
|
27
27
|
import warehousingProviderLoader from "./warehousingProviderLoader.js";
|
|
28
28
|
import orderLoader from "./orderLoader.js";
|
|
29
|
+
import cartLoader from "./cartLoader.js";
|
|
29
30
|
import quotationLoader from "./quotationLoader.js";
|
|
30
31
|
import tokenExportStatusLoader from "./tokenExportStatusLoader.js";
|
|
31
32
|
const loaders = (unchainedAPI) => {
|
|
@@ -58,6 +59,7 @@ const loaders = (unchainedAPI) => {
|
|
|
58
59
|
paymentProviderLoader: paymentProviderLoader(unchainedAPI),
|
|
59
60
|
warehousingProviderLoader: warehousingProviderLoader(unchainedAPI),
|
|
60
61
|
orderLoader: orderLoader(unchainedAPI),
|
|
62
|
+
cartLoader: cartLoader(unchainedAPI),
|
|
61
63
|
quotationLoader: quotationLoader(unchainedAPI),
|
|
62
64
|
tokenExportStatusLoader: tokenExportStatusLoader(unchainedAPI),
|
|
63
65
|
};
|
|
@@ -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);
|
package/lib/locale-context.d.ts
CHANGED
|
@@ -6,12 +6,10 @@ export interface UnchainedLocaleContext {
|
|
|
6
6
|
currencyCode: string;
|
|
7
7
|
}
|
|
8
8
|
export type GetHeaderFn = (key: string) => string | string[];
|
|
9
|
-
export declare const resolveDefaultContext: (
|
|
9
|
+
export declare const resolveDefaultContext: import("@unchainedshop/utils/lib/memoize-with-ttl.js").MemoizedWithTTL<[{
|
|
10
10
|
acceptLang: any;
|
|
11
11
|
acceptCountry: any;
|
|
12
|
-
}, unchainedAPI: UnchainedCore
|
|
13
|
-
clear: () => void;
|
|
14
|
-
};
|
|
12
|
+
}, unchainedAPI: UnchainedCore], UnchainedLocaleContext>;
|
|
15
13
|
export declare const getLocaleContext: ({ getHeader, }: {
|
|
16
14
|
getHeader: UnchainedHTTPServerContext["getHeader"];
|
|
17
15
|
}, unchainedAPI: UnchainedCore) => Promise<UnchainedLocaleContext>;
|