@unchainedshop/api 5.0.0-alpha.1 → 5.0.0-alpha.3
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 +6 -4
- package/lib/adminUiPlugins.d.ts +73 -0
- package/lib/adminUiPlugins.js +164 -0
- package/lib/adminUiTheme.d.ts +25 -0
- package/lib/adminUiTheme.js +34 -0
- 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 +52 -61
- package/lib/express/createMCPMiddleware.js +19 -78
- package/lib/express/index.d.ts +8 -2
- package/lib/express/index.js +55 -7
- package/lib/fastify/chatHandler.js +52 -61
- package/lib/fastify/index.d.ts +7 -1
- package/lib/fastify/index.js +58 -11
- package/lib/fastify/mcpHandler.js +24 -75
- package/lib/loaders/cartLoader.d.ts +13 -0
- package/lib/loaders/cartLoader.js +8 -0
- package/lib/loaders/enrollmentByOrderLoader.d.ts +9 -0
- package/lib/loaders/enrollmentByOrderLoader.js +17 -0
- package/lib/loaders/index.d.ts +14 -0
- package/lib/loaders/index.js +4 -0
- package/lib/loaders/orderDeliveryLoader.d.ts +9 -0
- package/lib/loaders/orderDeliveryLoader.js +12 -0
- package/lib/loaders/orderDiscountsLoader.d.ts +9 -0
- package/lib/loaders/orderDiscountsLoader.js +17 -0
- package/lib/loaders/orderPaymentLoader.d.ts +9 -0
- package/lib/loaders/orderPaymentLoader.js +12 -0
- package/lib/loaders/orderPositionsLoader.d.ts +9 -0
- package/lib/loaders/orderPositionsLoader.js +17 -0
- package/lib/loaders/tokenExportStatusLoader.d.ts +9 -0
- package/lib/loaders/tokenExportStatusLoader.js +31 -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/getAssortment.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/handlers/getProduct.js +1 -1
- 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/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/sharedSchemas.d.ts +1 -0
- package/lib/mcp/utils/sharedSchemas.js +1 -0
- package/lib/middleware/createAuthMiddleware.js +1 -3
- 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/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/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 +12 -8
- 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/token-types.d.ts +3 -3
- package/lib/resolvers/type/token-types.js +6 -5
- package/lib/resolvers/type/user-types.d.ts +1 -1
- package/lib/resolvers/type/user-types.js +2 -2
- package/lib/roles/all.js +1 -1
- 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/schema/types/user.js +1 -1
- package/lib/utils/optionalPeerError.d.ts +1 -0
- package/lib/utils/optionalPeerError.js +6 -0
- package/package.json +39 -15
|
@@ -1,89 +1,60 @@
|
|
|
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
|
+
}
|
|
35
42
|
let client;
|
|
43
|
+
const lifecycle = createChatRequestLifecycle(res.raw, configuredAbortSignal);
|
|
36
44
|
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: {
|
|
45
|
+
client = await createMCPClient({
|
|
46
|
+
transport: {
|
|
47
|
+
type: 'http',
|
|
48
|
+
url: unchainedMCPUrl,
|
|
55
49
|
headers: {
|
|
56
50
|
Cookie: req.headers.cookie || '',
|
|
57
51
|
},
|
|
58
52
|
},
|
|
53
|
+
initializationOptions: { signal: lifecycle.signal },
|
|
59
54
|
});
|
|
60
|
-
|
|
61
|
-
transport,
|
|
62
|
-
});
|
|
55
|
+
lifecycle.setClientClose(() => client.close());
|
|
63
56
|
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
|
-
}
|
|
57
|
+
const resourceContext = await buildChatResourceContext(req.unchainedContext);
|
|
87
58
|
const tools = {
|
|
88
59
|
...defaultUnchainedTools,
|
|
89
60
|
...additionalTools,
|
|
@@ -92,6 +63,7 @@ const setupMCPChatHandler = (chatConfiguration) => {
|
|
|
92
63
|
tools.generateImage = generateImageHandler(req)(imageGenerationTool);
|
|
93
64
|
}
|
|
94
65
|
if (req.method === 'GET') {
|
|
66
|
+
await lifecycle.close();
|
|
95
67
|
return res.status(200).send({
|
|
96
68
|
tools: normalizeToolsIndex(tools),
|
|
97
69
|
cached: false,
|
|
@@ -127,14 +99,28 @@ const setupMCPChatHandler = (chatConfiguration) => {
|
|
|
127
99
|
const messagesToInclude = normalizedMessages.slice(startIndex);
|
|
128
100
|
const result = streamText({
|
|
129
101
|
stopWhen: stepCountIs(500),
|
|
130
|
-
temperature: 0.2,
|
|
131
102
|
maxRetries: 3,
|
|
132
103
|
...restChatConfig,
|
|
104
|
+
abortSignal: lifecycle.signal,
|
|
133
105
|
messages: messagesToInclude,
|
|
134
106
|
system: system + resourceContext,
|
|
107
|
+
model,
|
|
135
108
|
tools: cacheControlledTools,
|
|
136
|
-
|
|
137
|
-
|
|
109
|
+
onEnd: async (event) => {
|
|
110
|
+
try {
|
|
111
|
+
await (configuredOnEnd ?? configuredOnFinish)?.(event);
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
await lifecycle.close();
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
onAbort: async (event) => {
|
|
118
|
+
try {
|
|
119
|
+
await configuredOnAbort?.(event);
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
await lifecycle.close();
|
|
123
|
+
}
|
|
138
124
|
},
|
|
139
125
|
providerOptions: {
|
|
140
126
|
anthropic: {
|
|
@@ -144,21 +130,26 @@ const setupMCPChatHandler = (chatConfiguration) => {
|
|
|
144
130
|
},
|
|
145
131
|
},
|
|
146
132
|
});
|
|
133
|
+
void result.finishReason.then(undefined, async () => {
|
|
134
|
+
await lifecycle.close();
|
|
135
|
+
});
|
|
147
136
|
return res.send(result.toUIMessageStreamResponse({
|
|
148
137
|
onError: errorHandler,
|
|
149
138
|
}));
|
|
150
139
|
}
|
|
151
140
|
catch (err) {
|
|
152
141
|
logger.error(err);
|
|
153
|
-
await
|
|
154
|
-
res.
|
|
142
|
+
await lifecycle.close();
|
|
143
|
+
if (lifecycle.signal.aborted || res.raw.destroyed)
|
|
144
|
+
return res;
|
|
145
|
+
res.status(chatErrorStatus(err));
|
|
155
146
|
return res.send({ error: errorHandler(err) });
|
|
156
147
|
}
|
|
157
148
|
};
|
|
158
149
|
return mcpChatHandler;
|
|
159
150
|
};
|
|
160
151
|
export const connectChat = (app, chatConfiguration) => {
|
|
161
|
-
if (!createMCPClient) {
|
|
152
|
+
if (!createMCPClient || !streamText) {
|
|
162
153
|
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
154
|
return;
|
|
164
155
|
}
|
package/lib/fastify/index.d.ts
CHANGED
|
@@ -4,9 +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
11
|
export interface AdminUIRouterOptions {
|
|
8
|
-
prefix
|
|
12
|
+
prefix?: string;
|
|
9
13
|
enabled?: boolean;
|
|
14
|
+
theme?: AdminUIThemeConfig;
|
|
15
|
+
plugins?: AdminUIPluginConfig[];
|
|
10
16
|
}
|
|
11
17
|
export declare const unchainedLogger: (prefix: string) => FastifyBaseLogger;
|
|
12
18
|
export declare const connect: (fastify: FastifyInstance, { graphqlHandler, unchainedAPI, }: {
|
package/lib/fastify/index.js
CHANGED
|
@@ -6,8 +6,12 @@ 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 { generateThemeCSS } from '@unchainedshop/admin-ui/theme';
|
|
14
|
+
import { preparePluginAssets, resolveAdminUIPath } from "../adminUiPlugins.js";
|
|
11
15
|
const resolveUserRemoteAddress = (req, trustProxy = false) => {
|
|
12
16
|
let remoteAddress;
|
|
13
17
|
if (trustProxy) {
|
|
@@ -120,9 +124,47 @@ export const connect = async (fastify, { graphqlHandler, unchainedAPI, }, { allo
|
|
|
120
124
|
}
|
|
121
125
|
mountRoutes(fastify, unchainedAPI, routes);
|
|
122
126
|
if (adminUI) {
|
|
127
|
+
const adminUIOptions = typeof adminUI === 'object' ? adminUI : undefined;
|
|
128
|
+
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}"`;
|
|
132
|
+
fastify.get('/admin-ui-theme.css', async (request, reply) => {
|
|
133
|
+
if (request.headers['if-none-match'] === themeEtag) {
|
|
134
|
+
return reply.code(304).send();
|
|
135
|
+
}
|
|
136
|
+
return reply
|
|
137
|
+
.header('Cache-Control', 'public, max-age=0, must-revalidate')
|
|
138
|
+
.header('ETag', themeEtag)
|
|
139
|
+
.type('text/css')
|
|
140
|
+
.send(themeCSS);
|
|
141
|
+
});
|
|
142
|
+
const devMode = process.env.NODE_ENV !== 'production';
|
|
143
|
+
const { routes: pluginRoutes } = preparePluginAssets(adminUIPlugins, fastify.log, {
|
|
144
|
+
devMode,
|
|
145
|
+
});
|
|
146
|
+
for (const [path, asset] of pluginRoutes) {
|
|
147
|
+
fastify.get(path, async (request, reply) => {
|
|
148
|
+
if (asset.etag) {
|
|
149
|
+
const ifNoneMatch = request.headers['if-none-match'];
|
|
150
|
+
if (ifNoneMatch === asset.etag) {
|
|
151
|
+
return reply.code(304).send();
|
|
152
|
+
}
|
|
153
|
+
reply.header('ETag', asset.etag);
|
|
154
|
+
}
|
|
155
|
+
const body = typeof asset.content === 'function' ? asset.content() : asset.content;
|
|
156
|
+
return reply
|
|
157
|
+
.header('Cache-Control', asset.cacheControl)
|
|
158
|
+
.header('X-Content-Type-Options', 'nosniff')
|
|
159
|
+
.header('Access-Control-Allow-Origin', '*')
|
|
160
|
+
.type(asset.contentType)
|
|
161
|
+
.send(body);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
123
164
|
fastify.register(adminUIRouter, {
|
|
124
165
|
enabled: true,
|
|
125
|
-
prefix:
|
|
166
|
+
prefix: adminUIOptions?.prefix || '/',
|
|
167
|
+
plugins: adminUIPlugins,
|
|
126
168
|
});
|
|
127
169
|
}
|
|
128
170
|
};
|
|
@@ -142,15 +184,6 @@ const fallbackLandingPageHandler = (request, reply) => {
|
|
|
142
184
|
return reply.status(404).send();
|
|
143
185
|
}
|
|
144
186
|
};
|
|
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
187
|
export const adminUIRouter = async (fastify, opts) => {
|
|
155
188
|
try {
|
|
156
189
|
let fastifyStatic;
|
|
@@ -164,9 +197,23 @@ export const adminUIRouter = async (fastify, opts) => {
|
|
|
164
197
|
if (fastifyStatic) {
|
|
165
198
|
const adminUIPath = resolveAdminUIPath();
|
|
166
199
|
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;
|
|
167
203
|
await fastify.register(fastifyStatic, {
|
|
168
204
|
root: adminUIPath,
|
|
169
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);
|
|
213
|
+
}
|
|
214
|
+
return reply.type('text/html').send(indexHtml);
|
|
215
|
+
}
|
|
216
|
+
return reply.code(404).send({ error: 'Not Found' });
|
|
170
217
|
});
|
|
171
218
|
return;
|
|
172
219
|
}
|
|
@@ -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;
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { UnchainedCore } from '@unchainedshop/core';
|
|
2
|
+
import type { Enrollment } from '@unchainedshop/core-enrollments';
|
|
3
|
+
import DataLoader from 'dataloader';
|
|
4
|
+
declare const _default: (unchainedAPI: UnchainedCore) => DataLoader<{
|
|
5
|
+
orderId: string;
|
|
6
|
+
}, Enrollment | null, {
|
|
7
|
+
orderId: string;
|
|
8
|
+
}>;
|
|
9
|
+
export default _default;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import DataLoader from 'dataloader';
|
|
2
|
+
export default (unchainedAPI) => new DataLoader(async (queries) => {
|
|
3
|
+
const orderIds = [...new Set(queries.map((q) => q.orderId).filter(Boolean))];
|
|
4
|
+
const requestedOrderIds = new Set(orderIds);
|
|
5
|
+
const enrollments = await unchainedAPI.modules.enrollments.findEnrollmentsByOrderIds({
|
|
6
|
+
orderIds,
|
|
7
|
+
});
|
|
8
|
+
const enrollmentMap = {};
|
|
9
|
+
for (const enrollment of enrollments) {
|
|
10
|
+
for (const period of enrollment.periods || []) {
|
|
11
|
+
if (period.orderId && requestedOrderIds.has(period.orderId) && !enrollmentMap[period.orderId]) {
|
|
12
|
+
enrollmentMap[period.orderId] = enrollment;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return queries.map((q) => enrollmentMap[q.orderId] ?? null);
|
|
17
|
+
});
|
package/lib/loaders/index.d.ts
CHANGED
|
@@ -166,11 +166,25 @@ 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, {
|
|
172
181
|
quotationId: string;
|
|
173
182
|
}>;
|
|
183
|
+
tokenExportStatusLoader: import("dataloader")<{
|
|
184
|
+
token: import("@unchainedshop/core-warehousing").TokenSurrogate;
|
|
185
|
+
}, import("@unchainedshop/core-warehousing").TokenStatus, {
|
|
186
|
+
token: import("@unchainedshop/core-warehousing").TokenSurrogate;
|
|
187
|
+
}>;
|
|
174
188
|
};
|
|
175
189
|
export type UnchainedLoaders = Awaited<ReturnType<typeof loaders>>;
|
|
176
190
|
export default loaders;
|
package/lib/loaders/index.js
CHANGED
|
@@ -26,7 +26,9 @@ 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";
|
|
31
|
+
import tokenExportStatusLoader from "./tokenExportStatusLoader.js";
|
|
30
32
|
const loaders = (unchainedAPI) => {
|
|
31
33
|
return {
|
|
32
34
|
assortmentLoader: assortmentLoader(unchainedAPI),
|
|
@@ -57,7 +59,9 @@ const loaders = (unchainedAPI) => {
|
|
|
57
59
|
paymentProviderLoader: paymentProviderLoader(unchainedAPI),
|
|
58
60
|
warehousingProviderLoader: warehousingProviderLoader(unchainedAPI),
|
|
59
61
|
orderLoader: orderLoader(unchainedAPI),
|
|
62
|
+
cartLoader: cartLoader(unchainedAPI),
|
|
60
63
|
quotationLoader: quotationLoader(unchainedAPI),
|
|
64
|
+
tokenExportStatusLoader: tokenExportStatusLoader(unchainedAPI),
|
|
61
65
|
};
|
|
62
66
|
};
|
|
63
67
|
export default loaders;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { UnchainedCore } from '@unchainedshop/core';
|
|
2
|
+
import type { OrderDelivery } from '@unchainedshop/core-orders';
|
|
3
|
+
import DataLoader from 'dataloader';
|
|
4
|
+
declare const _default: (unchainedAPI: UnchainedCore) => DataLoader<{
|
|
5
|
+
orderDeliveryId: string;
|
|
6
|
+
}, OrderDelivery | null, {
|
|
7
|
+
orderDeliveryId: string;
|
|
8
|
+
}>;
|
|
9
|
+
export default _default;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import DataLoader from 'dataloader';
|
|
2
|
+
export default (unchainedAPI) => new DataLoader(async (queries) => {
|
|
3
|
+
const orderDeliveryIds = [...new Set(queries.map((q) => q.orderDeliveryId).filter(Boolean))];
|
|
4
|
+
const deliveries = await unchainedAPI.modules.orders.deliveries.findDeliveries({
|
|
5
|
+
orderDeliveryIds,
|
|
6
|
+
});
|
|
7
|
+
const deliveryMap = {};
|
|
8
|
+
for (const delivery of deliveries) {
|
|
9
|
+
deliveryMap[delivery._id] = delivery;
|
|
10
|
+
}
|
|
11
|
+
return queries.map((q) => deliveryMap[q.orderDeliveryId] ?? null);
|
|
12
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { UnchainedCore } from '@unchainedshop/core';
|
|
2
|
+
import type { OrderDiscount } from '@unchainedshop/core-orders';
|
|
3
|
+
import DataLoader from 'dataloader';
|
|
4
|
+
declare const _default: (unchainedAPI: UnchainedCore) => DataLoader<{
|
|
5
|
+
orderId: string;
|
|
6
|
+
}, OrderDiscount[], {
|
|
7
|
+
orderId: string;
|
|
8
|
+
}>;
|
|
9
|
+
export default _default;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import DataLoader from 'dataloader';
|
|
2
|
+
export default (unchainedAPI) => new DataLoader(async (queries) => {
|
|
3
|
+
const orderIds = [...new Set(queries.map((q) => q.orderId).filter(Boolean))];
|
|
4
|
+
const discounts = await unchainedAPI.modules.orders.discounts.findOrderDiscounts({
|
|
5
|
+
orderIds,
|
|
6
|
+
});
|
|
7
|
+
const discountsMap = {};
|
|
8
|
+
for (const discount of discounts) {
|
|
9
|
+
if (!discountsMap[discount.orderId]) {
|
|
10
|
+
discountsMap[discount.orderId] = [discount];
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
discountsMap[discount.orderId].push(discount);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return queries.map((q) => discountsMap[q.orderId] || []);
|
|
17
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { UnchainedCore } from '@unchainedshop/core';
|
|
2
|
+
import type { OrderPayment } from '@unchainedshop/core-orders';
|
|
3
|
+
import DataLoader from 'dataloader';
|
|
4
|
+
declare const _default: (unchainedAPI: UnchainedCore) => DataLoader<{
|
|
5
|
+
orderPaymentId: string;
|
|
6
|
+
}, OrderPayment | null, {
|
|
7
|
+
orderPaymentId: string;
|
|
8
|
+
}>;
|
|
9
|
+
export default _default;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import DataLoader from 'dataloader';
|
|
2
|
+
export default (unchainedAPI) => new DataLoader(async (queries) => {
|
|
3
|
+
const orderPaymentIds = [...new Set(queries.map((q) => q.orderPaymentId).filter(Boolean))];
|
|
4
|
+
const payments = await unchainedAPI.modules.orders.payments.findOrderPayments({
|
|
5
|
+
orderPaymentIds,
|
|
6
|
+
});
|
|
7
|
+
const paymentMap = {};
|
|
8
|
+
for (const payment of payments) {
|
|
9
|
+
paymentMap[payment._id] = payment;
|
|
10
|
+
}
|
|
11
|
+
return queries.map((q) => paymentMap[q.orderPaymentId] ?? null);
|
|
12
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { UnchainedCore } from '@unchainedshop/core';
|
|
2
|
+
import type { OrderPosition } from '@unchainedshop/core-orders';
|
|
3
|
+
import DataLoader from 'dataloader';
|
|
4
|
+
declare const _default: (unchainedAPI: UnchainedCore) => DataLoader<{
|
|
5
|
+
orderId: string;
|
|
6
|
+
}, OrderPosition[], {
|
|
7
|
+
orderId: string;
|
|
8
|
+
}>;
|
|
9
|
+
export default _default;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import DataLoader from 'dataloader';
|
|
2
|
+
export default (unchainedAPI) => new DataLoader(async (queries) => {
|
|
3
|
+
const orderIds = [...new Set(queries.map((q) => q.orderId).filter(Boolean))];
|
|
4
|
+
const positions = await unchainedAPI.modules.orders.positions.findOrderPositions({
|
|
5
|
+
orderIds,
|
|
6
|
+
});
|
|
7
|
+
const positionsMap = {};
|
|
8
|
+
for (const position of positions) {
|
|
9
|
+
if (!positionsMap[position.orderId]) {
|
|
10
|
+
positionsMap[position.orderId] = [position];
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
positionsMap[position.orderId].push(position);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return queries.map((q) => positionsMap[q.orderId] || []);
|
|
17
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { UnchainedCore } from '@unchainedshop/core';
|
|
2
|
+
import type { TokenSurrogate, TokenStatus as TokenStatusType } from '@unchainedshop/core-warehousing';
|
|
3
|
+
import DataLoader from 'dataloader';
|
|
4
|
+
declare const _default: (unchainedAPI: UnchainedCore) => DataLoader<{
|
|
5
|
+
token: TokenSurrogate;
|
|
6
|
+
}, TokenStatusType, {
|
|
7
|
+
token: TokenSurrogate;
|
|
8
|
+
}>;
|
|
9
|
+
export default _default;
|