@bleedingdev/modern-js-plugin-bff 3.2.0-ultramodern.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +26 -0
- package/cli.js +1 -0
- package/dist/cjs/cli.js +294 -0
- package/dist/cjs/constants.js +48 -0
- package/dist/cjs/index.js +58 -0
- package/dist/cjs/loader.js +106 -0
- package/dist/cjs/runtime/create-request/index.js +48 -0
- package/dist/cjs/runtime/data-platform/index.js +693 -0
- package/dist/cjs/runtime/effect/adapter.js +311 -0
- package/dist/cjs/runtime/effect/context.js +48 -0
- package/dist/cjs/runtime/effect/index.js +608 -0
- package/dist/cjs/runtime/effect-client/index.js +178 -0
- package/dist/cjs/runtime/hono/adapter.js +168 -0
- package/dist/cjs/runtime/hono/index.js +65 -0
- package/dist/cjs/runtime/hono/operators.js +68 -0
- package/dist/cjs/server.js +179 -0
- package/dist/cjs/utils/clientGenerator.js +342 -0
- package/dist/cjs/utils/createHonoRoutes.js +138 -0
- package/dist/cjs/utils/crossProjectApiPlugin.js +118 -0
- package/dist/cjs/utils/effectClientGenerator.js +673 -0
- package/dist/cjs/utils/pluginGenerator.js +73 -0
- package/dist/cjs/utils/runtimeGenerator.js +133 -0
- package/dist/esm/cli.mjs +245 -0
- package/dist/esm/constants.mjs +11 -0
- package/dist/esm/index.mjs +1 -0
- package/dist/esm/loader.mjs +62 -0
- package/dist/esm/runtime/create-request/index.mjs +1 -0
- package/dist/esm/runtime/data-platform/index.mjs +599 -0
- package/dist/esm/runtime/effect/adapter.mjs +267 -0
- package/dist/esm/runtime/effect/context.mjs +11 -0
- package/dist/esm/runtime/effect/index.mjs +438 -0
- package/dist/esm/runtime/effect-client/index.mjs +90 -0
- package/dist/esm/runtime/hono/adapter.mjs +124 -0
- package/dist/esm/runtime/hono/index.mjs +2 -0
- package/dist/esm/runtime/hono/operators.mjs +31 -0
- package/dist/esm/server.mjs +135 -0
- package/dist/esm/utils/clientGenerator.mjs +293 -0
- package/dist/esm/utils/createHonoRoutes.mjs +92 -0
- package/dist/esm/utils/crossProjectApiPlugin.mjs +54 -0
- package/dist/esm/utils/effectClientGenerator.mjs +623 -0
- package/dist/esm/utils/pluginGenerator.mjs +29 -0
- package/dist/esm/utils/runtimeGenerator.mjs +89 -0
- package/dist/esm-node/cli.mjs +249 -0
- package/dist/esm-node/constants.mjs +12 -0
- package/dist/esm-node/index.mjs +2 -0
- package/dist/esm-node/loader.mjs +64 -0
- package/dist/esm-node/runtime/create-request/index.mjs +2 -0
- package/dist/esm-node/runtime/data-platform/index.mjs +600 -0
- package/dist/esm-node/runtime/effect/adapter.mjs +269 -0
- package/dist/esm-node/runtime/effect/context.mjs +12 -0
- package/dist/esm-node/runtime/effect/index.mjs +439 -0
- package/dist/esm-node/runtime/effect-client/index.mjs +91 -0
- package/dist/esm-node/runtime/hono/adapter.mjs +125 -0
- package/dist/esm-node/runtime/hono/index.mjs +3 -0
- package/dist/esm-node/runtime/hono/operators.mjs +32 -0
- package/dist/esm-node/server.mjs +136 -0
- package/dist/esm-node/utils/clientGenerator.mjs +294 -0
- package/dist/esm-node/utils/createHonoRoutes.mjs +93 -0
- package/dist/esm-node/utils/crossProjectApiPlugin.mjs +55 -0
- package/dist/esm-node/utils/effectClientGenerator.mjs +625 -0
- package/dist/esm-node/utils/pluginGenerator.mjs +33 -0
- package/dist/esm-node/utils/runtimeGenerator.mjs +91 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/constants.d.ts +2 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/loader.d.ts +27 -0
- package/dist/types/runtime/create-request/index.d.ts +2 -0
- package/dist/types/runtime/data-platform/index.d.ts +187 -0
- package/dist/types/runtime/effect/adapter.d.ts +22 -0
- package/dist/types/runtime/effect/context.d.ts +8 -0
- package/dist/types/runtime/effect/index.d.ts +171 -0
- package/dist/types/runtime/effect-client/index.d.ts +47 -0
- package/dist/types/runtime/hono/adapter.d.ts +19 -0
- package/dist/types/runtime/hono/index.d.ts +2 -0
- package/dist/types/runtime/hono/operators.d.ts +10 -0
- package/dist/types/server.d.ts +3 -0
- package/dist/types/utils/clientGenerator.d.ts +37 -0
- package/dist/types/utils/createHonoRoutes.d.ts +10 -0
- package/dist/types/utils/crossProjectApiPlugin.d.ts +9 -0
- package/dist/types/utils/effectClientGenerator.d.ts +27 -0
- package/dist/types/utils/pluginGenerator.d.ts +9 -0
- package/dist/types/utils/runtimeGenerator.d.ts +7 -0
- package/docs/data-platform-architecture.md +61 -0
- package/package.json +172 -0
- package/rslib.config.mts +4 -0
- package/rstest.config.mts +10 -0
- package/server.js +1 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import * as __rspack_external_effect_Layer_16f7a8fc from "effect/Layer";
|
|
2
|
+
import { HttpRouter, HttpServerResponse, HttpTraceContext } from "effect/unstable/http";
|
|
3
|
+
import { HttpApiBuilder, OpenApi } from "effect/unstable/httpapi";
|
|
4
|
+
import { RpcSerialization, RpcServer } from "effect/unstable/rpc";
|
|
5
|
+
import { DEFAULT_DATA_BATCH_ENDPOINT, DEFAULT_DATA_BATCH_HEADER, DEFAULT_DATA_ENVELOPE_HEADER, decodeRequestEnvelopeHeader, validateRequestEnvelope, validateSelectionPlan } from "../data-platform/index.mjs";
|
|
6
|
+
import * as __rspack_external__effect_opentelemetry_8bbbb5af from "@effect/opentelemetry";
|
|
7
|
+
import * as __rspack_external_effect_Config_29be8a92 from "effect/Config";
|
|
8
|
+
import * as __rspack_external_effect_Effect_194ac36c from "effect/Effect";
|
|
9
|
+
import * as __rspack_external_effect_Option_4d691636 from "effect/Option";
|
|
10
|
+
import * as __rspack_external_effect_Schema_f8472650 from "effect/Schema";
|
|
11
|
+
export * from "effect/unstable/http";
|
|
12
|
+
export * from "effect/unstable/httpapi";
|
|
13
|
+
export * from "effect/unstable/rpc";
|
|
14
|
+
function normalizeOpenApiPath(pathname) {
|
|
15
|
+
if (!pathname.startsWith('/')) return `/${pathname}`;
|
|
16
|
+
return pathname;
|
|
17
|
+
}
|
|
18
|
+
function getOpenApiOptions(openapi) {
|
|
19
|
+
if (!openapi || 'object' != typeof openapi) return;
|
|
20
|
+
if (!openapi.path) return;
|
|
21
|
+
return {
|
|
22
|
+
path: normalizeOpenApiPath(openapi.path)
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function normalizeRpcPath(pathname) {
|
|
26
|
+
if (!pathname || '/' === pathname) return '/rpc';
|
|
27
|
+
if (!pathname.startsWith('/')) return `/${pathname}`;
|
|
28
|
+
return pathname;
|
|
29
|
+
}
|
|
30
|
+
function normalizeBatchPath(pathname) {
|
|
31
|
+
if (!pathname || '/' === pathname) return DEFAULT_DATA_BATCH_ENDPOINT;
|
|
32
|
+
if (!pathname.startsWith('/')) return `/${pathname}`;
|
|
33
|
+
return pathname;
|
|
34
|
+
}
|
|
35
|
+
function isPlainObject(value) {
|
|
36
|
+
return 'object' == typeof value && null !== value && !Array.isArray(value);
|
|
37
|
+
}
|
|
38
|
+
function toTextLength(value) {
|
|
39
|
+
if ("u" > typeof TextEncoder) return new TextEncoder().encode(value).length;
|
|
40
|
+
if ("u" > typeof Buffer) return Buffer.byteLength(value);
|
|
41
|
+
return value.length;
|
|
42
|
+
}
|
|
43
|
+
function toHeaderRecord(headers) {
|
|
44
|
+
const record = {};
|
|
45
|
+
headers.forEach((value, key)=>{
|
|
46
|
+
record[key] = value;
|
|
47
|
+
});
|
|
48
|
+
return record;
|
|
49
|
+
}
|
|
50
|
+
function normalizeItemMethod(method) {
|
|
51
|
+
return (method || 'GET').toUpperCase();
|
|
52
|
+
}
|
|
53
|
+
function normalizeBatchAllowedMethods(allowedMethods) {
|
|
54
|
+
const source = Array.isArray(allowedMethods) && allowedMethods.length > 0 ? allowedMethods : [
|
|
55
|
+
'GET'
|
|
56
|
+
];
|
|
57
|
+
return new Set(source.map((method)=>method.toUpperCase()));
|
|
58
|
+
}
|
|
59
|
+
function isBatchRequestPayload(value) {
|
|
60
|
+
return isPlainObject(value) && 1 === value.protocolVersion && 'string' == typeof value.batchId && 'number' == typeof value.sentAt && Array.isArray(value.items);
|
|
61
|
+
}
|
|
62
|
+
function createBatchValidationResponse(message, status = 400) {
|
|
63
|
+
return new Response(JSON.stringify({
|
|
64
|
+
message
|
|
65
|
+
}), {
|
|
66
|
+
status,
|
|
67
|
+
headers: {
|
|
68
|
+
'content-type': 'application/json; charset=utf-8'
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function toBatchItemError(id, status, message) {
|
|
73
|
+
return {
|
|
74
|
+
id,
|
|
75
|
+
status,
|
|
76
|
+
headers: {
|
|
77
|
+
'content-type': 'application/json; charset=utf-8'
|
|
78
|
+
},
|
|
79
|
+
body: JSON.stringify({
|
|
80
|
+
message
|
|
81
|
+
})
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function promiseWithTimeout(effect, timeoutMs) {
|
|
85
|
+
if (timeoutMs <= 0) return effect;
|
|
86
|
+
return new Promise((resolve, reject)=>{
|
|
87
|
+
const timer = setTimeout(()=>{
|
|
88
|
+
reject(new Error(`Batch item timeout after ${String(timeoutMs)}ms`));
|
|
89
|
+
}, timeoutMs);
|
|
90
|
+
effect.then((value)=>{
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
resolve(value);
|
|
93
|
+
}, (error)=>{
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
reject(error);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
async function mapWithConcurrency(items, concurrency, mapper) {
|
|
100
|
+
if (0 === items.length) return [];
|
|
101
|
+
const normalizedConcurrency = Math.max(1, concurrency);
|
|
102
|
+
const output = new Array(items.length);
|
|
103
|
+
let index = 0;
|
|
104
|
+
const workers = Array.from({
|
|
105
|
+
length: Math.min(normalizedConcurrency, items.length)
|
|
106
|
+
}, async ()=>{
|
|
107
|
+
while(true){
|
|
108
|
+
const currentIndex = index;
|
|
109
|
+
index += 1;
|
|
110
|
+
if (currentIndex >= items.length) return;
|
|
111
|
+
output[currentIndex] = await mapper(items[currentIndex], currentIndex);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
await Promise.all(workers);
|
|
115
|
+
return output;
|
|
116
|
+
}
|
|
117
|
+
function getRequestPathname(request) {
|
|
118
|
+
try {
|
|
119
|
+
return new URL(request.url).pathname;
|
|
120
|
+
} catch {
|
|
121
|
+
return new URL(request.url, 'http://localhost').pathname;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function normalizeMountPrefix(prefix) {
|
|
125
|
+
if (!prefix || '/' === prefix) return '';
|
|
126
|
+
return prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
|
|
127
|
+
}
|
|
128
|
+
function getMountedPrefixFromContext(request, context) {
|
|
129
|
+
if (!isPlainObject(context) || 'string' != typeof context.path) return '';
|
|
130
|
+
const contextPath = normalizeMountPrefix(context.path);
|
|
131
|
+
const requestPath = normalizeMountPrefix(getRequestPathname(request));
|
|
132
|
+
if (!contextPath || !requestPath || contextPath === requestPath || !contextPath.endsWith(requestPath)) return '';
|
|
133
|
+
return normalizeMountPrefix(contextPath.slice(0, contextPath.length - requestPath.length));
|
|
134
|
+
}
|
|
135
|
+
function removeMountedPrefixFromBatchPath(pathWithQuery, prefix) {
|
|
136
|
+
const normalizedPrefix = normalizeMountPrefix(prefix);
|
|
137
|
+
if (!normalizedPrefix) return pathWithQuery;
|
|
138
|
+
const [pathname, ...queryParts] = pathWithQuery.split('?');
|
|
139
|
+
if (!pathname) return pathWithQuery;
|
|
140
|
+
let nextPathname = pathname;
|
|
141
|
+
if (pathname === normalizedPrefix) nextPathname = '/';
|
|
142
|
+
else if (pathname.startsWith(`${normalizedPrefix}/`)) {
|
|
143
|
+
const sliced = pathname.slice(normalizedPrefix.length);
|
|
144
|
+
nextPathname = sliced.startsWith('/') ? sliced : `/${sliced}`;
|
|
145
|
+
}
|
|
146
|
+
if (0 === queryParts.length) return nextPathname;
|
|
147
|
+
return `${nextPathname}?${queryParts.join('?')}`;
|
|
148
|
+
}
|
|
149
|
+
function getRequestOrigin(request) {
|
|
150
|
+
try {
|
|
151
|
+
return new URL(request.url).origin;
|
|
152
|
+
} catch {
|
|
153
|
+
return new URL(request.url, 'http://localhost').origin;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function getExpectedEnvelopeOrigin(request) {
|
|
157
|
+
const origin = request.headers.get('origin');
|
|
158
|
+
if (origin && 'null' !== origin) return origin;
|
|
159
|
+
return getRequestOrigin(request);
|
|
160
|
+
}
|
|
161
|
+
function isRpcRequest(request, rpcPath) {
|
|
162
|
+
const pathname = getRequestPathname(request);
|
|
163
|
+
return pathname === rpcPath || pathname.startsWith(`${rpcPath}/`);
|
|
164
|
+
}
|
|
165
|
+
function getRpcSerializationLayer(serialization) {
|
|
166
|
+
switch(serialization){
|
|
167
|
+
case 'ndjson':
|
|
168
|
+
return RpcSerialization.layerNdjson;
|
|
169
|
+
case 'jsonRpc':
|
|
170
|
+
return RpcSerialization.layerJsonRpc();
|
|
171
|
+
case 'ndJsonRpc':
|
|
172
|
+
return RpcSerialization.layerNdJsonRpc();
|
|
173
|
+
case 'msgPack':
|
|
174
|
+
return RpcSerialization.layerMsgPack;
|
|
175
|
+
default:
|
|
176
|
+
return RpcSerialization.layerJsonRpc();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function createRpcApiHandler(options) {
|
|
180
|
+
const rpcPath = normalizeRpcPath(options.path);
|
|
181
|
+
const rpcLayer = __rspack_external_effect_Layer_16f7a8fc.provide(RpcServer.layerHttp({
|
|
182
|
+
group: options.group,
|
|
183
|
+
path: rpcPath,
|
|
184
|
+
protocol: 'http',
|
|
185
|
+
disableTracing: options.disableTracing,
|
|
186
|
+
spanPrefix: options.spanPrefix,
|
|
187
|
+
spanAttributes: options.spanAttributes
|
|
188
|
+
}), __rspack_external_effect_Layer_16f7a8fc.mergeAll(options.layer, getRpcSerializationLayer(options.serialization)));
|
|
189
|
+
return HttpRouter.toWebHandler(rpcLayer);
|
|
190
|
+
}
|
|
191
|
+
function createOpenApiLayer(api, openapi) {
|
|
192
|
+
const openApiOptions = getOpenApiOptions(openapi);
|
|
193
|
+
if (!openApiOptions) return null;
|
|
194
|
+
return HttpRouter.add('GET', openApiOptions.path, HttpServerResponse.jsonUnsafe(OpenApi.fromApi(api)));
|
|
195
|
+
}
|
|
196
|
+
function createInvalidEnvelopeResponse(message, errors) {
|
|
197
|
+
return new Response(JSON.stringify({
|
|
198
|
+
message,
|
|
199
|
+
...errors && errors.length > 0 ? {
|
|
200
|
+
errors
|
|
201
|
+
} : {}
|
|
202
|
+
}), {
|
|
203
|
+
status: 400,
|
|
204
|
+
headers: {
|
|
205
|
+
'content-type': 'application/json; charset=utf-8'
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
function validateDataPlatformRequestEnvelope(request, options) {
|
|
210
|
+
const isEnabled = options?.enabled ?? true;
|
|
211
|
+
if (!isEnabled) return null;
|
|
212
|
+
const envelopeHeader = options?.envelopeHeader || DEFAULT_DATA_ENVELOPE_HEADER;
|
|
213
|
+
const encodedEnvelope = request.headers.get(envelopeHeader);
|
|
214
|
+
if (!encodedEnvelope) {
|
|
215
|
+
if (options?.requireEnvelope) return createInvalidEnvelopeResponse(`Missing required data envelope header: ${envelopeHeader}`);
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
const envelope = decodeRequestEnvelopeHeader(encodedEnvelope);
|
|
219
|
+
if (!envelope) return createInvalidEnvelopeResponse(`Invalid data envelope header format: ${envelopeHeader}`);
|
|
220
|
+
const validation = validateRequestEnvelope(envelope, {
|
|
221
|
+
expectedProtocolVersion: 1,
|
|
222
|
+
expectedNamespace: options?.expectedNamespace,
|
|
223
|
+
expectedOrigin: options?.validateOrigin === false ? void 0 : getExpectedEnvelopeOrigin(request),
|
|
224
|
+
requireTraceContext: options?.requireTraceContext
|
|
225
|
+
});
|
|
226
|
+
if (!validation.ok) return createInvalidEnvelopeResponse('Invalid data envelope', validation.errors);
|
|
227
|
+
if (envelope.selectionPlan) {
|
|
228
|
+
const selectionValidation = validateSelectionPlan(envelope.selectionPlan, {
|
|
229
|
+
maxDepth: options?.selection?.maxDepth,
|
|
230
|
+
maxFields: options?.selection?.maxFields,
|
|
231
|
+
allowedLeafPaths: options?.selection?.allowedLeafPaths
|
|
232
|
+
});
|
|
233
|
+
if (!selectionValidation.ok) return createInvalidEnvelopeResponse('Invalid data envelope selection plan', selectionValidation.errors);
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
function mergeDataPlatformOptions(base, override) {
|
|
238
|
+
if (!base && !override) return;
|
|
239
|
+
const baseSelection = base?.selection;
|
|
240
|
+
const overrideSelection = override?.selection;
|
|
241
|
+
const baseBatch = base?.batch;
|
|
242
|
+
const overrideBatch = override?.batch;
|
|
243
|
+
return {
|
|
244
|
+
...base,
|
|
245
|
+
...override,
|
|
246
|
+
selection: baseSelection || overrideSelection ? {
|
|
247
|
+
...baseSelection,
|
|
248
|
+
...overrideSelection
|
|
249
|
+
} : void 0,
|
|
250
|
+
batch: baseBatch || overrideBatch ? {
|
|
251
|
+
...baseBatch,
|
|
252
|
+
...overrideBatch
|
|
253
|
+
} : void 0
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function defineEffectBff(definition) {
|
|
257
|
+
const createHandler = (options)=>{
|
|
258
|
+
const rpcDefinition = definition.rpc;
|
|
259
|
+
let mergedRpcOptions = rpcDefinition;
|
|
260
|
+
if (rpcDefinition && options?.rpc) mergedRpcOptions = {
|
|
261
|
+
...rpcDefinition,
|
|
262
|
+
...options.rpc
|
|
263
|
+
};
|
|
264
|
+
return createHttpApiHandler({
|
|
265
|
+
api: definition.api,
|
|
266
|
+
layer: definition.layer,
|
|
267
|
+
openapi: options?.openapi,
|
|
268
|
+
rpc: mergedRpcOptions,
|
|
269
|
+
dataPlatform: mergeDataPlatformOptions(definition.dataPlatform, options?.dataPlatform)
|
|
270
|
+
});
|
|
271
|
+
};
|
|
272
|
+
const client = void 0;
|
|
273
|
+
return {
|
|
274
|
+
...definition,
|
|
275
|
+
createHandler,
|
|
276
|
+
client
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function defineEffectRpcBff(definition) {
|
|
280
|
+
const createHandler = (options)=>createRpcApiHandler({
|
|
281
|
+
...definition,
|
|
282
|
+
...options
|
|
283
|
+
});
|
|
284
|
+
return {
|
|
285
|
+
...definition,
|
|
286
|
+
createHandler
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function createHttpApiHandler(options) {
|
|
290
|
+
const apiLayer = options.layer;
|
|
291
|
+
const openApiLayer = createOpenApiLayer(options.api, options.openapi);
|
|
292
|
+
const mergedLayer = openApiLayer ? __rspack_external_effect_Layer_16f7a8fc.mergeAll(apiLayer, openApiLayer) : apiLayer;
|
|
293
|
+
const httpApiHandler = HttpRouter.toWebHandler(mergedLayer);
|
|
294
|
+
const dataPlatformBatchOptions = options.dataPlatform?.batch;
|
|
295
|
+
const batchEnabled = dataPlatformBatchOptions?.enabled !== false;
|
|
296
|
+
const batchPath = normalizeBatchPath(dataPlatformBatchOptions?.endpoint);
|
|
297
|
+
const batchMaxSize = Math.max(1, dataPlatformBatchOptions?.maxBatchSize ?? 16);
|
|
298
|
+
const batchMaxBytes = Math.max(1024, dataPlatformBatchOptions?.maxBatchBytes ?? 65536);
|
|
299
|
+
const batchConcurrency = Math.max(1, dataPlatformBatchOptions?.maxConcurrency ?? 4);
|
|
300
|
+
const batchItemTimeoutMs = Math.max(0, dataPlatformBatchOptions?.requestTimeoutMs ?? 10000);
|
|
301
|
+
const batchAllowedMethods = normalizeBatchAllowedMethods(dataPlatformBatchOptions?.allowedMethods);
|
|
302
|
+
const envelopeHeader = options.dataPlatform?.envelopeHeader || DEFAULT_DATA_ENVELOPE_HEADER;
|
|
303
|
+
const normalizedEnvelopeHeader = envelopeHeader.toLowerCase();
|
|
304
|
+
const withDataPlatformValidation = async (request, context)=>{
|
|
305
|
+
const validationError = validateDataPlatformRequestEnvelope(request, options.dataPlatform);
|
|
306
|
+
if (validationError) return validationError;
|
|
307
|
+
return httpApiHandler.handler(request, context);
|
|
308
|
+
};
|
|
309
|
+
const handleBatchRequest = async (request, context)=>{
|
|
310
|
+
const mountedPrefix = getMountedPrefixFromContext(request, context);
|
|
311
|
+
const method = normalizeItemMethod(request.method);
|
|
312
|
+
if ('POST' !== method) return createBatchValidationResponse('Batch endpoint only supports POST requests', 405);
|
|
313
|
+
const contentType = request.headers.get('content-type') || '';
|
|
314
|
+
if (!contentType.includes('application/json')) return createBatchValidationResponse('Batch endpoint requires application/json content-type', 415);
|
|
315
|
+
const payloadText = await request.text();
|
|
316
|
+
if (toTextLength(payloadText) > batchMaxBytes) return createBatchValidationResponse(`Batch payload exceeds max size (${String(batchMaxBytes)} bytes)`, 413);
|
|
317
|
+
let payload;
|
|
318
|
+
try {
|
|
319
|
+
payload = JSON.parse(payloadText);
|
|
320
|
+
} catch {
|
|
321
|
+
return createBatchValidationResponse('Invalid batch payload JSON');
|
|
322
|
+
}
|
|
323
|
+
if (!isBatchRequestPayload(payload)) return createBatchValidationResponse('Invalid batch payload shape');
|
|
324
|
+
if (0 === payload.items.length) return createBatchValidationResponse('Batch payload items cannot be empty');
|
|
325
|
+
if (payload.items.length > batchMaxSize) return createBatchValidationResponse(`Batch item count exceeds max size (${String(batchMaxSize)})`, 413);
|
|
326
|
+
const responseItems = await mapWithConcurrency(payload.items, batchConcurrency, async (rawItem, index)=>{
|
|
327
|
+
const fallbackId = `item_${String(index)}`;
|
|
328
|
+
const itemId = isPlainObject(rawItem) && 'string' == typeof rawItem.id ? rawItem.id : fallbackId;
|
|
329
|
+
if (!isPlainObject(rawItem)) return toBatchItemError(itemId, 400, 'Invalid batch item; expected object');
|
|
330
|
+
if ('string' != typeof rawItem.path || 0 === rawItem.path.length) return toBatchItemError(itemId, 400, 'Invalid batch item path');
|
|
331
|
+
if (!rawItem.path.startsWith('/')) return toBatchItemError(itemId, 400, 'Batch item path must start with "/"');
|
|
332
|
+
const normalizedItemPath = removeMountedPrefixFromBatchPath(rawItem.path, mountedPrefix);
|
|
333
|
+
const itemPathname = normalizedItemPath.split('?')[0] || normalizedItemPath;
|
|
334
|
+
if (itemPathname === batchPath || itemPathname.startsWith(`${batchPath}/`)) return toBatchItemError(itemId, 400, 'Batch item path cannot target batch endpoint');
|
|
335
|
+
const itemMethod = normalizeItemMethod('string' == typeof rawItem.method ? rawItem.method : void 0);
|
|
336
|
+
if (!batchAllowedMethods.has(itemMethod)) return toBatchItemError(itemId, 405, `Batch item method ${itemMethod} is not allowed`);
|
|
337
|
+
if (void 0 !== rawItem.body && null !== rawItem.body && 'string' != typeof rawItem.body) return toBatchItemError(itemId, 400, 'Batch item body must be a string when provided');
|
|
338
|
+
if (('GET' === itemMethod || 'HEAD' === itemMethod) && 'string' == typeof rawItem.body) return toBatchItemError(itemId, 400, `${itemMethod} batch item cannot include body`);
|
|
339
|
+
const normalizedHeaders = {};
|
|
340
|
+
if (void 0 !== rawItem.headers) {
|
|
341
|
+
if (!isPlainObject(rawItem.headers)) return toBatchItemError(itemId, 400, 'Batch item headers must be an object');
|
|
342
|
+
for (const [key, value] of Object.entries(rawItem.headers)){
|
|
343
|
+
if ('string' != typeof value) return toBatchItemError(itemId, 400, `Invalid header "${key}" for batch item`);
|
|
344
|
+
normalizedHeaders[key.toLowerCase()] = value;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (!normalizedHeaders.traceparent) {
|
|
348
|
+
const encodedEnvelope = normalizedHeaders[normalizedEnvelopeHeader];
|
|
349
|
+
if ('string' == typeof encodedEnvelope) {
|
|
350
|
+
const envelope = decodeRequestEnvelopeHeader(encodedEnvelope);
|
|
351
|
+
if (envelope?.traceparent) normalizedHeaders.traceparent = envelope.traceparent;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (!normalizedHeaders.traceparent) {
|
|
355
|
+
const requestTraceparent = request.headers.get('traceparent');
|
|
356
|
+
if (requestTraceparent) normalizedHeaders.traceparent = requestTraceparent;
|
|
357
|
+
}
|
|
358
|
+
const targetUrl = new URL(normalizedItemPath, request.url);
|
|
359
|
+
const requestHeaders = new Headers(normalizedHeaders);
|
|
360
|
+
const body = 'GET' === itemMethod || 'HEAD' === itemMethod ? void 0 : rawItem.body;
|
|
361
|
+
if (void 0 === body) requestHeaders.delete('content-type');
|
|
362
|
+
const itemRequest = new Request(targetUrl.toString(), {
|
|
363
|
+
method: itemMethod,
|
|
364
|
+
headers: requestHeaders,
|
|
365
|
+
body
|
|
366
|
+
});
|
|
367
|
+
try {
|
|
368
|
+
const itemResponse = await promiseWithTimeout(withDataPlatformValidation(itemRequest, context), batchItemTimeoutMs);
|
|
369
|
+
if (!(itemResponse instanceof Response)) return toBatchItemError(itemId, 500, 'Invalid response returned by batch item handler');
|
|
370
|
+
const bodyText = await itemResponse.text();
|
|
371
|
+
const responseItem = {
|
|
372
|
+
id: itemId,
|
|
373
|
+
status: itemResponse.status,
|
|
374
|
+
headers: toHeaderRecord(itemResponse.headers),
|
|
375
|
+
...bodyText ? {
|
|
376
|
+
body: bodyText
|
|
377
|
+
} : {}
|
|
378
|
+
};
|
|
379
|
+
return responseItem;
|
|
380
|
+
} catch (error) {
|
|
381
|
+
if (error instanceof Response) {
|
|
382
|
+
const bodyText = await error.text();
|
|
383
|
+
return {
|
|
384
|
+
id: itemId,
|
|
385
|
+
status: error.status,
|
|
386
|
+
headers: toHeaderRecord(error.headers),
|
|
387
|
+
...bodyText ? {
|
|
388
|
+
body: bodyText
|
|
389
|
+
} : {}
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
393
|
+
return toBatchItemError(itemId, 500, message);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
const responsePayload = {
|
|
397
|
+
protocolVersion: 1,
|
|
398
|
+
batchId: payload.batchId,
|
|
399
|
+
receivedAt: Date.now(),
|
|
400
|
+
items: responseItems
|
|
401
|
+
};
|
|
402
|
+
return new Response(JSON.stringify(responsePayload), {
|
|
403
|
+
status: 200,
|
|
404
|
+
headers: {
|
|
405
|
+
'content-type': 'application/json; charset=utf-8',
|
|
406
|
+
[DEFAULT_DATA_BATCH_HEADER]: '1',
|
|
407
|
+
'x-modernjs-data-batch-id': payload.batchId
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
};
|
|
411
|
+
const handleHttpApiRequest = async (request, context)=>{
|
|
412
|
+
const pathname = getRequestPathname(request);
|
|
413
|
+
if (batchEnabled && pathname === batchPath) return handleBatchRequest(request, context);
|
|
414
|
+
return withDataPlatformValidation(request, context);
|
|
415
|
+
};
|
|
416
|
+
if (!options.rpc) return {
|
|
417
|
+
handler: handleHttpApiRequest,
|
|
418
|
+
dispose: async ()=>{
|
|
419
|
+
await httpApiHandler.dispose();
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
const rpcPath = normalizeRpcPath(options.rpc.path);
|
|
423
|
+
const rpcHandler = createRpcApiHandler(options.rpc);
|
|
424
|
+
return {
|
|
425
|
+
handler: async (request, context)=>{
|
|
426
|
+
if (isRpcRequest(request, rpcPath)) return rpcHandler.handler(request, context);
|
|
427
|
+
return handleHttpApiRequest(request);
|
|
428
|
+
},
|
|
429
|
+
dispose: async ()=>{
|
|
430
|
+
await Promise.all([
|
|
431
|
+
httpApiHandler.dispose(),
|
|
432
|
+
rpcHandler.dispose()
|
|
433
|
+
]);
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
export { useEffectContext } from "./context.mjs";
|
|
438
|
+
export { HttpApiBuilder, HttpTraceContext, __rspack_external__effect_opentelemetry_8bbbb5af as OpenTelemetry, __rspack_external_effect_Config_29be8a92 as Config, __rspack_external_effect_Effect_194ac36c as Effect, __rspack_external_effect_Layer_16f7a8fc as Layer, __rspack_external_effect_Option_4d691636 as Option, __rspack_external_effect_Schema_f8472650 as Schema, createHttpApiHandler, defineEffectBff, defineEffectRpcBff };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import * as __rspack_external_effect_Effect_194ac36c from "effect/Effect";
|
|
2
|
+
import * as __rspack_external_effect_Layer_16f7a8fc from "effect/Layer";
|
|
3
|
+
import { FetchHttpClient } from "effect/unstable/http";
|
|
4
|
+
import { HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/unstable/httpapi";
|
|
5
|
+
import { Rpc, RpcClient, RpcGroup, RpcSchema, RpcSerialization } from "effect/unstable/rpc";
|
|
6
|
+
import * as __rspack_external_effect_Schema_f8472650 from "effect/Schema";
|
|
7
|
+
import * as __rspack_external_effect_Exit_a10cfb96 from "effect/Exit";
|
|
8
|
+
import * as __rspack_external_effect_ManagedRuntime_94dd2709 from "effect/ManagedRuntime";
|
|
9
|
+
import * as __rspack_external_effect_Scope_3211a2d6 from "effect/Scope";
|
|
10
|
+
function isRecord(value) {
|
|
11
|
+
return 'object' == typeof value && null !== value && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
function applySelection(value, selection) {
|
|
14
|
+
if (true === selection || void 0 === selection) return value;
|
|
15
|
+
if (Array.isArray(value)) return value.map((item)=>applySelection(item, selection));
|
|
16
|
+
if (!isRecord(value) || !isRecord(selection)) return value;
|
|
17
|
+
const masked = {};
|
|
18
|
+
for (const key of Object.keys(selection))if (key in value) masked[key] = applySelection(value[key], selection[key]);
|
|
19
|
+
return masked;
|
|
20
|
+
}
|
|
21
|
+
function view() {
|
|
22
|
+
return (selection)=>selection;
|
|
23
|
+
}
|
|
24
|
+
function mask(value, selection) {
|
|
25
|
+
return applySelection(value, selection);
|
|
26
|
+
}
|
|
27
|
+
function runEffectView(request, selection) {
|
|
28
|
+
return new Promise((resolve, reject)=>{
|
|
29
|
+
request.then((value)=>resolve(mask(value, selection)), (reason)=>reject(reason));
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function getRpcSerializationLayer(serialization) {
|
|
33
|
+
switch(serialization){
|
|
34
|
+
case 'ndjson':
|
|
35
|
+
return RpcSerialization.layerNdjson;
|
|
36
|
+
case 'jsonRpc':
|
|
37
|
+
return RpcSerialization.layerJsonRpc();
|
|
38
|
+
case 'ndJsonRpc':
|
|
39
|
+
return RpcSerialization.layerNdJsonRpc();
|
|
40
|
+
case 'msgPack':
|
|
41
|
+
return RpcSerialization.layerMsgPack;
|
|
42
|
+
default:
|
|
43
|
+
return RpcSerialization.layerJsonRpc();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function makeEffectHttpApiClient(api, options) {
|
|
47
|
+
return HttpApiClient.make(api, {
|
|
48
|
+
baseUrl: options?.baseUrl
|
|
49
|
+
}).pipe(__rspack_external_effect_Effect_194ac36c.provide(FetchHttpClient.layer));
|
|
50
|
+
}
|
|
51
|
+
function makeEffectRpcClient(group, options) {
|
|
52
|
+
const protocolLayer = __rspack_external_effect_Layer_16f7a8fc.provide(RpcClient.layerProtocolHttp({
|
|
53
|
+
url: options.url
|
|
54
|
+
}), __rspack_external_effect_Layer_16f7a8fc.mergeAll(getRpcSerializationLayer(options.serialization), FetchHttpClient.layer));
|
|
55
|
+
const middlewareLayer = options.middlewareLayer ?? __rspack_external_effect_Layer_16f7a8fc.empty;
|
|
56
|
+
const runtimeLayer = __rspack_external_effect_Layer_16f7a8fc.mergeAll(protocolLayer, middlewareLayer);
|
|
57
|
+
return __rspack_external_effect_Effect_194ac36c.tryPromise({
|
|
58
|
+
try: async ()=>{
|
|
59
|
+
const runtime = __rspack_external_effect_ManagedRuntime_94dd2709.make(runtimeLayer);
|
|
60
|
+
const scope = await runtime.runPromise(__rspack_external_effect_Scope_3211a2d6.make());
|
|
61
|
+
try {
|
|
62
|
+
const client = await runtime.runPromise(RpcClient.make(group, {
|
|
63
|
+
flatten: options.flatten
|
|
64
|
+
}).pipe(__rspack_external_effect_Effect_194ac36c.provideService(__rspack_external_effect_Scope_3211a2d6.Scope, scope)));
|
|
65
|
+
let disposed = false;
|
|
66
|
+
const clientWithDispose = Object.assign(client, {
|
|
67
|
+
dispose: async ()=>{
|
|
68
|
+
if (!disposed) {
|
|
69
|
+
disposed = true;
|
|
70
|
+
await runtime.runPromise(__rspack_external_effect_Scope_3211a2d6.close(scope, __rspack_external_effect_Exit_a10cfb96["void"]));
|
|
71
|
+
}
|
|
72
|
+
await runtime.dispose();
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
return clientWithDispose;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
try {
|
|
78
|
+
await runtime.runPromise(__rspack_external_effect_Scope_3211a2d6.close(scope, __rspack_external_effect_Exit_a10cfb96["void"]));
|
|
79
|
+
} catch {}
|
|
80
|
+
try {
|
|
81
|
+
await runtime.dispose();
|
|
82
|
+
} catch {}
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
catch: (error)=>error instanceof Error ? error : new Error(String(error))
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const runEffectRequest = __rspack_external_effect_Effect_194ac36c.runPromise;
|
|
90
|
+
export { HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, Rpc, RpcClient, RpcGroup, RpcSchema, RpcSerialization, __rspack_external_effect_Effect_194ac36c as Effect, __rspack_external_effect_Layer_16f7a8fc as Layer, __rspack_external_effect_Schema_f8472650 as Schema, makeEffectHttpApiClient, makeEffectRpcClient, mask, runEffectRequest, runEffectView, view };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Hono, run } from "@modern-js/server-core";
|
|
2
|
+
import { isProd, logger } from "@modern-js/utils";
|
|
3
|
+
import createHonoRoutes from "../../utils/createHonoRoutes.mjs";
|
|
4
|
+
const before = [
|
|
5
|
+
'custom-server-hook',
|
|
6
|
+
'custom-server-middleware',
|
|
7
|
+
'render'
|
|
8
|
+
];
|
|
9
|
+
const kParentHonoVars = Symbol.for('modernjs.hono.parentVars');
|
|
10
|
+
class HonoAdapter {
|
|
11
|
+
wrapInArray(handler) {
|
|
12
|
+
if (Array.isArray(handler)) return handler;
|
|
13
|
+
return [
|
|
14
|
+
handler
|
|
15
|
+
];
|
|
16
|
+
}
|
|
17
|
+
constructor(api){
|
|
18
|
+
this.apiMiddleware = [];
|
|
19
|
+
this.apiServer = null;
|
|
20
|
+
this.isHono = true;
|
|
21
|
+
this.setHandlers = async ()=>{
|
|
22
|
+
if (!this.isHono) return;
|
|
23
|
+
const { apiHandlerInfos } = this.api.getServerContext();
|
|
24
|
+
const honoHandlers = createHonoRoutes(apiHandlerInfos);
|
|
25
|
+
this.apiMiddleware = honoHandlers.map(({ path, method, handler })=>({
|
|
26
|
+
name: 'hono-bff-api',
|
|
27
|
+
path,
|
|
28
|
+
method,
|
|
29
|
+
handler,
|
|
30
|
+
order: 'post',
|
|
31
|
+
before: before
|
|
32
|
+
}));
|
|
33
|
+
};
|
|
34
|
+
this.registerApiRoutes = async ()=>{
|
|
35
|
+
if (!this.isHono) return;
|
|
36
|
+
this.apiServer = new Hono();
|
|
37
|
+
this.apiServer.use('*', run);
|
|
38
|
+
this.apiServer.use('*', async (c, next)=>{
|
|
39
|
+
const nodeReq = c.env?.node?.req;
|
|
40
|
+
const parentVars = nodeReq?.[kParentHonoVars];
|
|
41
|
+
if (parentVars && 'object' == typeof parentVars) {
|
|
42
|
+
delete nodeReq[kParentHonoVars];
|
|
43
|
+
for (const [key, value] of Object.entries(parentVars))if (void 0 === c.get(key)) c.set(key, value);
|
|
44
|
+
}
|
|
45
|
+
await next();
|
|
46
|
+
});
|
|
47
|
+
this.apiMiddleware.forEach(({ path = '*', method = 'all', handler })=>{
|
|
48
|
+
const handlers = this.wrapInArray(handler);
|
|
49
|
+
if (0 === handlers.length) return;
|
|
50
|
+
const firstHandler = handlers[0];
|
|
51
|
+
const restHandlers = handlers.slice(1);
|
|
52
|
+
const m = method;
|
|
53
|
+
const server = this.apiServer;
|
|
54
|
+
if (!server) return;
|
|
55
|
+
const register = server[m];
|
|
56
|
+
register.call(server, path, firstHandler, ...restHandlers);
|
|
57
|
+
});
|
|
58
|
+
this.apiServer.onError(async (err, c)=>{
|
|
59
|
+
try {
|
|
60
|
+
const serverConfig = this.api.getServerConfig();
|
|
61
|
+
const onErrorHandler = serverConfig?.onError;
|
|
62
|
+
if (onErrorHandler) {
|
|
63
|
+
const result = await onErrorHandler(err, c);
|
|
64
|
+
if (result instanceof Response) return result;
|
|
65
|
+
} else logger.error(err);
|
|
66
|
+
} catch (configError) {
|
|
67
|
+
logger.error(`Error in serverConfig.onError handler: ${configError}`);
|
|
68
|
+
}
|
|
69
|
+
const status = 'object' == typeof err && null !== err && 'status' in err && 'number' == typeof err.status ? err.status : 500;
|
|
70
|
+
return new Response(JSON.stringify({
|
|
71
|
+
message: err instanceof Error ? err.message : '[BFF] Internal Server Error'
|
|
72
|
+
}), {
|
|
73
|
+
status,
|
|
74
|
+
headers: {
|
|
75
|
+
'content-type': 'application/json; charset=utf-8'
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
this.registerMiddleware = async (options)=>{
|
|
81
|
+
const { prefix } = options;
|
|
82
|
+
const { bffRuntimeFramework } = this.api.getServerContext();
|
|
83
|
+
if ('hono' !== bffRuntimeFramework) {
|
|
84
|
+
this.isHono = false;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const { middlewares: globalMiddlewares } = this.api.getServerContext();
|
|
88
|
+
await this.setHandlers();
|
|
89
|
+
if (isProd()) globalMiddlewares.push(...this.apiMiddleware);
|
|
90
|
+
else {
|
|
91
|
+
await this.registerApiRoutes();
|
|
92
|
+
const dynamicApiMiddleware = {
|
|
93
|
+
name: 'dynamic-bff-handler',
|
|
94
|
+
path: `${prefix}/*`,
|
|
95
|
+
method: 'all',
|
|
96
|
+
order: 'post',
|
|
97
|
+
before: before,
|
|
98
|
+
handler: async (c, next)=>{
|
|
99
|
+
if (this.apiServer) {
|
|
100
|
+
const nodeReq = c.env?.node?.req;
|
|
101
|
+
if (nodeReq) {
|
|
102
|
+
const parentVars = c?.var;
|
|
103
|
+
nodeReq[kParentHonoVars] = parentVars && 'object' == typeof parentVars ? {
|
|
104
|
+
...parentVars
|
|
105
|
+
} : parentVars;
|
|
106
|
+
}
|
|
107
|
+
const response = await this.apiServer.fetch(c.req.raw, c.env);
|
|
108
|
+
if (404 !== response.status) return new Response(response.body, response);
|
|
109
|
+
}
|
|
110
|
+
await next();
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
globalMiddlewares.push(dynamicApiMiddleware);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
this.onApiHandlersUpdated = async ()=>{
|
|
117
|
+
if (!this.isHono) return;
|
|
118
|
+
await this.setHandlers();
|
|
119
|
+
await this.registerApiRoutes();
|
|
120
|
+
};
|
|
121
|
+
this.api = api;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export { HonoAdapter };
|