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