@bleedingdev/modern-js-plugin-bff 3.2.0-ultramodern.12 → 3.2.0-ultramodern.121
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/dist/cjs/cli.js +9 -5
- package/dist/cjs/constants.js +13 -9
- package/dist/cjs/index.js +9 -5
- package/dist/cjs/loader.js +32 -5
- package/dist/cjs/runtime/create-request/index.js +9 -5
- package/dist/cjs/runtime/data-platform/index.js +50 -26
- package/dist/cjs/runtime/effect/adapter.js +99 -93
- package/dist/cjs/runtime/effect/context.js +19 -7
- package/dist/cjs/runtime/effect/edge.js +169 -0
- package/dist/cjs/runtime/effect/endpoint-contracts.js +130 -0
- package/dist/cjs/runtime/effect/handler.js +642 -0
- package/dist/cjs/runtime/effect/index.js +30 -547
- package/dist/cjs/runtime/effect/module.js +151 -0
- package/dist/cjs/runtime/effect/operation-context.js +103 -0
- package/dist/cjs/runtime/effect-client/index.js +22 -6
- package/dist/cjs/runtime/effect-client/runtime.js +266 -0
- package/dist/cjs/runtime/hono/adapter.js +30 -14
- package/dist/cjs/runtime/hono/index.js +9 -5
- package/dist/cjs/runtime/hono/operators.js +9 -5
- package/dist/cjs/runtime/safe-failure.js +83 -0
- package/dist/cjs/server.js +9 -5
- package/dist/cjs/utils/clientGenerator.js +13 -9
- package/dist/cjs/utils/createHonoRoutes.js +9 -5
- package/dist/cjs/utils/crossProjectApiPlugin.js +9 -5
- package/dist/cjs/utils/crossProjectServerPolicy.js +104 -0
- package/dist/cjs/utils/effectClientGenerator.js +116 -488
- package/dist/cjs/utils/pluginGenerator.js +9 -5
- package/dist/cjs/utils/runtimeGenerator.js +9 -5
- package/dist/esm/loader.mjs +23 -0
- package/dist/esm/runtime/data-platform/index.mjs +33 -22
- package/dist/esm/runtime/effect/adapter.mjs +91 -89
- package/dist/esm/runtime/effect/context.mjs +3 -1
- package/dist/esm/runtime/effect/edge.mjs +83 -0
- package/dist/esm/runtime/effect/endpoint-contracts.mjs +68 -0
- package/dist/esm/runtime/effect/handler.mjs +470 -0
- package/dist/esm/runtime/effect/index.mjs +3 -437
- package/dist/esm/runtime/effect/module.mjs +113 -0
- package/dist/esm/runtime/effect/operation-context.mjs +65 -0
- package/dist/esm/runtime/effect-client/index.mjs +14 -2
- package/dist/esm/runtime/effect-client/runtime.mjs +228 -0
- package/dist/esm/runtime/hono/adapter.mjs +21 -9
- package/dist/esm/runtime/safe-failure.mjs +45 -0
- package/dist/esm/utils/clientGenerator.mjs +5 -5
- package/dist/esm/utils/crossProjectServerPolicy.mjs +50 -0
- package/dist/esm/utils/effectClientGenerator.mjs +105 -484
- package/dist/esm-node/loader.mjs +23 -0
- package/dist/esm-node/runtime/data-platform/index.mjs +33 -22
- package/dist/esm-node/runtime/effect/adapter.mjs +91 -89
- package/dist/esm-node/runtime/effect/context.mjs +3 -1
- package/dist/esm-node/runtime/effect/edge.mjs +84 -0
- package/dist/esm-node/runtime/effect/endpoint-contracts.mjs +69 -0
- package/dist/esm-node/runtime/effect/handler.mjs +471 -0
- package/dist/esm-node/runtime/effect/index.mjs +3 -437
- package/dist/esm-node/runtime/effect/module.mjs +114 -0
- package/dist/esm-node/runtime/effect/operation-context.mjs +66 -0
- package/dist/esm-node/runtime/effect-client/index.mjs +14 -2
- package/dist/esm-node/runtime/effect-client/runtime.mjs +229 -0
- package/dist/esm-node/runtime/hono/adapter.mjs +21 -9
- package/dist/esm-node/runtime/safe-failure.mjs +46 -0
- package/dist/esm-node/utils/clientGenerator.mjs +5 -5
- package/dist/esm-node/utils/crossProjectServerPolicy.mjs +52 -0
- package/dist/esm-node/utils/effectClientGenerator.mjs +105 -484
- package/dist/types/runtime/create-request/index.d.ts +1 -0
- package/dist/types/runtime/data-platform/index.d.ts +4 -0
- package/dist/types/runtime/effect/adapter.d.ts +25 -0
- package/dist/types/runtime/effect/context.d.ts +3 -6
- package/dist/types/runtime/effect/edge.d.ts +25 -0
- package/dist/types/runtime/effect/endpoint-contracts.d.ts +62 -0
- package/dist/types/runtime/effect/handler.d.ts +203 -0
- package/dist/types/runtime/effect/index.d.ts +2 -170
- package/dist/types/runtime/effect/module.d.ts +48 -0
- package/dist/types/runtime/effect/operation-context.d.ts +10 -0
- package/dist/types/runtime/effect-client/index.d.ts +6 -1
- package/dist/types/runtime/effect-client/runtime.d.ts +71 -0
- package/dist/types/runtime/hono/adapter.d.ts +3 -0
- package/dist/types/runtime/safe-failure.d.ts +1 -0
- package/dist/types/utils/createHonoRoutes.d.ts +3 -3
- package/dist/types/utils/crossProjectServerPolicy.d.ts +35 -0
- package/dist/types/utils/effectClientGenerator.d.ts +16 -2
- package/package.json +41 -20
package/dist/esm-node/loader.mjs
CHANGED
|
@@ -4,6 +4,24 @@ import { generateClient } from "@modern-js/bff-core";
|
|
|
4
4
|
import { logger } from "@modern-js/utils";
|
|
5
5
|
import path from "path";
|
|
6
6
|
import { generateEffectClientCode, resolveEffectEntryFile } from "./utils/effectClientGenerator.mjs";
|
|
7
|
+
async function transformEffectRuntimeSource(source, filename) {
|
|
8
|
+
const swc = await import("@swc/core");
|
|
9
|
+
const result = await swc.transform(source, {
|
|
10
|
+
filename,
|
|
11
|
+
sourceMaps: false,
|
|
12
|
+
jsc: {
|
|
13
|
+
parser: {
|
|
14
|
+
syntax: "typescript",
|
|
15
|
+
tsx: filename.endsWith('.tsx') || filename.endsWith('.jsx')
|
|
16
|
+
},
|
|
17
|
+
target: 'es2022'
|
|
18
|
+
},
|
|
19
|
+
module: {
|
|
20
|
+
type: 'es6'
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
return result.code;
|
|
24
|
+
}
|
|
7
25
|
async function loader(source) {
|
|
8
26
|
this.cacheable();
|
|
9
27
|
const { resourcePath } = this;
|
|
@@ -15,6 +33,11 @@ async function loader(source) {
|
|
|
15
33
|
apiDir: draftOptions.apiDir,
|
|
16
34
|
effectEntry: draftOptions.effectEntry
|
|
17
35
|
});
|
|
36
|
+
if ('effect' === draftOptions.bffRuntimeFramework && effectEntryFile && path.resolve(effectEntryFile) === path.resolve(resourcePath) && this.resourceQuery.includes('modern-bff-runtime')) {
|
|
37
|
+
const code = await transformEffectRuntimeSource(source, resourcePath);
|
|
38
|
+
callback(void 0, code);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
18
41
|
if ('effect' === draftOptions.bffRuntimeFramework && effectEntryFile && path.resolve(effectEntryFile) === path.resolve(resourcePath)) {
|
|
19
42
|
const code = await generateEffectClientCode({
|
|
20
43
|
appDir: draftOptions.appDir,
|
|
@@ -1,8 +1,30 @@
|
|
|
1
1
|
import "node:module";
|
|
2
|
+
import { parseTraceparent } from "@modern-js/create-request";
|
|
3
|
+
import { trace as api_trace } from "@opentelemetry/api";
|
|
4
|
+
const DATA_BATCH_TRANSPORT_OTEL_EVENT = 'modernjs.data.batch';
|
|
5
|
+
function createDataBatchTransportTelemetryAttributes(event) {
|
|
6
|
+
return {
|
|
7
|
+
'modernjs.data.batch.type': event.type,
|
|
8
|
+
'modernjs.data.batch.endpoint': event.endpoint,
|
|
9
|
+
'modernjs.data.batch.degraded': 'fallback' === event.type || 'disable' === event.type,
|
|
10
|
+
...event.batchId ? {
|
|
11
|
+
'modernjs.data.batch.id': event.batchId
|
|
12
|
+
} : {},
|
|
13
|
+
...'number' == typeof event.size ? {
|
|
14
|
+
'modernjs.data.batch.size': event.size
|
|
15
|
+
} : {},
|
|
16
|
+
...event.reason ? {
|
|
17
|
+
'modernjs.data.batch.reason': event.reason
|
|
18
|
+
} : {}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function emitDataBatchTransportEvent(onEvent, event) {
|
|
22
|
+
onEvent?.(event);
|
|
23
|
+
api_trace.getActiveSpan()?.addEvent(DATA_BATCH_TRANSPORT_OTEL_EVENT, createDataBatchTransportTelemetryAttributes(event));
|
|
24
|
+
}
|
|
2
25
|
const DEFAULT_DATA_ENVELOPE_HEADER = 'x-modernjs-data-envelope';
|
|
3
26
|
const DEFAULT_DATA_BATCH_ENDPOINT = '/_data/batch';
|
|
4
27
|
const DEFAULT_DATA_BATCH_HEADER = 'x-modernjs-data-batch';
|
|
5
|
-
const TRACEPARENT_REGEX = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/i;
|
|
6
28
|
function isPlainObject(value) {
|
|
7
29
|
if ('object' != typeof value || null === value || Array.isArray(value)) return false;
|
|
8
30
|
const proto = Object.getPrototypeOf(value);
|
|
@@ -99,18 +121,7 @@ function isValidHex(value, length) {
|
|
|
99
121
|
return value.length === length && /^[0-9a-f]+$/.test(value);
|
|
100
122
|
}
|
|
101
123
|
function parseTraceparentHeader(header) {
|
|
102
|
-
|
|
103
|
-
if (!match) return null;
|
|
104
|
-
const traceId = match[1].toLowerCase();
|
|
105
|
-
const spanId = match[2].toLowerCase();
|
|
106
|
-
const flags = match[3].toLowerCase();
|
|
107
|
-
if (isAllZeroHex(traceId) || isAllZeroHex(spanId)) return null;
|
|
108
|
-
const sampled = (0x1 & Number.parseInt(flags, 16)) === 1;
|
|
109
|
-
return {
|
|
110
|
-
traceId,
|
|
111
|
-
spanId,
|
|
112
|
-
sampled
|
|
113
|
-
};
|
|
124
|
+
return parseTraceparent(header) ?? null;
|
|
114
125
|
}
|
|
115
126
|
function formatTraceparentHeader(trace) {
|
|
116
127
|
const traceId = trace.traceId.toLowerCase();
|
|
@@ -416,7 +427,7 @@ function createDataBatchTransport(options = {}) {
|
|
|
416
427
|
bucket.items = [];
|
|
417
428
|
bucket.bytes = 0;
|
|
418
429
|
if (1 === items.length || disabledEndpoints.has(endpoint)) {
|
|
419
|
-
onEvent
|
|
430
|
+
emitDataBatchTransportEvent(onEvent, {
|
|
420
431
|
type: disabledEndpoints.has(endpoint) ? 'fallback' : 'flush',
|
|
421
432
|
endpoint,
|
|
422
433
|
size: items.length,
|
|
@@ -433,7 +444,7 @@ function createDataBatchTransport(options = {}) {
|
|
|
433
444
|
sentAt: Date.now(),
|
|
434
445
|
items: items.map((item)=>item.item)
|
|
435
446
|
};
|
|
436
|
-
onEvent
|
|
447
|
+
emitDataBatchTransportEvent(onEvent, {
|
|
437
448
|
type: 'flush',
|
|
438
449
|
endpoint,
|
|
439
450
|
batchId,
|
|
@@ -460,7 +471,7 @@ function createDataBatchTransport(options = {}) {
|
|
|
460
471
|
requestInit.signal = controller.signal;
|
|
461
472
|
timeoutHandle = setTimeout(()=>{
|
|
462
473
|
controller.abort();
|
|
463
|
-
onEvent
|
|
474
|
+
emitDataBatchTransportEvent(onEvent, {
|
|
464
475
|
type: 'fallback',
|
|
465
476
|
endpoint,
|
|
466
477
|
batchId,
|
|
@@ -473,13 +484,13 @@ function createDataBatchTransport(options = {}) {
|
|
|
473
484
|
if (!response.ok) {
|
|
474
485
|
if (404 === response.status || 405 === response.status) {
|
|
475
486
|
disabledEndpoints.add(endpoint);
|
|
476
|
-
onEvent
|
|
487
|
+
emitDataBatchTransportEvent(onEvent, {
|
|
477
488
|
type: 'disable',
|
|
478
489
|
endpoint,
|
|
479
490
|
batchId,
|
|
480
491
|
reason: `batch-endpoint-unavailable-${String(response.status)}`
|
|
481
492
|
});
|
|
482
|
-
} else onEvent
|
|
493
|
+
} else emitDataBatchTransportEvent(onEvent, {
|
|
483
494
|
type: 'fallback',
|
|
484
495
|
endpoint,
|
|
485
496
|
batchId,
|
|
@@ -492,7 +503,7 @@ function createDataBatchTransport(options = {}) {
|
|
|
492
503
|
}
|
|
493
504
|
const result = await response.json();
|
|
494
505
|
if (!isBatchResponsePayload(result)) {
|
|
495
|
-
onEvent
|
|
506
|
+
emitDataBatchTransportEvent(onEvent, {
|
|
496
507
|
type: 'fallback',
|
|
497
508
|
endpoint,
|
|
498
509
|
batchId,
|
|
@@ -515,7 +526,7 @@ function createDataBatchTransport(options = {}) {
|
|
|
515
526
|
return parseResponseLikeCreateRequest(reconstructedResponse);
|
|
516
527
|
});
|
|
517
528
|
} catch (error) {
|
|
518
|
-
onEvent
|
|
529
|
+
emitDataBatchTransportEvent(onEvent, {
|
|
519
530
|
type: 'fallback',
|
|
520
531
|
endpoint,
|
|
521
532
|
batchId,
|
|
@@ -582,7 +593,7 @@ function createDataBatchTransport(options = {}) {
|
|
|
582
593
|
};
|
|
583
594
|
bucket.items.push(queued);
|
|
584
595
|
bucket.bytes += size;
|
|
585
|
-
onEvent
|
|
596
|
+
emitDataBatchTransportEvent(onEvent, {
|
|
586
597
|
type: 'enqueue',
|
|
587
598
|
endpoint,
|
|
588
599
|
size: bucket.items.length
|
|
@@ -597,4 +608,4 @@ function createDataBatchTransport(options = {}) {
|
|
|
597
608
|
return promise;
|
|
598
609
|
};
|
|
599
610
|
}
|
|
600
|
-
export { DEFAULT_DATA_BATCH_ENDPOINT, DEFAULT_DATA_BATCH_HEADER, DEFAULT_DATA_ENVELOPE_HEADER, buildQueryKey, buildScopeKey, createDataBatchTransport, createHydrationEnvelope, createInvalidationEvent, createOperationId, createRequestEnvelope, decodeRequestEnvelopeHeader, deriveChildTraceContext, encodeRequestEnvelopeHeader, formatTraceparentHeader, normalizeOrigin, parseTraceparentHeader, shouldApplyInvalidation, stableStringify, validateHydrationEnvelope, validateRequestEnvelope, validateSelectionPlan };
|
|
611
|
+
export { DATA_BATCH_TRANSPORT_OTEL_EVENT, DEFAULT_DATA_BATCH_ENDPOINT, DEFAULT_DATA_BATCH_HEADER, DEFAULT_DATA_ENVELOPE_HEADER, buildQueryKey, buildScopeKey, createDataBatchTransport, createDataBatchTransportTelemetryAttributes, createHydrationEnvelope, createInvalidationEvent, createOperationId, createRequestEnvelope, decodeRequestEnvelopeHeader, deriveChildTraceContext, emitDataBatchTransportEvent, encodeRequestEnvelopeHeader, formatTraceparentHeader, normalizeOrigin, parseTraceparentHeader, shouldApplyInvalidation, stableStringify, validateHydrationEnvelope, validateRequestEnvelope, validateSelectionPlan };
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import __rslib_shim_module__ from "node:module";
|
|
2
2
|
const require = /*#__PURE__*/ __rslib_shim_module__.createRequire(/*#__PURE__*/ (()=>import.meta.url)());
|
|
3
|
+
import { ApiRouter } from "@modern-js/bff-core";
|
|
3
4
|
import { API_DIR, compatibleRequire, findExists, fs, isProd, logger } from "@modern-js/utils";
|
|
4
5
|
import { HttpApi } from "effect/unstable/httpapi";
|
|
5
6
|
import path from "path";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
7
|
+
import { checkCrossProjectPolicyForRequest, resolveAdapterCrossProjectPolicy } from "../../utils/crossProjectServerPolicy.mjs";
|
|
8
|
+
import { createSafeFailureResponse } from "../safe-failure.mjs";
|
|
9
|
+
import { createEffectOperationContext, runWithEffectContext } from "./context.mjs";
|
|
10
|
+
import { collectEffectEndpoints, extractHttpApiFromModule, toOperationContractSources } from "./endpoint-contracts.mjs";
|
|
11
|
+
import { resolveEffectBffModuleHandler } from "./module.mjs";
|
|
8
12
|
const before = [
|
|
9
13
|
'custom-server-hook',
|
|
10
14
|
'custom-server-middleware',
|
|
@@ -37,24 +41,9 @@ function createRequestForMountedPrefix(req, prefix) {
|
|
|
37
41
|
url.pathname = nextPath;
|
|
38
42
|
return new Request(url, req);
|
|
39
43
|
}
|
|
40
|
-
function isRequestHandler(value) {
|
|
41
|
-
return 'function' == typeof value;
|
|
42
|
-
}
|
|
43
44
|
function maybeResponse(value) {
|
|
44
45
|
return value instanceof Response;
|
|
45
46
|
}
|
|
46
|
-
function isRecord(value) {
|
|
47
|
-
return 'object' == typeof value && null !== value;
|
|
48
|
-
}
|
|
49
|
-
function includesRuntimeExports(value) {
|
|
50
|
-
return 'api' in value || 'layer' in value || 'createHandler' in value || 'handler' in value;
|
|
51
|
-
}
|
|
52
|
-
function isHttpApiWithProps(value) {
|
|
53
|
-
return HttpApi.isHttpApi(value) && isRecord(value) && 'string' == typeof value.identifier && isRecord(value.groups);
|
|
54
|
-
}
|
|
55
|
-
function isEffectApiDefinition(module) {
|
|
56
|
-
return isHttpApiWithProps(module.api) && void 0 !== module.layer;
|
|
57
|
-
}
|
|
58
47
|
class EffectAdapter {
|
|
59
48
|
resolveEntryFile() {
|
|
60
49
|
const { appDirectory, apiDirectory } = this.api.getServerContext();
|
|
@@ -64,71 +53,77 @@ class EffectAdapter {
|
|
|
64
53
|
const entryWithoutExt = configuredEntry ? path.isAbsolute(configuredEntry) ? configuredEntry : path.resolve(appDirectory || process.cwd(), configuredEntry) : defaultEntry;
|
|
65
54
|
return findExists(JS_OR_TS_EXTS.map((ext)=>`${entryWithoutExt}${ext}`));
|
|
66
55
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
};
|
|
88
|
-
mergeRuntimeExports(out);
|
|
89
|
-
}
|
|
90
|
-
if (isRecord(entry)) normalizedModule = {
|
|
91
|
-
...normalizedModule,
|
|
92
|
-
...entry
|
|
93
|
-
};
|
|
94
|
-
if (isRecord(entry) && 'handler' in entry) {
|
|
95
|
-
const maybeHandler = entry.handler;
|
|
96
|
-
if (isRequestHandler(maybeHandler)) normalizedModule = {
|
|
97
|
-
...normalizedModule,
|
|
98
|
-
handler: maybeHandler
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
if (isRequestHandler(normalizedModule.handler)) return {
|
|
102
|
-
handler: normalizedModule.handler
|
|
103
|
-
};
|
|
104
|
-
if ('function' == typeof normalizedModule.createHandler) {
|
|
105
|
-
const webHandler = normalizedModule.createHandler({
|
|
106
|
-
openapi: this.api.getServerConfig()?.bff?.effect?.openapi,
|
|
107
|
-
dataPlatform: this.api.getServerConfig()?.bff?.effect?.dataPlatform
|
|
56
|
+
isApiRequestPath(requestPath, prefix, enableHandleWeb) {
|
|
57
|
+
if (!enableHandleWeb) return true;
|
|
58
|
+
const normalized = normalizePrefix(prefix);
|
|
59
|
+
if (!normalized) return true;
|
|
60
|
+
return requestPath === normalized || requestPath.startsWith(`${normalized}/`);
|
|
61
|
+
}
|
|
62
|
+
async collectLambdaContractSources() {
|
|
63
|
+
try {
|
|
64
|
+
const serverContext = this.api.getServerContext();
|
|
65
|
+
const appDir = serverContext.distDirectory || serverContext.appDirectory;
|
|
66
|
+
if (!appDir) return [];
|
|
67
|
+
const apiDir = 'string' == typeof serverContext.apiDirectory ? serverContext.apiDirectory : path.resolve(appDir, API_DIR);
|
|
68
|
+
const lambdaDir = 'string' == typeof serverContext.lambdaDirectory ? serverContext.lambdaDirectory : path.join(apiDir, 'lambda');
|
|
69
|
+
if (!await fs.pathExists(lambdaDir)) return [];
|
|
70
|
+
const apiRouter = new ApiRouter({
|
|
71
|
+
appDir,
|
|
72
|
+
apiDir,
|
|
73
|
+
lambdaDir,
|
|
74
|
+
prefix: this.prefix,
|
|
75
|
+
httpMethodDecider: this.api.getServerConfig()?.bff?.httpMethodDecider
|
|
108
76
|
});
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
77
|
+
const handlerInfos = await apiRouter.getApiHandlers();
|
|
78
|
+
return handlerInfos.map((info)=>({
|
|
79
|
+
name: info.name,
|
|
80
|
+
httpMethod: info.httpMethod,
|
|
81
|
+
routePath: info.routePath,
|
|
82
|
+
filename: info.filename,
|
|
83
|
+
handler: info.handler
|
|
84
|
+
}));
|
|
85
|
+
} catch (error) {
|
|
86
|
+
logger.warn(`[BFF][Effect] Failed to derive lambda operation contracts for the cross-project policy: ${String(error)}`);
|
|
87
|
+
return [];
|
|
115
88
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
};
|
|
89
|
+
}
|
|
90
|
+
async refreshCrossProjectPolicy(mod) {
|
|
91
|
+
let contractSources = [];
|
|
92
|
+
if (mod) try {
|
|
93
|
+
const api = await extractHttpApiFromModule(mod, HttpApi.isHttpApi);
|
|
94
|
+
if (api) {
|
|
95
|
+
const reflect = (apiValue, handlers)=>HttpApi.reflect(apiValue, {
|
|
96
|
+
onGroup: handlers.onGroup ?? (()=>{}),
|
|
97
|
+
onEndpoint: handlers.onEndpoint
|
|
98
|
+
});
|
|
99
|
+
contractSources = toOperationContractSources(collectEffectEndpoints(reflect, api, this.prefix));
|
|
100
|
+
}
|
|
101
|
+
} catch (error) {
|
|
102
|
+
logger.warn(`[BFF][Effect] Failed to reflect HttpApi endpoints for the cross-project policy: ${String(error)}`);
|
|
130
103
|
}
|
|
131
|
-
|
|
104
|
+
let policy = resolveAdapterCrossProjectPolicy(this.api, contractSources);
|
|
105
|
+
if (policy?.enabled) {
|
|
106
|
+
const lambdaSources = await this.collectLambdaContractSources();
|
|
107
|
+
if (lambdaSources.length > 0) {
|
|
108
|
+
contractSources = [
|
|
109
|
+
...contractSources,
|
|
110
|
+
...lambdaSources
|
|
111
|
+
];
|
|
112
|
+
policy = resolveAdapterCrossProjectPolicy(this.api, contractSources);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
this.crossProjectPolicy = policy;
|
|
116
|
+
if (this.crossProjectPolicy?.enabled && 0 === contractSources.length) logger.warn('[BFF][Effect] Cross-project policy is enabled but no HttpApi endpoints could be reflected; operation-contract matching is disabled for this server (envelope and operation-context checks still apply).');
|
|
117
|
+
}
|
|
118
|
+
async loadEffectHandlerFromModule(mod) {
|
|
119
|
+
return resolveEffectBffModuleHandler(mod, {
|
|
120
|
+
openapi: this.api.getServerConfig()?.bff?.effect?.openapi,
|
|
121
|
+
dataPlatform: this.api.getServerConfig()?.bff?.effect?.dataPlatform,
|
|
122
|
+
validateRequest: (request)=>checkCrossProjectPolicyForRequest(request, this.crossProjectPolicy),
|
|
123
|
+
onWarning: (message)=>{
|
|
124
|
+
logger.warn(message);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
132
127
|
}
|
|
133
128
|
async reloadHandler() {
|
|
134
129
|
if (!this.isEffect) return;
|
|
@@ -154,6 +149,7 @@ class EffectAdapter {
|
|
|
154
149
|
this.handler = null;
|
|
155
150
|
return;
|
|
156
151
|
}
|
|
152
|
+
await this.refreshCrossProjectPolicy(mod);
|
|
157
153
|
const loaded = await this.loadEffectHandlerFromModule(mod);
|
|
158
154
|
if (!loaded) {
|
|
159
155
|
logger.warn(`[BFF][Effect] Invalid Effect entry module: ${entryFile}. Export { api, layer } or handler.`);
|
|
@@ -162,6 +158,7 @@ class EffectAdapter {
|
|
|
162
158
|
}
|
|
163
159
|
this.handler = loaded.handler;
|
|
164
160
|
this.dispose = loaded.dispose || null;
|
|
161
|
+
this.policyEnforcedInMiddleware = !loaded.appliesRequestValidator;
|
|
165
162
|
}
|
|
166
163
|
async disposeCurrentHandler() {
|
|
167
164
|
if (!this.dispose) return;
|
|
@@ -185,15 +182,7 @@ class EffectAdapter {
|
|
|
185
182
|
} catch (configError) {
|
|
186
183
|
logger.error(`Error in serverConfig.onError handler: ${configError}`);
|
|
187
184
|
}
|
|
188
|
-
|
|
189
|
-
return new Response(JSON.stringify({
|
|
190
|
-
message: error instanceof Error ? error.message : '[BFF] Internal Server Error'
|
|
191
|
-
}), {
|
|
192
|
-
status,
|
|
193
|
-
headers: {
|
|
194
|
-
'content-type': 'application/json; charset=utf-8'
|
|
195
|
-
}
|
|
196
|
-
});
|
|
185
|
+
return createSafeFailureResponse(error);
|
|
197
186
|
}
|
|
198
187
|
ensureJsonContext(c) {
|
|
199
188
|
const maybeJsonContext = c;
|
|
@@ -220,6 +209,8 @@ class EffectAdapter {
|
|
|
220
209
|
this.effectMiddleware = null;
|
|
221
210
|
this.handler = null;
|
|
222
211
|
this.dispose = null;
|
|
212
|
+
this.prefix = '/api';
|
|
213
|
+
this.policyEnforcedInMiddleware = false;
|
|
223
214
|
this.registerMiddleware = async (options)=>{
|
|
224
215
|
const { prefix, enableHandleWeb } = options;
|
|
225
216
|
const { bffRuntimeFramework, middlewares: globalMiddlewares } = this.api.getServerContext();
|
|
@@ -227,6 +218,7 @@ class EffectAdapter {
|
|
|
227
218
|
this.isEffect = false;
|
|
228
219
|
return;
|
|
229
220
|
}
|
|
221
|
+
this.prefix = prefix || this.prefix;
|
|
230
222
|
await this.reloadHandler();
|
|
231
223
|
this.effectMiddleware = {
|
|
232
224
|
name: 'effect-bff-handler',
|
|
@@ -239,6 +231,10 @@ class EffectAdapter {
|
|
|
239
231
|
if (enableHandleWeb) return void await next();
|
|
240
232
|
return this.handleRuntimeError(new Error('[BFF][Effect] Missing Effect entry. Define api/effect/index or configure bff.effect.entry.'), c);
|
|
241
233
|
}
|
|
234
|
+
if (this.crossProjectPolicy?.enabled && this.policyEnforcedInMiddleware && this.isApiRequestPath(c.req.path, prefix, enableHandleWeb)) {
|
|
235
|
+
const denial = checkCrossProjectPolicyForRequest(c.req.raw, this.crossProjectPolicy);
|
|
236
|
+
if (denial) return denial;
|
|
237
|
+
}
|
|
242
238
|
let response;
|
|
243
239
|
try {
|
|
244
240
|
const effectRequest = createRequestForMountedPrefix(c.req.raw, prefix);
|
|
@@ -246,7 +242,13 @@ class EffectAdapter {
|
|
|
246
242
|
request: effectRequest,
|
|
247
243
|
env: c.env,
|
|
248
244
|
path: c.req.path,
|
|
249
|
-
method: c.req.method
|
|
245
|
+
method: c.req.method,
|
|
246
|
+
operationContext: createEffectOperationContext({
|
|
247
|
+
request: effectRequest,
|
|
248
|
+
env: c.env,
|
|
249
|
+
path: c.req.path,
|
|
250
|
+
method: c.req.method
|
|
251
|
+
})
|
|
250
252
|
};
|
|
251
253
|
response = await runWithEffectContext(effectContext, ()=>this.handler.length > 1 ? this.handler(effectRequest, effectContext) : this.handler(effectRequest));
|
|
252
254
|
} catch (error) {
|
|
@@ -9,4 +9,6 @@ const useEffectContext = ()=>{
|
|
|
9
9
|
if (!context) throw new Error("Can't call useEffectContext out of Effect runtime scope");
|
|
10
10
|
return context;
|
|
11
11
|
};
|
|
12
|
-
|
|
12
|
+
const useOperationContext = ()=>useEffectContext().operationContext;
|
|
13
|
+
export { createEffectOperationContext } from "./operation-context.mjs";
|
|
14
|
+
export { runWithEffectContext, useEffectContext, useOperationContext };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { createSafeFailureResponse } from "../safe-failure.mjs";
|
|
3
|
+
import { resolveEffectBffModuleHandler } from "./module.mjs";
|
|
4
|
+
import { createEffectOperationContext } from "./operation-context.mjs";
|
|
5
|
+
export * from "./handler.mjs";
|
|
6
|
+
function normalizePrefix(prefix) {
|
|
7
|
+
if (!prefix || '/' === prefix) return '';
|
|
8
|
+
return prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
|
|
9
|
+
}
|
|
10
|
+
function removePrefixFromPath(pathname, prefix) {
|
|
11
|
+
const normalized = normalizePrefix(prefix);
|
|
12
|
+
if (!normalized || pathname !== normalized && !pathname.startsWith(`${normalized}/`)) return pathname;
|
|
13
|
+
const sliced = pathname.slice(normalized.length);
|
|
14
|
+
return sliced.startsWith('/') ? sliced : `/${sliced}`;
|
|
15
|
+
}
|
|
16
|
+
function matchesPrefix(pathname, prefix) {
|
|
17
|
+
const normalized = normalizePrefix(prefix);
|
|
18
|
+
return !normalized || pathname === normalized || pathname.startsWith(`${normalized}/`);
|
|
19
|
+
}
|
|
20
|
+
function createRequestForMountedPrefix(req, prefix) {
|
|
21
|
+
const url = new URL(req.url);
|
|
22
|
+
const nextPath = removePrefixFromPath(url.pathname, prefix);
|
|
23
|
+
if (nextPath === url.pathname) return req;
|
|
24
|
+
url.pathname = nextPath;
|
|
25
|
+
return new Request(url, req);
|
|
26
|
+
}
|
|
27
|
+
function createEdgeEffectContext(originalRequest, effectRequest, options) {
|
|
28
|
+
const originalPath = options.path || new URL(originalRequest.url).pathname;
|
|
29
|
+
const method = options.method || originalRequest.method;
|
|
30
|
+
return {
|
|
31
|
+
request: effectRequest,
|
|
32
|
+
env: options.env || {},
|
|
33
|
+
path: originalPath,
|
|
34
|
+
method,
|
|
35
|
+
operationContext: createEffectOperationContext({
|
|
36
|
+
request: effectRequest,
|
|
37
|
+
env: options.env || {},
|
|
38
|
+
path: originalPath,
|
|
39
|
+
method
|
|
40
|
+
})
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function createRuntimeErrorResponse(error) {
|
|
44
|
+
return createSafeFailureResponse(error);
|
|
45
|
+
}
|
|
46
|
+
async function dispatchEffectBffRequest(handler, request, options = {}) {
|
|
47
|
+
const requestPathname = new URL(request.url).pathname;
|
|
48
|
+
if (!matchesPrefix(requestPathname, options.prefix)) return new Response(null, {
|
|
49
|
+
status: 404
|
|
50
|
+
});
|
|
51
|
+
const effectRequest = createRequestForMountedPrefix(request, options.prefix);
|
|
52
|
+
const effectContext = createEdgeEffectContext(request, effectRequest, options);
|
|
53
|
+
try {
|
|
54
|
+
const response = handler.length > 1 ? await handler(effectRequest, effectContext) : await handler(effectRequest);
|
|
55
|
+
if (!(response instanceof Response)) throw new Error('[BFF][Effect] Effect handler must return a Response instance.');
|
|
56
|
+
return new Response(response.body, response);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (error instanceof Response) return new Response(error.body, error);
|
|
59
|
+
if (options.onError) {
|
|
60
|
+
const errorResponse = await options.onError(error, effectContext);
|
|
61
|
+
if (errorResponse instanceof Response) return errorResponse;
|
|
62
|
+
}
|
|
63
|
+
return createRuntimeErrorResponse(error);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function createEffectBffEdgeHandler(options) {
|
|
67
|
+
const loaded = await resolveEffectBffModuleHandler(options.module, {
|
|
68
|
+
openapi: options.openapi,
|
|
69
|
+
dataPlatform: options.dataPlatform,
|
|
70
|
+
onWarning: options.onWarning
|
|
71
|
+
});
|
|
72
|
+
if (!loaded) throw new Error('[BFF][Effect] Invalid Effect edge module. Export { api, layer }, createHandler, or handler.');
|
|
73
|
+
return {
|
|
74
|
+
handler: (request, dispatchOptions = {})=>dispatchEffectBffRequest(loaded.handler, request, {
|
|
75
|
+
...dispatchOptions,
|
|
76
|
+
prefix: options.prefix,
|
|
77
|
+
onError: options.onError
|
|
78
|
+
}),
|
|
79
|
+
dispose: async ()=>{
|
|
80
|
+
await loaded.dispose?.();
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export { createEffectBffEdgeHandler, createEffectOperationContext, dispatchEffectBffRequest };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { createOperationContractHash } from "@modern-js/bff-core";
|
|
3
|
+
function ensureLeadingSlash(pathname) {
|
|
4
|
+
return pathname.startsWith('/') ? pathname : `/${pathname}`;
|
|
5
|
+
}
|
|
6
|
+
function normalizeEffectPrefix(prefix) {
|
|
7
|
+
if ('/' === prefix) return '';
|
|
8
|
+
return ensureLeadingSlash(prefix || '/api');
|
|
9
|
+
}
|
|
10
|
+
function getEffectRoutePath(prefix, endpointPath) {
|
|
11
|
+
const normalizedPrefix = normalizeEffectPrefix(prefix);
|
|
12
|
+
const normalizedEndpointPath = ensureLeadingSlash(endpointPath);
|
|
13
|
+
const finalEndpointPath = '/' === normalizedEndpointPath ? '' : endpointPath;
|
|
14
|
+
if (!normalizedPrefix && !finalEndpointPath) return '/';
|
|
15
|
+
return `${normalizedPrefix}${finalEndpointPath || ''}`;
|
|
16
|
+
}
|
|
17
|
+
function resolveEffectApiId(api) {
|
|
18
|
+
const fallback = 'EffectHttpApi';
|
|
19
|
+
if ('identifier' in api && 'string' == typeof api.identifier && api.identifier) return api.identifier;
|
|
20
|
+
return fallback;
|
|
21
|
+
}
|
|
22
|
+
function collectEffectEndpoints(reflect, api, prefix) {
|
|
23
|
+
const endpoints = [];
|
|
24
|
+
const apiId = resolveEffectApiId(api);
|
|
25
|
+
reflect(api, {
|
|
26
|
+
onGroup: ()=>{},
|
|
27
|
+
onEndpoint: ({ group, endpoint })=>{
|
|
28
|
+
endpoints.push({
|
|
29
|
+
apiId,
|
|
30
|
+
groupName: String(group.identifier),
|
|
31
|
+
endpointName: String(endpoint.name),
|
|
32
|
+
method: String(endpoint.method).toUpperCase(),
|
|
33
|
+
routePath: getEffectRoutePath(prefix, String(endpoint.path))
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
return endpoints.sort((a, b)=>{
|
|
38
|
+
if (a.groupName === b.groupName) return a.endpointName.localeCompare(b.endpointName);
|
|
39
|
+
return a.groupName.localeCompare(b.groupName);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function toOperationContractSources(endpoints) {
|
|
43
|
+
return endpoints.map(createEffectOperationContractSource);
|
|
44
|
+
}
|
|
45
|
+
function createEffectOperationContractSource(endpoint) {
|
|
46
|
+
return {
|
|
47
|
+
name: endpoint.endpointName,
|
|
48
|
+
httpMethod: endpoint.method,
|
|
49
|
+
routePath: endpoint.routePath
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function isRecord(value) {
|
|
53
|
+
return 'object' == typeof value && null !== value;
|
|
54
|
+
}
|
|
55
|
+
async function extractHttpApiFromModule(mod, isHttpApi) {
|
|
56
|
+
if (!isRecord(mod)) return null;
|
|
57
|
+
if (isHttpApi(mod.api)) return mod.api;
|
|
58
|
+
const entry = mod.default;
|
|
59
|
+
if (isRecord(entry) && isHttpApi(entry.api)) return entry.api;
|
|
60
|
+
if ('function' == typeof entry && 0 === entry.length) {
|
|
61
|
+
const output = await entry();
|
|
62
|
+
if (isRecord(output) && isHttpApi(output.api)) return output.api;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
function createEffectEndpointContractHash(endpoint, requestId) {
|
|
67
|
+
return createOperationContractHash(createEffectOperationContractSource(endpoint), requestId);
|
|
68
|
+
}
|
|
69
|
+
export { collectEffectEndpoints, createEffectEndpointContractHash, createEffectOperationContractSource, ensureLeadingSlash, extractHttpApiFromModule, getEffectRoutePath, normalizeEffectPrefix, resolveEffectApiId, toOperationContractSources };
|