@x12i/helpers 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -0
- package/index.js +3 -0
- package/package.json +8 -2
- package/src/helpers/api-contracts.js +398 -0
- package/src/helpers/apiMapper.js +459 -0
- package/src/helpers/firebaseRtdb.js +471 -0
- package/src/helpers/json-mapper.js +3 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ╔══════════════════════════════════════════════════════════════╗
|
|
3
|
+
* ║ API MAPPER ║
|
|
4
|
+
* ╠══════════════════════════════════════════════════════════════╣
|
|
5
|
+
* ║ Translates API calls between two contracts/protocols. ║
|
|
6
|
+
* ║ REST ↔ GraphQL ↔ JSON-RPC ↔ SOAP ↔ Custom ║
|
|
7
|
+
* ║ ║
|
|
8
|
+
* ║ Uses json-mapper.js for deep property mapping of both ║
|
|
9
|
+
* ║ request params and response bodies. ║
|
|
10
|
+
* ╚══════════════════════════════════════════════════════════════╝
|
|
11
|
+
*
|
|
12
|
+
* BRIDGE CONFIG:
|
|
13
|
+
* ┌──────────────────────────────────────────────────────────────┐
|
|
14
|
+
* │ { │
|
|
15
|
+
* │ source: Contract, // caller's view │
|
|
16
|
+
* │ target: Contract, // actual API │
|
|
17
|
+
* │ requestMapping: [...], // json-mapper: src → tgt │
|
|
18
|
+
* │ responseMapping: [...], // json-mapper: tgt → src │
|
|
19
|
+
* │ beforeRequest: fn, // middleware │
|
|
20
|
+
* │ afterResponse: fn, // middleware │
|
|
21
|
+
* │ errorMapping: {...}, // status→handler │
|
|
22
|
+
* │ retry: {...}, // attempts/backoff │
|
|
23
|
+
* │ credentials: {...}, // per-side auth │
|
|
24
|
+
* │ } │
|
|
25
|
+
* └──────────────────────────────────────────────────────────────┘
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const { mapObject } = require("./json-mapper.js");
|
|
29
|
+
const { defineContract } = require("./api-contracts.js");
|
|
30
|
+
|
|
31
|
+
// ═══════════════════════════════════════════════════════════════
|
|
32
|
+
// HELPERS
|
|
33
|
+
// ═══════════════════════════════════════════════════════════════
|
|
34
|
+
|
|
35
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
36
|
+
|
|
37
|
+
function buildUrl(base, query = {}) {
|
|
38
|
+
const qs = Object.entries(query)
|
|
39
|
+
.filter(([, v]) => v != null)
|
|
40
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
|
41
|
+
.join("&");
|
|
42
|
+
return qs ? `${base}?${qs}` : base;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resolveContract(ref, registry) {
|
|
46
|
+
if (typeof ref === "string") {
|
|
47
|
+
if (!registry) throw new Error(`String contract "${ref}" needs a registry`);
|
|
48
|
+
return registry.get(ref);
|
|
49
|
+
}
|
|
50
|
+
return ref._protocol ? ref : defineContract(ref);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ═══════════════════════════════════════════════════════════════
|
|
54
|
+
// DEFAULT HTTP CLIENT (Node 18+ / browser fetch)
|
|
55
|
+
// ═══════════════════════════════════════════════════════════════
|
|
56
|
+
|
|
57
|
+
async function defaultHttpClient(req) {
|
|
58
|
+
const url = buildUrl(req.url, req.query);
|
|
59
|
+
const opts = { method: req.method, headers: req.headers || {} };
|
|
60
|
+
if (req.body !== undefined) {
|
|
61
|
+
opts.body = typeof req.body === "string" ? req.body : JSON.stringify(req.body);
|
|
62
|
+
}
|
|
63
|
+
const res = await fetch(url, opts);
|
|
64
|
+
let body;
|
|
65
|
+
const ct = res.headers.get("content-type") || "";
|
|
66
|
+
if (ct.includes("json")) body = await res.json();
|
|
67
|
+
else if (ct.includes("xml") || ct.includes("text")) body = await res.text();
|
|
68
|
+
else { try { body = await res.json(); } catch { body = await res.text(); } }
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
status: res.status,
|
|
72
|
+
statusText: res.statusText,
|
|
73
|
+
headers: Object.fromEntries(res.headers.entries()),
|
|
74
|
+
body,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ═══════════════════════════════════════════════════════════════
|
|
79
|
+
// RETRY
|
|
80
|
+
// ═══════════════════════════════════════════════════════════════
|
|
81
|
+
|
|
82
|
+
async function executeWithRetry(executor, request, cfg) {
|
|
83
|
+
const {
|
|
84
|
+
attempts = 3,
|
|
85
|
+
delay = 1000,
|
|
86
|
+
backoff = "exponential",
|
|
87
|
+
retryOn = (r) => r.status >= 500,
|
|
88
|
+
} = cfg;
|
|
89
|
+
|
|
90
|
+
let last;
|
|
91
|
+
for (let i = 0; i < attempts; i++) {
|
|
92
|
+
last = await executor(request);
|
|
93
|
+
if (!retryOn(last)) return last;
|
|
94
|
+
if (i < attempts - 1) {
|
|
95
|
+
const wait =
|
|
96
|
+
backoff === "exponential" ? delay * 2 ** i :
|
|
97
|
+
backoff === "linear" ? delay * (i + 1) : delay;
|
|
98
|
+
await sleep(wait);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return last;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ═══════════════════════════════════════════════════════════════
|
|
105
|
+
// CORE: createBridge
|
|
106
|
+
// ═══════════════════════════════════════════════════════════════
|
|
107
|
+
|
|
108
|
+
function createBridge(config) {
|
|
109
|
+
const {
|
|
110
|
+
requestMapping = [],
|
|
111
|
+
responseMapping = [],
|
|
112
|
+
beforeRequest,
|
|
113
|
+
afterResponse,
|
|
114
|
+
errorMapping = {},
|
|
115
|
+
retry: retryConfig,
|
|
116
|
+
credentials = {},
|
|
117
|
+
registry,
|
|
118
|
+
httpClient,
|
|
119
|
+
} = config;
|
|
120
|
+
|
|
121
|
+
const source = resolveContract(config.source, registry);
|
|
122
|
+
const target = resolveContract(config.target, registry);
|
|
123
|
+
|
|
124
|
+
async function call(sourceParams, opts = {}) {
|
|
125
|
+
const creds = { ...credentials, ...opts.credentials };
|
|
126
|
+
|
|
127
|
+
// 1 — validate source params
|
|
128
|
+
const sv = source.validateRequest(sourceParams);
|
|
129
|
+
if (!sv.valid) {
|
|
130
|
+
return { ok: false, error: "Source validation failed", validationErrors: sv.errors, data: null };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 2 — map source params → target params
|
|
134
|
+
let targetParams = requestMapping.length
|
|
135
|
+
? mapObject(sourceParams, requestMapping)
|
|
136
|
+
: { ...sourceParams };
|
|
137
|
+
|
|
138
|
+
// 3 — build normalized HTTP request via target protocol
|
|
139
|
+
let targetReq = target.buildRequest(targetParams, creds.target);
|
|
140
|
+
|
|
141
|
+
// 4 — middleware: beforeRequest
|
|
142
|
+
if (beforeRequest) targetReq = beforeRequest(targetReq, sourceParams) || targetReq;
|
|
143
|
+
if (opts.beforeRequest) targetReq = opts.beforeRequest(targetReq, sourceParams) || targetReq;
|
|
144
|
+
|
|
145
|
+
// 5 — dry-run: return translated request without calling
|
|
146
|
+
if (opts.dryRun) {
|
|
147
|
+
return {
|
|
148
|
+
ok: true, dryRun: true,
|
|
149
|
+
sourceContract: source.name, targetContract: target.name,
|
|
150
|
+
sourceParams, targetParams, targetRequest: targetReq, data: null,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 6 — execute
|
|
155
|
+
const exec = httpClient || opts.httpClient || defaultHttpClient;
|
|
156
|
+
const rawRes = retryConfig
|
|
157
|
+
? await executeWithRetry(exec, targetReq, retryConfig)
|
|
158
|
+
: await exec(targetReq);
|
|
159
|
+
|
|
160
|
+
// 7 — parse via target protocol
|
|
161
|
+
const parsed = target.parseResponse(rawRes);
|
|
162
|
+
|
|
163
|
+
// 8 — error mapping
|
|
164
|
+
if (parsed.error) {
|
|
165
|
+
const h = errorMapping[String(rawRes.status)] || errorMapping["*"];
|
|
166
|
+
const err = h ? h(parsed.error, rawRes) : parsed.error;
|
|
167
|
+
return { ok: false, error: err, status: parsed.status, data: null, raw: parsed };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 9 — map response data → source shape
|
|
171
|
+
let data = parsed.data;
|
|
172
|
+
if (responseMapping.length && data && typeof data === "object") {
|
|
173
|
+
data = mapObject(data, responseMapping);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 10 — validate response
|
|
177
|
+
const rv = source.validateResponse(data);
|
|
178
|
+
|
|
179
|
+
// 11 — middleware: afterResponse
|
|
180
|
+
if (afterResponse) data = afterResponse(data, parsed) || data;
|
|
181
|
+
if (opts.afterResponse) data = opts.afterResponse(data, parsed) || data;
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
ok: true, status: parsed.status, data,
|
|
185
|
+
headers: parsed.headers, raw: parsed,
|
|
186
|
+
...(rv.valid ? {} : { responseWarnings: rv.errors }),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function explain() {
|
|
191
|
+
return {
|
|
192
|
+
from: { name: source.name, protocol: source.protocol },
|
|
193
|
+
to: { name: target.name, protocol: target.protocol },
|
|
194
|
+
requestMappingRules: requestMapping.length,
|
|
195
|
+
responseMappingRules: responseMapping.length,
|
|
196
|
+
hasMiddleware: !!(beforeRequest || afterResponse),
|
|
197
|
+
hasRetry: !!retryConfig,
|
|
198
|
+
hasErrorMapping: Object.keys(errorMapping).length > 0,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return { call, explain, source, target };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ═══════════════════════════════════════════════════════════════
|
|
206
|
+
// BATCH
|
|
207
|
+
// ═══════════════════════════════════════════════════════════════
|
|
208
|
+
|
|
209
|
+
async function batchCall(bridge, paramsList, opts = {}) {
|
|
210
|
+
const { concurrency = 5, onProgress } = opts;
|
|
211
|
+
|
|
212
|
+
if (concurrency >= paramsList.length) {
|
|
213
|
+
return Promise.all(
|
|
214
|
+
paramsList.map((p, i) =>
|
|
215
|
+
bridge.call(p, opts).then((r) => { if (onProgress) onProgress(i, r); return r; })
|
|
216
|
+
)
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
const results = new Array(paramsList.length);
|
|
220
|
+
let cursor = 0;
|
|
221
|
+
async function worker() {
|
|
222
|
+
while (cursor < paramsList.length) {
|
|
223
|
+
const idx = cursor++;
|
|
224
|
+
results[idx] = await bridge.call(paramsList[idx], opts);
|
|
225
|
+
if (onProgress) onProgress(idx, results[idx]);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
await Promise.all(Array.from({ length: concurrency }, worker));
|
|
229
|
+
return results;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ═══════════════════════════════════════════════════════════════
|
|
233
|
+
// CHAIN (pipe: A→B→C)
|
|
234
|
+
// ═══════════════════════════════════════════════════════════════
|
|
235
|
+
|
|
236
|
+
function chainBridges(...bridges) {
|
|
237
|
+
return {
|
|
238
|
+
async call(sourceParams, opts = {}) {
|
|
239
|
+
let current = sourceParams;
|
|
240
|
+
const trace = [];
|
|
241
|
+
for (const b of bridges) {
|
|
242
|
+
const r = await b.call(current, opts);
|
|
243
|
+
trace.push({ from: b.source.name, to: b.target.name, ok: r.ok, status: r.status });
|
|
244
|
+
if (!r.ok) return { ...r, trace };
|
|
245
|
+
current = r.data;
|
|
246
|
+
}
|
|
247
|
+
return { ok: true, data: current, trace };
|
|
248
|
+
},
|
|
249
|
+
explain: () => bridges.map((b) => b.explain()),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ═══════════════════════════════════════════════════════════════
|
|
254
|
+
|
|
255
|
+
module.exports = { createBridge, batchCall, chainBridges, defaultHttpClient };
|
|
256
|
+
|
|
257
|
+
// ══════════════════════════════════════════════════════════════════
|
|
258
|
+
// DEMO — node api-mapper.js
|
|
259
|
+
// ══════════════════════════════════════════════════════════════════
|
|
260
|
+
|
|
261
|
+
if (require.main === module) {
|
|
262
|
+
const { defineContract } = require("./api-contracts.js");
|
|
263
|
+
|
|
264
|
+
// ── Mock HTTP (no real network) ──
|
|
265
|
+
function mockHttp(req) {
|
|
266
|
+
console.log(`\n ┌─ TARGET HTTP REQUEST ─────────────────────`);
|
|
267
|
+
console.log(` │ ${req.method} ${buildUrl(req.url, req.query)}`);
|
|
268
|
+
console.log(` │ Content-Type: ${req.headers["Content-Type"]}`);
|
|
269
|
+
if (req.headers.Authorization) console.log(` │ Auth: ${req.headers.Authorization}`);
|
|
270
|
+
if (req.headers.SOAPAction) console.log(` │ SOAPAction: ${req.headers.SOAPAction}`);
|
|
271
|
+
if (req.body) {
|
|
272
|
+
const preview = typeof req.body === "string"
|
|
273
|
+
? req.body.substring(0, 200)
|
|
274
|
+
: JSON.stringify(req.body, null, 2);
|
|
275
|
+
preview.split("\n").forEach((l) => console.log(` │ ${l}`));
|
|
276
|
+
}
|
|
277
|
+
console.log(` └─────────────────────────────────────────────`);
|
|
278
|
+
|
|
279
|
+
const ct = req.headers?.["Content-Type"] || "";
|
|
280
|
+
if (ct.includes("xml")) {
|
|
281
|
+
return Promise.resolve({ status: 200, headers: { "content-type": "text/xml" },
|
|
282
|
+
body: `<GetStockPriceResponse><Price>182.63</Price><Currency>USD</Currency></GetStockPriceResponse>` });
|
|
283
|
+
}
|
|
284
|
+
if (req.body?.jsonrpc) {
|
|
285
|
+
return Promise.resolve({ status: 200, headers: { "content-type": "application/json" },
|
|
286
|
+
body: { jsonrpc: "2.0", id: req.body.id, result: { balance: "3.14159", unit: "ETH" } } });
|
|
287
|
+
}
|
|
288
|
+
if (req.body?.query) {
|
|
289
|
+
return Promise.resolve({ status: 200, headers: { "content-type": "application/json" },
|
|
290
|
+
body: { data: { user: { id: "42", fullName: "Jane Doe", emailAddress: "jane@example.com", role: "ADMIN" } } } });
|
|
291
|
+
}
|
|
292
|
+
return Promise.resolve({ status: 200, headers: { "content-type": "application/json" },
|
|
293
|
+
body: { id: 42, name: "Jane Doe", email: "jane@example.com", role: "admin" } });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function demo() {
|
|
297
|
+
|
|
298
|
+
// ═════════════════════════════════════════
|
|
299
|
+
// 1: REST → GraphQL
|
|
300
|
+
// ═════════════════════════════════════════
|
|
301
|
+
console.log("\n╔═══════════════════════════════════════════════════╗");
|
|
302
|
+
console.log("║ 1 — REST caller → GraphQL backend ║");
|
|
303
|
+
console.log("╚═══════════════════════════════════════════════════╝");
|
|
304
|
+
|
|
305
|
+
const restGetUser = defineContract({
|
|
306
|
+
name: "restGetUser", protocol: "rest",
|
|
307
|
+
endpoint: "/api/users/{id}", method: "GET",
|
|
308
|
+
auth: { type: "bearer" },
|
|
309
|
+
requestSchema: {
|
|
310
|
+
pathParams: { type: "object", required: true,
|
|
311
|
+
properties: { id: { type: "string", required: true } } },
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const gqlGetUser = defineContract({
|
|
316
|
+
name: "gqlGetUser", protocol: "graphql",
|
|
317
|
+
endpoint: "https://api.example.com/graphql",
|
|
318
|
+
query: `query GetUser($userId: ID!) { user(id: $userId) { id fullName emailAddress role } }`,
|
|
319
|
+
auth: { type: "bearer" },
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const bridge1 = createBridge({
|
|
323
|
+
source: restGetUser,
|
|
324
|
+
target: gqlGetUser,
|
|
325
|
+
requestMapping: [
|
|
326
|
+
{ from: "pathParams.id", to: "variables.userId" },
|
|
327
|
+
],
|
|
328
|
+
responseMapping: [
|
|
329
|
+
"user.id",
|
|
330
|
+
"user.fullName -> user.name",
|
|
331
|
+
"user.emailAddress -> user.email",
|
|
332
|
+
{ from: "user.role", to: "user.role", transform: (v) => v?.toLowerCase() },
|
|
333
|
+
],
|
|
334
|
+
credentials: { target: { token: "gql-token-abc" } },
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const r1 = await bridge1.call({ pathParams: { id: "42" } }, { httpClient: mockHttp });
|
|
338
|
+
console.log("\n ✅ MAPPED RESPONSE (REST shape):", JSON.stringify(r1.data, null, 4));
|
|
339
|
+
|
|
340
|
+
// ═════════════════════════════════════════
|
|
341
|
+
// 2: REST → JSON-RPC
|
|
342
|
+
// ═════════════════════════════════════════
|
|
343
|
+
console.log("\n╔═══════════════════════════════════════════════════╗");
|
|
344
|
+
console.log("║ 2 — REST caller → JSON-RPC backend ║");
|
|
345
|
+
console.log("╚═══════════════════════════════════════════════════╝");
|
|
346
|
+
|
|
347
|
+
const restBalance = defineContract({
|
|
348
|
+
name: "restGetBalance", protocol: "rest",
|
|
349
|
+
endpoint: "/api/wallets/{address}/balance", method: "GET",
|
|
350
|
+
});
|
|
351
|
+
const rpcBalance = defineContract({
|
|
352
|
+
name: "rpcGetBalance", protocol: "jsonrpc",
|
|
353
|
+
endpoint: "https://rpc.example.com",
|
|
354
|
+
rpcMethod: "eth_getBalance", paramsAsArray: true,
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const bridge2 = createBridge({
|
|
358
|
+
source: restBalance,
|
|
359
|
+
target: rpcBalance,
|
|
360
|
+
requestMapping: [
|
|
361
|
+
{ from: "pathParams.address", to: "rpcParams.address" },
|
|
362
|
+
{ compute: () => "latest", to: "rpcParams.block" },
|
|
363
|
+
],
|
|
364
|
+
responseMapping: [
|
|
365
|
+
{ from: "balance", to: "balance" },
|
|
366
|
+
{ from: "unit", to: "currency" },
|
|
367
|
+
],
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
const r2 = await bridge2.call({ pathParams: { address: "0xABC123" } }, { httpClient: mockHttp });
|
|
371
|
+
console.log("\n ✅ MAPPED RESPONSE:", JSON.stringify(r2.data, null, 4));
|
|
372
|
+
|
|
373
|
+
// ═════════════════════════════════════════
|
|
374
|
+
// 3: REST → SOAP
|
|
375
|
+
// ═════════════════════════════════════════
|
|
376
|
+
console.log("\n╔═══════════════════════════════════════════════════╗");
|
|
377
|
+
console.log("║ 3 — REST caller → SOAP backend ║");
|
|
378
|
+
console.log("╚═══════════════════════════════════════════════════╝");
|
|
379
|
+
|
|
380
|
+
const restStock = defineContract({
|
|
381
|
+
name: "restGetStock", protocol: "rest",
|
|
382
|
+
endpoint: "/api/stocks/{symbol}", method: "GET",
|
|
383
|
+
});
|
|
384
|
+
const soapStock = defineContract({
|
|
385
|
+
name: "soapGetStock", protocol: "soap",
|
|
386
|
+
endpoint: "https://legacy.example.com/StockService",
|
|
387
|
+
operation: "GetStockPrice",
|
|
388
|
+
namespace: "http://example.com/stocks",
|
|
389
|
+
soapAction: "http://example.com/stocks/GetStockPrice",
|
|
390
|
+
responseExtractor: (xml) => {
|
|
391
|
+
const price = xml.match(/<Price>(.*?)<\/Price>/)?.[1];
|
|
392
|
+
const currency = xml.match(/<Currency>(.*?)<\/Currency>/)?.[1];
|
|
393
|
+
return { price: parseFloat(price), currency };
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
const bridge3 = createBridge({
|
|
398
|
+
source: restStock,
|
|
399
|
+
target: soapStock,
|
|
400
|
+
requestMapping: [
|
|
401
|
+
{ from: "pathParams.symbol", to: "soapBody.StockSymbol" },
|
|
402
|
+
],
|
|
403
|
+
responseMapping: [
|
|
404
|
+
{ from: "price", to: "price" },
|
|
405
|
+
{ from: "currency", to: "currency" },
|
|
406
|
+
],
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
const r3 = await bridge3.call({ pathParams: { symbol: "AAPL" } }, { httpClient: mockHttp });
|
|
410
|
+
console.log("\n ✅ MAPPED RESPONSE:", JSON.stringify(r3.data, null, 4));
|
|
411
|
+
|
|
412
|
+
// ═════════════════════════════════════════
|
|
413
|
+
// 4: DRY RUN
|
|
414
|
+
// ═════════════════════════════════════════
|
|
415
|
+
console.log("\n╔═══════════════════════════════════════════════════╗");
|
|
416
|
+
console.log("║ 4 — Dry-run (inspect without calling) ║");
|
|
417
|
+
console.log("╚═══════════════════════════════════════════════════╝");
|
|
418
|
+
|
|
419
|
+
const dry = await bridge1.call({ pathParams: { id: "99" } }, { dryRun: true });
|
|
420
|
+
console.log(JSON.stringify(dry, null, 2));
|
|
421
|
+
|
|
422
|
+
// ═════════════════════════════════════════
|
|
423
|
+
// 5: CHAIN (REST → GQL → JSON-RPC)
|
|
424
|
+
// ═════════════════════════════════════════
|
|
425
|
+
console.log("\n╔═══════════════════════════════════════════════════╗");
|
|
426
|
+
console.log("║ 5 — Chain: REST → GraphQL → JSON-RPC ║");
|
|
427
|
+
console.log("╚═══════════════════════════════════════════════════╝");
|
|
428
|
+
|
|
429
|
+
const enrichBridge = createBridge({
|
|
430
|
+
source: gqlGetUser,
|
|
431
|
+
target: rpcBalance,
|
|
432
|
+
requestMapping: [
|
|
433
|
+
{ from: "user.id", to: "rpcParams.address" },
|
|
434
|
+
{ compute: () => "latest", to: "rpcParams.block" },
|
|
435
|
+
],
|
|
436
|
+
responseMapping: [
|
|
437
|
+
{ from: "balance", to: "walletBalance" },
|
|
438
|
+
{ from: "unit", to: "walletCurrency" },
|
|
439
|
+
],
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
const chain = chainBridges(bridge1, enrichBridge);
|
|
443
|
+
const cr = await chain.call({ pathParams: { id: "42" } }, { httpClient: mockHttp });
|
|
444
|
+
console.log("\n ✅ CHAIN RESULT:", JSON.stringify(cr, null, 2));
|
|
445
|
+
|
|
446
|
+
// ═════════════════════════════════════════
|
|
447
|
+
// EXPLAIN
|
|
448
|
+
// ═════════════════════════════════════════
|
|
449
|
+
console.log("\n╔═══════════════════════════════════════════════════╗");
|
|
450
|
+
console.log("║ Bridge Introspection ║");
|
|
451
|
+
console.log("╚═══════════════════════════════════════════════════╝");
|
|
452
|
+
[bridge1, bridge2, bridge3].forEach((b) => {
|
|
453
|
+
const e = b.explain();
|
|
454
|
+
console.log(` ${e.from.name} (${e.from.protocol}) → ${e.to.name} (${e.to.protocol}) [${e.requestMappingRules} req, ${e.responseMappingRules} res rules]`);
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
demo().catch(console.error);
|
|
459
|
+
}
|