@bleedingdev/modern-js-plugin-bff 3.4.0-ultramodern.2 → 3.4.0-ultramodern.20
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/runtime/effect/adapter.js +12 -59
- package/dist/cjs/runtime/effect/edge.js +1 -1
- package/dist/cjs/runtime/effect/handler.js +40 -3
- package/dist/cjs/runtime/effect/module.js +46 -27
- package/dist/cjs/runtime/effect-client/index.js +4 -0
- package/dist/cjs/utils/effectClientGenerator.js +13 -8
- package/dist/esm/runtime/effect/adapter.mjs +12 -59
- package/dist/esm/runtime/effect/edge.mjs +1 -1
- package/dist/esm/runtime/effect/handler.mjs +40 -3
- package/dist/esm/runtime/effect/module.mjs +46 -27
- package/dist/esm/runtime/effect-client/index.mjs +2 -1
- package/dist/esm/utils/effectClientGenerator.mjs +13 -8
- package/dist/esm-node/runtime/effect/adapter.mjs +12 -59
- package/dist/esm-node/runtime/effect/edge.mjs +1 -1
- package/dist/esm-node/runtime/effect/handler.mjs +40 -3
- package/dist/esm-node/runtime/effect/module.mjs +46 -27
- package/dist/esm-node/runtime/effect-client/index.mjs +2 -1
- package/dist/esm-node/utils/effectClientGenerator.mjs +13 -8
- package/dist/types/runtime/effect/adapter.d.ts +3 -19
- package/dist/types/runtime/effect/handler.d.ts +2 -5
- package/dist/types/runtime/effect/module.d.ts +3 -16
- package/dist/types/runtime/effect-client/index.d.ts +2 -3
- package/package.json +13 -13
|
@@ -39,7 +39,6 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
39
39
|
__webpack_require__.d(__webpack_exports__, {
|
|
40
40
|
EffectAdapter: ()=>EffectAdapter
|
|
41
41
|
});
|
|
42
|
-
const bff_core_namespaceObject = require("@modern-js/bff-core");
|
|
43
42
|
const utils_namespaceObject = require("@modern-js/utils");
|
|
44
43
|
const httpapi_namespaceObject = require("effect/unstable/httpapi");
|
|
45
44
|
const external_path_namespaceObject = require("path");
|
|
@@ -94,43 +93,12 @@ class EffectAdapter {
|
|
|
94
93
|
const { appDirectory, apiDirectory } = this.api.getServerContext();
|
|
95
94
|
const bffConfig = this.api.getServerConfig()?.bff;
|
|
96
95
|
const configuredEntry = bffConfig?.effect?.entry;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
isApiRequestPath(requestPath, prefix, enableHandleWeb) {
|
|
102
|
-
if (!enableHandleWeb) return true;
|
|
103
|
-
const normalized = normalizePrefix(prefix);
|
|
104
|
-
if (!normalized) return true;
|
|
105
|
-
return requestPath === normalized || requestPath.startsWith(`${normalized}/`);
|
|
106
|
-
}
|
|
107
|
-
async collectLambdaContractSources() {
|
|
108
|
-
try {
|
|
109
|
-
const serverContext = this.api.getServerContext();
|
|
110
|
-
const appDir = serverContext.distDirectory || serverContext.appDirectory;
|
|
111
|
-
if (!appDir) return [];
|
|
112
|
-
const apiDir = 'string' == typeof serverContext.apiDirectory ? serverContext.apiDirectory : external_path_default().resolve(appDir, utils_namespaceObject.API_DIR);
|
|
113
|
-
const lambdaDir = 'string' == typeof serverContext.lambdaDirectory ? serverContext.lambdaDirectory : external_path_default().join(apiDir, 'lambda');
|
|
114
|
-
if (!await utils_namespaceObject.fs.pathExists(lambdaDir)) return [];
|
|
115
|
-
const apiRouter = new bff_core_namespaceObject.ApiRouter({
|
|
116
|
-
appDir,
|
|
117
|
-
apiDir,
|
|
118
|
-
lambdaDir,
|
|
119
|
-
prefix: this.prefix,
|
|
120
|
-
httpMethodDecider: this.api.getServerConfig()?.bff?.httpMethodDecider
|
|
121
|
-
});
|
|
122
|
-
const handlerInfos = await apiRouter.getApiHandlers();
|
|
123
|
-
return handlerInfos.map((info)=>({
|
|
124
|
-
name: info.name,
|
|
125
|
-
httpMethod: info.httpMethod,
|
|
126
|
-
routePath: info.routePath,
|
|
127
|
-
filename: info.filename,
|
|
128
|
-
handler: info.handler
|
|
129
|
-
}));
|
|
130
|
-
} catch (error) {
|
|
131
|
-
utils_namespaceObject.logger.warn(`[BFF][Effect] Failed to derive lambda operation contracts for the cross-project policy: ${String(error)}`);
|
|
132
|
-
return [];
|
|
96
|
+
if (configuredEntry) {
|
|
97
|
+
const entryWithoutExt = external_path_default().isAbsolute(configuredEntry) ? configuredEntry : external_path_default().resolve(appDirectory || process.cwd(), configuredEntry);
|
|
98
|
+
return resolveJsOrTsEntry(entryWithoutExt);
|
|
133
99
|
}
|
|
100
|
+
const apiRoot = external_path_default().resolve(appDirectory || process.cwd(), apiDirectory || utils_namespaceObject.API_DIR);
|
|
101
|
+
return resolveJsOrTsEntry(external_path_default().resolve(apiRoot, 'index'));
|
|
134
102
|
}
|
|
135
103
|
async refreshCrossProjectPolicy(mod) {
|
|
136
104
|
let contractSources = [];
|
|
@@ -146,24 +114,15 @@ class EffectAdapter {
|
|
|
146
114
|
} catch (error) {
|
|
147
115
|
utils_namespaceObject.logger.warn(`[BFF][Effect] Failed to reflect HttpApi endpoints for the cross-project policy: ${String(error)}`);
|
|
148
116
|
}
|
|
149
|
-
|
|
150
|
-
if (policy?.enabled) {
|
|
151
|
-
const lambdaSources = await this.collectLambdaContractSources();
|
|
152
|
-
if (lambdaSources.length > 0) {
|
|
153
|
-
contractSources = [
|
|
154
|
-
...contractSources,
|
|
155
|
-
...lambdaSources
|
|
156
|
-
];
|
|
157
|
-
policy = (0, crossProjectServerPolicy_js_namespaceObject.resolveAdapterCrossProjectPolicy)(this.api, contractSources);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
117
|
+
const policy = (0, crossProjectServerPolicy_js_namespaceObject.resolveAdapterCrossProjectPolicy)(this.api, contractSources);
|
|
160
118
|
this.crossProjectPolicy = policy;
|
|
161
119
|
if (this.crossProjectPolicy?.enabled && 0 === contractSources.length) utils_namespaceObject.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).');
|
|
162
120
|
}
|
|
163
121
|
async loadEffectHandlerFromModule(mod) {
|
|
122
|
+
const effectConfig = this.api.getServerConfig()?.bff?.effect;
|
|
164
123
|
return (0, external_module_js_namespaceObject.resolveEffectBffModuleHandler)(mod, {
|
|
165
|
-
openapi:
|
|
166
|
-
dataPlatform:
|
|
124
|
+
openapi: effectConfig?.openapi,
|
|
125
|
+
dataPlatform: effectConfig?.dataPlatform,
|
|
167
126
|
validateRequest: (request)=>(0, crossProjectServerPolicy_js_namespaceObject.checkCrossProjectPolicyForRequest)(request, this.crossProjectPolicy),
|
|
168
127
|
onWarning: (message)=>{
|
|
169
128
|
utils_namespaceObject.logger.warn(message);
|
|
@@ -197,13 +156,12 @@ class EffectAdapter {
|
|
|
197
156
|
await this.refreshCrossProjectPolicy(mod);
|
|
198
157
|
const loaded = await this.loadEffectHandlerFromModule(mod);
|
|
199
158
|
if (!loaded) {
|
|
200
|
-
utils_namespaceObject.logger.warn(`[BFF][Effect] Invalid Effect entry module: ${entryFile}. Export { api, layer }
|
|
159
|
+
utils_namespaceObject.logger.warn(`[BFF][Effect] Invalid Effect entry module: ${entryFile}. Export defineEffectBff(...) or a { api, layer } HttpApi module.`);
|
|
201
160
|
this.handler = null;
|
|
202
161
|
return;
|
|
203
162
|
}
|
|
204
163
|
this.handler = loaded.handler;
|
|
205
164
|
this.dispose = loaded.dispose || null;
|
|
206
|
-
this.policyEnforcedInMiddleware = !loaded.appliesRequestValidator;
|
|
207
165
|
}
|
|
208
166
|
async disposeCurrentHandler() {
|
|
209
167
|
if (!this.dispose) return;
|
|
@@ -255,7 +213,6 @@ class EffectAdapter {
|
|
|
255
213
|
this.handler = null;
|
|
256
214
|
this.dispose = null;
|
|
257
215
|
this.prefix = '/api';
|
|
258
|
-
this.policyEnforcedInMiddleware = false;
|
|
259
216
|
this.registerMiddleware = async (options)=>{
|
|
260
217
|
const { prefix, enableHandleWeb } = options;
|
|
261
218
|
const { bffRuntimeFramework, middlewares: globalMiddlewares } = this.api.getServerContext();
|
|
@@ -266,7 +223,7 @@ class EffectAdapter {
|
|
|
266
223
|
this.prefix = prefix || this.prefix;
|
|
267
224
|
await this.reloadHandler();
|
|
268
225
|
this.effectMiddleware = {
|
|
269
|
-
name: 'effect-
|
|
226
|
+
name: 'effect-api-handler',
|
|
270
227
|
path: enableHandleWeb ? '*' : `${prefix}/*`,
|
|
271
228
|
method: 'all',
|
|
272
229
|
order: 'post',
|
|
@@ -274,11 +231,7 @@ class EffectAdapter {
|
|
|
274
231
|
handler: async (c, next)=>{
|
|
275
232
|
if (!this.handler) {
|
|
276
233
|
if (enableHandleWeb) return void await next();
|
|
277
|
-
return this.handleRuntimeError(new Error('[BFF][Effect] Missing Effect entry. Define api/
|
|
278
|
-
}
|
|
279
|
-
if (this.crossProjectPolicy?.enabled && this.policyEnforcedInMiddleware && this.isApiRequestPath(c.req.path, prefix, enableHandleWeb)) {
|
|
280
|
-
const denial = (0, crossProjectServerPolicy_js_namespaceObject.checkCrossProjectPolicyForRequest)(c.req.raw, this.crossProjectPolicy);
|
|
281
|
-
if (denial) return denial;
|
|
234
|
+
return this.handleRuntimeError(new Error('[BFF][Effect] Missing Effect entry. Define api/index or configure bff.effect.entry.'), c);
|
|
282
235
|
}
|
|
283
236
|
let response;
|
|
284
237
|
try {
|
|
@@ -138,7 +138,7 @@ var __webpack_exports__ = {};
|
|
|
138
138
|
dataPlatform: options.dataPlatform,
|
|
139
139
|
onWarning: options.onWarning
|
|
140
140
|
});
|
|
141
|
-
if (!loaded) throw new Error('[BFF][Effect] Invalid Effect edge module. Export { api, layer }
|
|
141
|
+
if (!loaded) throw new Error('[BFF][Effect] Invalid Effect edge module. Export defineEffectBff(...) or a { api, layer } HttpApi module.');
|
|
142
142
|
return {
|
|
143
143
|
handler: (request, dispatchOptions = {})=>dispatchEffectBffRequest(loaded.handler, request, {
|
|
144
144
|
...dispatchOptions,
|
|
@@ -202,6 +202,41 @@ var __webpack_exports__ = {};
|
|
|
202
202
|
}
|
|
203
203
|
});
|
|
204
204
|
}
|
|
205
|
+
function createJsonValidationResponse(message, status = 400) {
|
|
206
|
+
return new Response(JSON.stringify({
|
|
207
|
+
message
|
|
208
|
+
}), {
|
|
209
|
+
status,
|
|
210
|
+
headers: {
|
|
211
|
+
'content-type': 'application/json; charset=utf-8'
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
function cloneWithoutJsonBodyHeaders(request) {
|
|
216
|
+
const headers = new Headers(request.headers);
|
|
217
|
+
headers.delete('content-type');
|
|
218
|
+
headers.delete('content-length');
|
|
219
|
+
return new Request(request.url, {
|
|
220
|
+
method: request.method,
|
|
221
|
+
headers,
|
|
222
|
+
signal: request.signal
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
async function prepareJsonRequestBody(request) {
|
|
226
|
+
const method = normalizeItemMethod(request.method);
|
|
227
|
+
if ('GET' === method || 'HEAD' === method) return request;
|
|
228
|
+
const contentType = (request.headers.get('content-type') || '').toLowerCase();
|
|
229
|
+
if (!contentType.includes('application/json')) return request;
|
|
230
|
+
if (null === request.body) return cloneWithoutJsonBodyHeaders(request);
|
|
231
|
+
try {
|
|
232
|
+
const bodyText = await request.clone().text();
|
|
233
|
+
if ('' === bodyText) return cloneWithoutJsonBodyHeaders(request);
|
|
234
|
+
JSON.parse(bodyText);
|
|
235
|
+
} catch {
|
|
236
|
+
return createJsonValidationResponse('Invalid JSON request body');
|
|
237
|
+
}
|
|
238
|
+
return request;
|
|
239
|
+
}
|
|
205
240
|
function toBatchItemError(id, status, message) {
|
|
206
241
|
return {
|
|
207
242
|
id,
|
|
@@ -422,7 +457,7 @@ var __webpack_exports__ = {};
|
|
|
422
457
|
]);
|
|
423
458
|
function createLoaderMaterializedClientPlaceholder() {
|
|
424
459
|
const explain = (property)=>{
|
|
425
|
-
throw new Error(`[BFF][Effect]
|
|
460
|
+
throw new Error(`[BFF][Effect] client.${String(property)} is not available here: the typed client only exists when the API entry is imported through the "@api/index" transformed path (the BFF loader replaces it with generated client code). On the server, use HttpApiClient or call the Effect layer directly.`);
|
|
426
461
|
};
|
|
427
462
|
return new Proxy(Object.create(null), {
|
|
428
463
|
get (_target, property) {
|
|
@@ -459,9 +494,11 @@ var __webpack_exports__ = {};
|
|
|
459
494
|
const withDataPlatformValidation = async (request, context)=>{
|
|
460
495
|
const policyDenial = options.validateRequest?.(request);
|
|
461
496
|
if (policyDenial) return policyDenial;
|
|
462
|
-
const
|
|
497
|
+
const preparedRequest = await prepareJsonRequestBody(request);
|
|
498
|
+
if (preparedRequest instanceof Response) return preparedRequest;
|
|
499
|
+
const validationError = validateDataPlatformRequestEnvelope(preparedRequest, options.dataPlatform);
|
|
463
500
|
if (validationError) return validationError;
|
|
464
|
-
return httpApiHandler.handler(
|
|
501
|
+
return httpApiHandler.handler(preparedRequest, context ?? emptyEffectServiceContext);
|
|
465
502
|
};
|
|
466
503
|
const handleBatchRequest = async (request, context)=>{
|
|
467
504
|
const mountedPrefix = getMountedPrefixFromContext(request, context);
|
|
@@ -49,16 +49,18 @@ function isEffectServiceContext(context) {
|
|
|
49
49
|
return 'object' == typeof context && null !== context && 'mapUnsafe' in context;
|
|
50
50
|
}
|
|
51
51
|
const emptyEffectServiceContext = Context_namespaceObject.empty();
|
|
52
|
+
const strictEffectApproachMessage = '[BFF][Effect] strictEffectApproach is enforced: Effect API entries must export defineEffectBff(...) or a { api, layer } HttpApi module. Raw handler exports, default request handlers, and unbranded custom createHandler factories are not valid Effect API entries.';
|
|
53
|
+
function rejectLegacyEffectModuleShape(options, shape) {
|
|
54
|
+
options.onWarning?.(`${strictEffectApproachMessage} Rejected ${shape}.`);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
52
57
|
function callEffectBffRequestHandler(handler, request, context) {
|
|
53
58
|
return void 0 === context ? handler(request) : handler(request, context);
|
|
54
59
|
}
|
|
55
|
-
function createLoadedHandler(webHandler
|
|
60
|
+
function createLoadedHandler(webHandler) {
|
|
56
61
|
return {
|
|
57
62
|
handler: (request, context)=>callEffectBffRequestHandler(webHandler.handler, request, context),
|
|
58
|
-
dispose: webHandler.dispose
|
|
59
|
-
...appliesRequestValidator ? {
|
|
60
|
-
appliesRequestValidator: true
|
|
61
|
-
} : {}
|
|
63
|
+
dispose: webHandler.dispose
|
|
62
64
|
};
|
|
63
65
|
}
|
|
64
66
|
function createLoadedHttpApiHandler(webHandler) {
|
|
@@ -67,18 +69,23 @@ function createLoadedHttpApiHandler(webHandler) {
|
|
|
67
69
|
const effectContext = isEffectServiceContext(context) ? context : emptyEffectServiceContext;
|
|
68
70
|
return webHandler.handler(request, effectContext);
|
|
69
71
|
},
|
|
70
|
-
dispose: webHandler.dispose
|
|
71
|
-
appliesRequestValidator: true
|
|
72
|
+
dispose: webHandler.dispose
|
|
72
73
|
};
|
|
73
74
|
}
|
|
74
75
|
function resolveNormalizedEffectBffModuleHandler(normalizedModule, options = {}) {
|
|
75
|
-
if (isRequestHandler(normalizedModule.handler))
|
|
76
|
-
handler
|
|
77
|
-
|
|
76
|
+
if (isRequestHandler(normalizedModule.handler)) {
|
|
77
|
+
if (rejectLegacyEffectModuleShape(options, '`handler` export')) return null;
|
|
78
|
+
return {
|
|
79
|
+
handler: normalizedModule.handler
|
|
80
|
+
};
|
|
81
|
+
}
|
|
78
82
|
const entry = normalizedModule.default;
|
|
79
|
-
if (isRequestHandler(entry))
|
|
80
|
-
handler
|
|
81
|
-
|
|
83
|
+
if (isRequestHandler(entry)) {
|
|
84
|
+
if (rejectLegacyEffectModuleShape(options, 'default request handler')) return null;
|
|
85
|
+
return {
|
|
86
|
+
handler: entry
|
|
87
|
+
};
|
|
88
|
+
}
|
|
82
89
|
if (isRecord(entry)) normalizedModule = {
|
|
83
90
|
...normalizedModule,
|
|
84
91
|
...entry
|
|
@@ -90,19 +97,22 @@ function resolveNormalizedEffectBffModuleHandler(normalizedModule, options = {})
|
|
|
90
97
|
handler: maybeHandler
|
|
91
98
|
};
|
|
92
99
|
}
|
|
93
|
-
if (isRequestHandler(normalizedModule.handler))
|
|
94
|
-
handler
|
|
95
|
-
|
|
100
|
+
if (isRequestHandler(normalizedModule.handler)) {
|
|
101
|
+
if (rejectLegacyEffectModuleShape(options, '`handler` export')) return null;
|
|
102
|
+
return {
|
|
103
|
+
handler: normalizedModule.handler
|
|
104
|
+
};
|
|
105
|
+
}
|
|
96
106
|
if ('function' == typeof normalizedModule.createHandler) {
|
|
97
107
|
const factory = normalizedModule.createHandler;
|
|
98
108
|
const validatorAware = (0, external_handler_js_namespaceObject.isValidatorAwareHandlerFactory)(factory);
|
|
99
|
-
if (!validatorAware &&
|
|
109
|
+
if (!validatorAware && rejectLegacyEffectModuleShape(options, 'unbranded `createHandler` export')) return null;
|
|
100
110
|
const webHandler = factory({
|
|
101
111
|
openapi: options.openapi,
|
|
102
112
|
dataPlatform: options.dataPlatform,
|
|
103
113
|
validateRequest: options.validateRequest
|
|
104
114
|
});
|
|
105
|
-
return createLoadedHandler(webHandler
|
|
115
|
+
return createLoadedHandler(webHandler);
|
|
106
116
|
}
|
|
107
117
|
if (isEffectApiDefinition(normalizedModule)) {
|
|
108
118
|
options.onWarning?.('[BFF][Effect] Detected { api, layer } export without createHandler. Prefer `defineEffectBff(...)` from @modern-js/plugin-bff/server to avoid module instance mismatch.');
|
|
@@ -126,17 +136,26 @@ function resolveEffectBffModuleHandler(mod, options = {}) {
|
|
|
126
136
|
...value
|
|
127
137
|
};
|
|
128
138
|
};
|
|
129
|
-
if (isRequestHandler(normalizedModule.handler))
|
|
130
|
-
handler
|
|
131
|
-
|
|
139
|
+
if (isRequestHandler(normalizedModule.handler)) {
|
|
140
|
+
if (rejectLegacyEffectModuleShape(options, '`handler` export')) return Promise.resolve(null);
|
|
141
|
+
return Promise.resolve({
|
|
142
|
+
handler: normalizedModule.handler
|
|
143
|
+
});
|
|
144
|
+
}
|
|
132
145
|
const entry = normalizedModule.default;
|
|
133
|
-
if (isRequestHandler(entry))
|
|
134
|
-
handler
|
|
135
|
-
|
|
146
|
+
if (isRequestHandler(entry)) {
|
|
147
|
+
if (rejectLegacyEffectModuleShape(options, 'default request handler')) return Promise.resolve(null);
|
|
148
|
+
return Promise.resolve({
|
|
149
|
+
handler: entry
|
|
150
|
+
});
|
|
151
|
+
}
|
|
136
152
|
if ('function' == typeof entry && 0 === entry.length) return Promise.resolve(entry()).then((out)=>{
|
|
137
|
-
if (isRequestHandler(out))
|
|
138
|
-
handler
|
|
139
|
-
|
|
153
|
+
if (isRequestHandler(out)) {
|
|
154
|
+
if (rejectLegacyEffectModuleShape(options, 'default factory returning a request handler')) return null;
|
|
155
|
+
return {
|
|
156
|
+
handler: out
|
|
157
|
+
};
|
|
158
|
+
}
|
|
140
159
|
mergeRuntimeExports(out);
|
|
141
160
|
return resolveNormalizedEffectBffModuleHandler(normalizedModule, options);
|
|
142
161
|
});
|
|
@@ -35,6 +35,7 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
35
35
|
HttpApiEndpoint: ()=>httpapi_namespaceObject.HttpApiEndpoint,
|
|
36
36
|
HttpApiGroup: ()=>httpapi_namespaceObject.HttpApiGroup,
|
|
37
37
|
HttpApiSchema: ()=>httpapi_namespaceObject.HttpApiSchema,
|
|
38
|
+
HttpClientError: ()=>HttpClientError_namespaceObject,
|
|
38
39
|
Layer: ()=>Layer_namespaceObject,
|
|
39
40
|
Rpc: ()=>rpc_namespaceObject.Rpc,
|
|
40
41
|
RpcClient: ()=>rpc_namespaceObject.RpcClient,
|
|
@@ -60,6 +61,7 @@ const http_namespaceObject = require("effect/unstable/http");
|
|
|
60
61
|
const httpapi_namespaceObject = require("effect/unstable/httpapi");
|
|
61
62
|
const rpc_namespaceObject = require("effect/unstable/rpc");
|
|
62
63
|
const Schema_namespaceObject = require("effect/Schema");
|
|
64
|
+
const HttpClientError_namespaceObject = require("effect/unstable/http/HttpClientError");
|
|
63
65
|
class EffectRpcClientError extends Data_namespaceObject.TaggedError('EffectRpcClientError') {
|
|
64
66
|
}
|
|
65
67
|
function isRecord(value) {
|
|
@@ -162,6 +164,7 @@ exports.HttpApiClient = __webpack_exports__.HttpApiClient;
|
|
|
162
164
|
exports.HttpApiEndpoint = __webpack_exports__.HttpApiEndpoint;
|
|
163
165
|
exports.HttpApiGroup = __webpack_exports__.HttpApiGroup;
|
|
164
166
|
exports.HttpApiSchema = __webpack_exports__.HttpApiSchema;
|
|
167
|
+
exports.HttpClientError = __webpack_exports__.HttpClientError;
|
|
165
168
|
exports.Layer = __webpack_exports__.Layer;
|
|
166
169
|
exports.Rpc = __webpack_exports__.Rpc;
|
|
167
170
|
exports.RpcClient = __webpack_exports__.RpcClient;
|
|
@@ -183,6 +186,7 @@ for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
|
183
186
|
"HttpApiEndpoint",
|
|
184
187
|
"HttpApiGroup",
|
|
185
188
|
"HttpApiSchema",
|
|
189
|
+
"HttpClientError",
|
|
186
190
|
"Layer",
|
|
187
191
|
"Rpc",
|
|
188
192
|
"RpcClient",
|
|
@@ -172,14 +172,14 @@ const __generated = createGeneratedEffectClient(__manifest, __config, __requestR
|
|
|
172
172
|
const client = __generated.client;
|
|
173
173
|
const operationManifest = __generated.operationManifest;
|
|
174
174
|
const createEffectRequestContext = __generated.createEffectRequestContext;
|
|
175
|
-
const
|
|
175
|
+
const apiModule = {
|
|
176
176
|
client,
|
|
177
177
|
operationManifest,
|
|
178
178
|
createEffectRequestContext,
|
|
179
179
|
};
|
|
180
180
|
|
|
181
181
|
export { client, createEffectRequestContext, operationManifest };
|
|
182
|
-
export default
|
|
182
|
+
export default apiModule;
|
|
183
183
|
`;
|
|
184
184
|
}
|
|
185
185
|
function renderClientShape(endpoints, valueType) {
|
|
@@ -252,13 +252,13 @@ export declare const createEffectRequestContext: (
|
|
|
252
252
|
requestContext: Record<string, unknown>,
|
|
253
253
|
) => EffectRequestContext;
|
|
254
254
|
export declare const operationManifest: GeneratedEffectOperationManifest;
|
|
255
|
-
declare const
|
|
255
|
+
declare const apiModule: {
|
|
256
256
|
client: GeneratedEffectClient;
|
|
257
257
|
createEffectRequestContext: typeof createEffectRequestContext;
|
|
258
258
|
operationManifest: GeneratedEffectOperationManifest;
|
|
259
259
|
};
|
|
260
260
|
|
|
261
|
-
export default
|
|
261
|
+
export default apiModule;
|
|
262
262
|
`;
|
|
263
263
|
}
|
|
264
264
|
async function generateEffectClient(options) {
|
|
@@ -281,10 +281,15 @@ async function generateEffectClientCode(options) {
|
|
|
281
281
|
}
|
|
282
282
|
function resolveEffectEntryFile(options) {
|
|
283
283
|
const { appDir, apiDir, effectEntry } = options;
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
284
|
+
const resolveEntry = (entryWithoutExt)=>{
|
|
285
|
+
if (external_path_default().extname(entryWithoutExt)) return utils_namespaceObject.fs.existsSync(entryWithoutExt) ? entryWithoutExt : void 0;
|
|
286
|
+
return (0, utils_namespaceObject.findExists)(JS_OR_TS_EXTS.map((ext)=>`${entryWithoutExt}${ext}`));
|
|
287
|
+
};
|
|
288
|
+
if (effectEntry) {
|
|
289
|
+
const entryWithoutExt = external_path_default().isAbsolute(effectEntry) ? effectEntry : external_path_default().resolve(appDir, effectEntry);
|
|
290
|
+
return resolveEntry(entryWithoutExt);
|
|
291
|
+
}
|
|
292
|
+
return resolveEntry(external_path_default().resolve(apiDir, 'index'));
|
|
288
293
|
}
|
|
289
294
|
exports.generateEffectClient = __webpack_exports__.generateEffectClient;
|
|
290
295
|
exports.generateEffectClientCode = __webpack_exports__.generateEffectClientCode;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { ApiRouter } from "@modern-js/bff-core";
|
|
2
1
|
import { API_DIR, compatibleRequire, findExists, fs, isProd, logger } from "@modern-js/utils";
|
|
3
2
|
import { HttpApi } from "effect/unstable/httpapi";
|
|
4
3
|
import path from "path";
|
|
@@ -52,43 +51,12 @@ class EffectAdapter {
|
|
|
52
51
|
const { appDirectory, apiDirectory } = this.api.getServerContext();
|
|
53
52
|
const bffConfig = this.api.getServerConfig()?.bff;
|
|
54
53
|
const configuredEntry = bffConfig?.effect?.entry;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
isApiRequestPath(requestPath, prefix, enableHandleWeb) {
|
|
60
|
-
if (!enableHandleWeb) return true;
|
|
61
|
-
const normalized = normalizePrefix(prefix);
|
|
62
|
-
if (!normalized) return true;
|
|
63
|
-
return requestPath === normalized || requestPath.startsWith(`${normalized}/`);
|
|
64
|
-
}
|
|
65
|
-
async collectLambdaContractSources() {
|
|
66
|
-
try {
|
|
67
|
-
const serverContext = this.api.getServerContext();
|
|
68
|
-
const appDir = serverContext.distDirectory || serverContext.appDirectory;
|
|
69
|
-
if (!appDir) return [];
|
|
70
|
-
const apiDir = 'string' == typeof serverContext.apiDirectory ? serverContext.apiDirectory : path.resolve(appDir, API_DIR);
|
|
71
|
-
const lambdaDir = 'string' == typeof serverContext.lambdaDirectory ? serverContext.lambdaDirectory : path.join(apiDir, 'lambda');
|
|
72
|
-
if (!await fs.pathExists(lambdaDir)) return [];
|
|
73
|
-
const apiRouter = new ApiRouter({
|
|
74
|
-
appDir,
|
|
75
|
-
apiDir,
|
|
76
|
-
lambdaDir,
|
|
77
|
-
prefix: this.prefix,
|
|
78
|
-
httpMethodDecider: this.api.getServerConfig()?.bff?.httpMethodDecider
|
|
79
|
-
});
|
|
80
|
-
const handlerInfos = await apiRouter.getApiHandlers();
|
|
81
|
-
return handlerInfos.map((info)=>({
|
|
82
|
-
name: info.name,
|
|
83
|
-
httpMethod: info.httpMethod,
|
|
84
|
-
routePath: info.routePath,
|
|
85
|
-
filename: info.filename,
|
|
86
|
-
handler: info.handler
|
|
87
|
-
}));
|
|
88
|
-
} catch (error) {
|
|
89
|
-
logger.warn(`[BFF][Effect] Failed to derive lambda operation contracts for the cross-project policy: ${String(error)}`);
|
|
90
|
-
return [];
|
|
54
|
+
if (configuredEntry) {
|
|
55
|
+
const entryWithoutExt = path.isAbsolute(configuredEntry) ? configuredEntry : path.resolve(appDirectory || process.cwd(), configuredEntry);
|
|
56
|
+
return resolveJsOrTsEntry(entryWithoutExt);
|
|
91
57
|
}
|
|
58
|
+
const apiRoot = path.resolve(appDirectory || process.cwd(), apiDirectory || API_DIR);
|
|
59
|
+
return resolveJsOrTsEntry(path.resolve(apiRoot, 'index'));
|
|
92
60
|
}
|
|
93
61
|
async refreshCrossProjectPolicy(mod) {
|
|
94
62
|
let contractSources = [];
|
|
@@ -104,24 +72,15 @@ class EffectAdapter {
|
|
|
104
72
|
} catch (error) {
|
|
105
73
|
logger.warn(`[BFF][Effect] Failed to reflect HttpApi endpoints for the cross-project policy: ${String(error)}`);
|
|
106
74
|
}
|
|
107
|
-
|
|
108
|
-
if (policy?.enabled) {
|
|
109
|
-
const lambdaSources = await this.collectLambdaContractSources();
|
|
110
|
-
if (lambdaSources.length > 0) {
|
|
111
|
-
contractSources = [
|
|
112
|
-
...contractSources,
|
|
113
|
-
...lambdaSources
|
|
114
|
-
];
|
|
115
|
-
policy = resolveAdapterCrossProjectPolicy(this.api, contractSources);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
75
|
+
const policy = resolveAdapterCrossProjectPolicy(this.api, contractSources);
|
|
118
76
|
this.crossProjectPolicy = policy;
|
|
119
77
|
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).');
|
|
120
78
|
}
|
|
121
79
|
async loadEffectHandlerFromModule(mod) {
|
|
80
|
+
const effectConfig = this.api.getServerConfig()?.bff?.effect;
|
|
122
81
|
return resolveEffectBffModuleHandler(mod, {
|
|
123
|
-
openapi:
|
|
124
|
-
dataPlatform:
|
|
82
|
+
openapi: effectConfig?.openapi,
|
|
83
|
+
dataPlatform: effectConfig?.dataPlatform,
|
|
125
84
|
validateRequest: (request)=>checkCrossProjectPolicyForRequest(request, this.crossProjectPolicy),
|
|
126
85
|
onWarning: (message)=>{
|
|
127
86
|
logger.warn(message);
|
|
@@ -155,13 +114,12 @@ class EffectAdapter {
|
|
|
155
114
|
await this.refreshCrossProjectPolicy(mod);
|
|
156
115
|
const loaded = await this.loadEffectHandlerFromModule(mod);
|
|
157
116
|
if (!loaded) {
|
|
158
|
-
logger.warn(`[BFF][Effect] Invalid Effect entry module: ${entryFile}. Export { api, layer }
|
|
117
|
+
logger.warn(`[BFF][Effect] Invalid Effect entry module: ${entryFile}. Export defineEffectBff(...) or a { api, layer } HttpApi module.`);
|
|
159
118
|
this.handler = null;
|
|
160
119
|
return;
|
|
161
120
|
}
|
|
162
121
|
this.handler = loaded.handler;
|
|
163
122
|
this.dispose = loaded.dispose || null;
|
|
164
|
-
this.policyEnforcedInMiddleware = !loaded.appliesRequestValidator;
|
|
165
123
|
}
|
|
166
124
|
async disposeCurrentHandler() {
|
|
167
125
|
if (!this.dispose) return;
|
|
@@ -213,7 +171,6 @@ class EffectAdapter {
|
|
|
213
171
|
this.handler = null;
|
|
214
172
|
this.dispose = null;
|
|
215
173
|
this.prefix = '/api';
|
|
216
|
-
this.policyEnforcedInMiddleware = false;
|
|
217
174
|
this.registerMiddleware = async (options)=>{
|
|
218
175
|
const { prefix, enableHandleWeb } = options;
|
|
219
176
|
const { bffRuntimeFramework, middlewares: globalMiddlewares } = this.api.getServerContext();
|
|
@@ -224,7 +181,7 @@ class EffectAdapter {
|
|
|
224
181
|
this.prefix = prefix || this.prefix;
|
|
225
182
|
await this.reloadHandler();
|
|
226
183
|
this.effectMiddleware = {
|
|
227
|
-
name: 'effect-
|
|
184
|
+
name: 'effect-api-handler',
|
|
228
185
|
path: enableHandleWeb ? '*' : `${prefix}/*`,
|
|
229
186
|
method: 'all',
|
|
230
187
|
order: 'post',
|
|
@@ -232,11 +189,7 @@ class EffectAdapter {
|
|
|
232
189
|
handler: async (c, next)=>{
|
|
233
190
|
if (!this.handler) {
|
|
234
191
|
if (enableHandleWeb) return void await next();
|
|
235
|
-
return this.handleRuntimeError(new Error('[BFF][Effect] Missing Effect entry. Define api/
|
|
236
|
-
}
|
|
237
|
-
if (this.crossProjectPolicy?.enabled && this.policyEnforcedInMiddleware && this.isApiRequestPath(c.req.path, prefix, enableHandleWeb)) {
|
|
238
|
-
const denial = checkCrossProjectPolicyForRequest(c.req.raw, this.crossProjectPolicy);
|
|
239
|
-
if (denial) return denial;
|
|
192
|
+
return this.handleRuntimeError(new Error('[BFF][Effect] Missing Effect entry. Define api/index or configure bff.effect.entry.'), c);
|
|
240
193
|
}
|
|
241
194
|
let response;
|
|
242
195
|
try {
|
|
@@ -68,7 +68,7 @@ async function createEffectBffEdgeHandler(options) {
|
|
|
68
68
|
dataPlatform: options.dataPlatform,
|
|
69
69
|
onWarning: options.onWarning
|
|
70
70
|
});
|
|
71
|
-
if (!loaded) throw new Error('[BFF][Effect] Invalid Effect edge module. Export { api, layer }
|
|
71
|
+
if (!loaded) throw new Error('[BFF][Effect] Invalid Effect edge module. Export defineEffectBff(...) or a { api, layer } HttpApi module.');
|
|
72
72
|
return {
|
|
73
73
|
handler: (request, dispatchOptions = {})=>dispatchEffectBffRequest(loaded.handler, request, {
|
|
74
74
|
...dispatchOptions,
|
|
@@ -74,6 +74,41 @@ function createBatchValidationResponse(message, status = 400) {
|
|
|
74
74
|
}
|
|
75
75
|
});
|
|
76
76
|
}
|
|
77
|
+
function createJsonValidationResponse(message, status = 400) {
|
|
78
|
+
return new Response(JSON.stringify({
|
|
79
|
+
message
|
|
80
|
+
}), {
|
|
81
|
+
status,
|
|
82
|
+
headers: {
|
|
83
|
+
'content-type': 'application/json; charset=utf-8'
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function cloneWithoutJsonBodyHeaders(request) {
|
|
88
|
+
const headers = new Headers(request.headers);
|
|
89
|
+
headers.delete('content-type');
|
|
90
|
+
headers.delete('content-length');
|
|
91
|
+
return new Request(request.url, {
|
|
92
|
+
method: request.method,
|
|
93
|
+
headers,
|
|
94
|
+
signal: request.signal
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
async function prepareJsonRequestBody(request) {
|
|
98
|
+
const method = normalizeItemMethod(request.method);
|
|
99
|
+
if ('GET' === method || 'HEAD' === method) return request;
|
|
100
|
+
const contentType = (request.headers.get('content-type') || '').toLowerCase();
|
|
101
|
+
if (!contentType.includes('application/json')) return request;
|
|
102
|
+
if (null === request.body) return cloneWithoutJsonBodyHeaders(request);
|
|
103
|
+
try {
|
|
104
|
+
const bodyText = await request.clone().text();
|
|
105
|
+
if ('' === bodyText) return cloneWithoutJsonBodyHeaders(request);
|
|
106
|
+
JSON.parse(bodyText);
|
|
107
|
+
} catch {
|
|
108
|
+
return createJsonValidationResponse('Invalid JSON request body');
|
|
109
|
+
}
|
|
110
|
+
return request;
|
|
111
|
+
}
|
|
77
112
|
function toBatchItemError(id, status, message) {
|
|
78
113
|
return {
|
|
79
114
|
id,
|
|
@@ -294,7 +329,7 @@ const LOADER_CLIENT_IGNORED_KEYS = new Set([
|
|
|
294
329
|
]);
|
|
295
330
|
function createLoaderMaterializedClientPlaceholder() {
|
|
296
331
|
const explain = (property)=>{
|
|
297
|
-
throw new Error(`[BFF][Effect]
|
|
332
|
+
throw new Error(`[BFF][Effect] client.${String(property)} is not available here: the typed client only exists when the API entry is imported through the "@api/index" transformed path (the BFF loader replaces it with generated client code). On the server, use HttpApiClient or call the Effect layer directly.`);
|
|
298
333
|
};
|
|
299
334
|
return new Proxy(Object.create(null), {
|
|
300
335
|
get (_target, property) {
|
|
@@ -331,9 +366,11 @@ function createHttpApiHandler(options) {
|
|
|
331
366
|
const withDataPlatformValidation = async (request, context)=>{
|
|
332
367
|
const policyDenial = options.validateRequest?.(request);
|
|
333
368
|
if (policyDenial) return policyDenial;
|
|
334
|
-
const
|
|
369
|
+
const preparedRequest = await prepareJsonRequestBody(request);
|
|
370
|
+
if (preparedRequest instanceof Response) return preparedRequest;
|
|
371
|
+
const validationError = validateDataPlatformRequestEnvelope(preparedRequest, options.dataPlatform);
|
|
335
372
|
if (validationError) return validationError;
|
|
336
|
-
return httpApiHandler.handler(
|
|
373
|
+
return httpApiHandler.handler(preparedRequest, context ?? emptyEffectServiceContext);
|
|
337
374
|
};
|
|
338
375
|
const handleBatchRequest = async (request, context)=>{
|
|
339
376
|
const mountedPrefix = getMountedPrefixFromContext(request, context);
|