@nkmc/agent-fs 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-7LIZT7L3.js +966 -0
- package/dist/index.cjs +1278 -0
- package/dist/index.d.cts +96 -0
- package/dist/index.d.ts +96 -0
- package/dist/index.js +419 -0
- package/dist/rpc-D1IHpjF_.d.cts +330 -0
- package/dist/rpc-D1IHpjF_.d.ts +330 -0
- package/dist/testing.cjs +842 -0
- package/dist/testing.d.cts +29 -0
- package/dist/testing.d.ts +29 -0
- package/dist/testing.js +10 -0
- package/package.json +25 -0
- package/src/agent-fs.ts +151 -0
- package/src/backends/http.ts +835 -0
- package/src/backends/memory.ts +183 -0
- package/src/backends/rpc.ts +456 -0
- package/src/index.ts +36 -0
- package/src/mount.ts +84 -0
- package/src/parser.ts +162 -0
- package/src/server.ts +158 -0
- package/src/testing.ts +3 -0
- package/src/types.ts +52 -0
- package/test/agent-fs.test.ts +325 -0
- package/test/http-204.test.ts +102 -0
- package/test/http-auth-prefix.test.ts +79 -0
- package/test/http-cloudflare.test.ts +533 -0
- package/test/http-form-encoding.test.ts +119 -0
- package/test/http-github.test.ts +580 -0
- package/test/http-listkey.test.ts +128 -0
- package/test/http-oauth2.test.ts +174 -0
- package/test/http-pagination.test.ts +200 -0
- package/test/http-param-styles.test.ts +98 -0
- package/test/http-passthrough.test.ts +282 -0
- package/test/http-retry.test.ts +132 -0
- package/test/http.test.ts +360 -0
- package/test/memory.test.ts +120 -0
- package/test/mount.test.ts +94 -0
- package/test/parser.test.ts +100 -0
- package/test/rpc-crud.test.ts +627 -0
- package/test/rpc-evm.test.ts +390 -0
- package/tsconfig.json +8 -0
- package/tsup.config.ts +8 -0
package/dist/testing.cjs
ADDED
|
@@ -0,0 +1,842 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/testing.ts
|
|
21
|
+
var testing_exports = {};
|
|
22
|
+
__export(testing_exports, {
|
|
23
|
+
HttpBackend: () => HttpBackend,
|
|
24
|
+
MemoryBackend: () => MemoryBackend,
|
|
25
|
+
RpcBackend: () => RpcBackend
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(testing_exports);
|
|
28
|
+
|
|
29
|
+
// src/backends/memory.ts
|
|
30
|
+
var MemoryBackend = class {
|
|
31
|
+
collections = /* @__PURE__ */ new Map();
|
|
32
|
+
nextId = 1;
|
|
33
|
+
/** Pre-seed a collection with data */
|
|
34
|
+
seed(collection, records) {
|
|
35
|
+
const col = this.getOrCreateCollection(collection);
|
|
36
|
+
for (const record of records) {
|
|
37
|
+
col.set(record.id, { ...record });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async list(path) {
|
|
41
|
+
const { collection, id } = this.parsePath(path);
|
|
42
|
+
if (!collection) {
|
|
43
|
+
return Array.from(this.collections.keys()).map((c) => c + "/");
|
|
44
|
+
}
|
|
45
|
+
if (!id) {
|
|
46
|
+
const col = this.collections.get(collection);
|
|
47
|
+
if (!col) return [];
|
|
48
|
+
return Array.from(col.keys()).map((k) => `${k}.json`);
|
|
49
|
+
}
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
async read(path) {
|
|
53
|
+
const { collection, id } = this.parsePath(path);
|
|
54
|
+
if (!collection) {
|
|
55
|
+
throw new NotFoundError(path);
|
|
56
|
+
}
|
|
57
|
+
const col = this.collections.get(collection);
|
|
58
|
+
if (!col) {
|
|
59
|
+
throw new NotFoundError(path);
|
|
60
|
+
}
|
|
61
|
+
if (id === "_schema") {
|
|
62
|
+
return this.getSchema(collection);
|
|
63
|
+
}
|
|
64
|
+
if (id === "_count") {
|
|
65
|
+
return { count: col.size };
|
|
66
|
+
}
|
|
67
|
+
if (!id) {
|
|
68
|
+
return Array.from(col.values());
|
|
69
|
+
}
|
|
70
|
+
const record = col.get(id);
|
|
71
|
+
if (!record) {
|
|
72
|
+
throw new NotFoundError(path);
|
|
73
|
+
}
|
|
74
|
+
return record;
|
|
75
|
+
}
|
|
76
|
+
async write(path, data) {
|
|
77
|
+
const { collection, id } = this.parsePath(path);
|
|
78
|
+
if (!collection) {
|
|
79
|
+
throw new Error("Cannot write to root");
|
|
80
|
+
}
|
|
81
|
+
const col = this.getOrCreateCollection(collection);
|
|
82
|
+
const record = data;
|
|
83
|
+
if (id) {
|
|
84
|
+
const existing = col.get(id);
|
|
85
|
+
if (!existing) {
|
|
86
|
+
throw new NotFoundError(path);
|
|
87
|
+
}
|
|
88
|
+
const updated = { ...existing, ...record, id };
|
|
89
|
+
col.set(id, updated);
|
|
90
|
+
return { id };
|
|
91
|
+
}
|
|
92
|
+
const newId = record.id ?? String(this.nextId++);
|
|
93
|
+
const newRecord = { ...record, id: newId };
|
|
94
|
+
col.set(newId, newRecord);
|
|
95
|
+
return { id: newId };
|
|
96
|
+
}
|
|
97
|
+
async remove(path) {
|
|
98
|
+
const { collection, id } = this.parsePath(path);
|
|
99
|
+
if (!collection || !id) {
|
|
100
|
+
throw new Error("Cannot remove a collection, specify a record path");
|
|
101
|
+
}
|
|
102
|
+
const col = this.collections.get(collection);
|
|
103
|
+
if (!col || !col.has(id)) {
|
|
104
|
+
throw new NotFoundError(path);
|
|
105
|
+
}
|
|
106
|
+
col.delete(id);
|
|
107
|
+
}
|
|
108
|
+
async search(path, pattern) {
|
|
109
|
+
const { collection } = this.parsePath(path);
|
|
110
|
+
if (!collection) {
|
|
111
|
+
throw new Error("grep requires a collection path");
|
|
112
|
+
}
|
|
113
|
+
const col = this.collections.get(collection);
|
|
114
|
+
if (!col) return [];
|
|
115
|
+
const results = [];
|
|
116
|
+
for (const record of col.values()) {
|
|
117
|
+
const json = JSON.stringify(record);
|
|
118
|
+
if (json.includes(pattern)) {
|
|
119
|
+
results.push(record);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return results;
|
|
123
|
+
}
|
|
124
|
+
parsePath(path) {
|
|
125
|
+
const cleaned = path.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
126
|
+
if (!cleaned) return {};
|
|
127
|
+
const parts = cleaned.split("/");
|
|
128
|
+
const collection = parts[0];
|
|
129
|
+
let id = parts[1];
|
|
130
|
+
if (id?.endsWith(".json")) {
|
|
131
|
+
id = id.slice(0, -5);
|
|
132
|
+
}
|
|
133
|
+
return { collection, id };
|
|
134
|
+
}
|
|
135
|
+
getOrCreateCollection(name) {
|
|
136
|
+
let col = this.collections.get(name);
|
|
137
|
+
if (!col) {
|
|
138
|
+
col = /* @__PURE__ */ new Map();
|
|
139
|
+
this.collections.set(name, col);
|
|
140
|
+
}
|
|
141
|
+
return col;
|
|
142
|
+
}
|
|
143
|
+
getSchema(collection) {
|
|
144
|
+
const col = this.collections.get(collection);
|
|
145
|
+
if (!col || col.size === 0) {
|
|
146
|
+
return { collection, fields: [] };
|
|
147
|
+
}
|
|
148
|
+
const first = col.values().next().value;
|
|
149
|
+
const fields = Object.entries(first).map(([name, value]) => ({
|
|
150
|
+
name,
|
|
151
|
+
type: typeof value
|
|
152
|
+
}));
|
|
153
|
+
return { collection, fields };
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
var NotFoundError = class extends Error {
|
|
157
|
+
constructor(path) {
|
|
158
|
+
super(`Not found: ${path}`);
|
|
159
|
+
this.name = "NotFoundError";
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// src/backends/http.ts
|
|
164
|
+
var HttpBackend = class {
|
|
165
|
+
baseUrl;
|
|
166
|
+
auth;
|
|
167
|
+
resourceList;
|
|
168
|
+
endpoints;
|
|
169
|
+
params;
|
|
170
|
+
_fetch;
|
|
171
|
+
bodyEncoding;
|
|
172
|
+
pagination;
|
|
173
|
+
retryConfig;
|
|
174
|
+
/** Cached OAuth2 access token */
|
|
175
|
+
_oauth2Token;
|
|
176
|
+
constructor(config) {
|
|
177
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
178
|
+
this.auth = config.auth;
|
|
179
|
+
this.resourceList = config.resources ?? [];
|
|
180
|
+
this.endpoints = new Map(
|
|
181
|
+
(config.endpoints ?? []).map((e) => [e.name, e])
|
|
182
|
+
);
|
|
183
|
+
this.params = config.params ?? {};
|
|
184
|
+
this._fetch = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
185
|
+
this.bodyEncoding = config.bodyEncoding ?? "json";
|
|
186
|
+
this.retryConfig = {
|
|
187
|
+
maxRetries: config.retry?.maxRetries ?? 3,
|
|
188
|
+
baseDelayMs: config.retry?.baseDelayMs ?? 1e3
|
|
189
|
+
};
|
|
190
|
+
if (config.pagination) {
|
|
191
|
+
this.pagination = {
|
|
192
|
+
...config.pagination,
|
|
193
|
+
maxPages: config.pagination.maxPages ?? 10
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async list(path) {
|
|
198
|
+
const parsed = this.parsePath(path);
|
|
199
|
+
if (parsed.type === "root") {
|
|
200
|
+
const entries = [];
|
|
201
|
+
for (const r of this.resourceList) {
|
|
202
|
+
entries.push(r.name + "/");
|
|
203
|
+
}
|
|
204
|
+
if (this.endpoints.size > 0) {
|
|
205
|
+
entries.push("_api/");
|
|
206
|
+
}
|
|
207
|
+
return entries;
|
|
208
|
+
}
|
|
209
|
+
if (parsed.type === "api-list") {
|
|
210
|
+
return Array.from(this.endpoints.values()).map(
|
|
211
|
+
(e) => `${e.name} [${e.method}]`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
if (parsed.type === "resource-list") {
|
|
215
|
+
const { resource, resolvedApiPath } = parsed;
|
|
216
|
+
const items = await this.fetchAllItems(resource, resolvedApiPath);
|
|
217
|
+
if (!Array.isArray(items)) return [];
|
|
218
|
+
return this.formatListItems(resource, items);
|
|
219
|
+
}
|
|
220
|
+
if (parsed.type === "passthrough") {
|
|
221
|
+
const resp = await this.request("GET", parsed.apiPath);
|
|
222
|
+
if (!resp.ok) return [];
|
|
223
|
+
const data = await safeJson(resp);
|
|
224
|
+
if (data === null) return [];
|
|
225
|
+
if (Array.isArray(data)) return data.map((item) => String(item.id ?? item.name ?? JSON.stringify(item)));
|
|
226
|
+
return Object.keys(data);
|
|
227
|
+
}
|
|
228
|
+
if (parsed.type === "resource-item") {
|
|
229
|
+
const { resource, resolvedApiPath, id } = parsed;
|
|
230
|
+
if (resource.pathMode === "tree") {
|
|
231
|
+
const resp = await this.request("GET", `${resolvedApiPath}/${id}`);
|
|
232
|
+
if (!resp.ok) return [];
|
|
233
|
+
const data = await safeJson(resp);
|
|
234
|
+
if (data === null) return [];
|
|
235
|
+
if (Array.isArray(data)) {
|
|
236
|
+
return this.formatListItems(resource, data);
|
|
237
|
+
}
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
if (resource.children && resource.children.length > 0) {
|
|
241
|
+
return resource.children.map((c) => c.name + "/");
|
|
242
|
+
}
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
async read(path) {
|
|
248
|
+
const parsed = this.parsePath(path);
|
|
249
|
+
if (parsed.type === "resource-item") {
|
|
250
|
+
const { resource, resolvedApiPath, id } = parsed;
|
|
251
|
+
if (id === "_schema") {
|
|
252
|
+
return {
|
|
253
|
+
resource: resource.name,
|
|
254
|
+
fields: resource.fields ?? []
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
if (id === "_count") {
|
|
258
|
+
const resp2 = await this.request("GET", resolvedApiPath);
|
|
259
|
+
const data = await safeJson(resp2) ?? {};
|
|
260
|
+
const items = extractList(data, resource.listKey);
|
|
261
|
+
return { count: Array.isArray(items) ? items.length : 0 };
|
|
262
|
+
}
|
|
263
|
+
const resp = await this.request("GET", `${resolvedApiPath}/${id}`);
|
|
264
|
+
if (!resp.ok) throw new NotFoundError(path);
|
|
265
|
+
let result = await safeJson(resp);
|
|
266
|
+
if (resource.transform?.read) result = resource.transform.read(result);
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
269
|
+
if (parsed.type === "resource-list") {
|
|
270
|
+
const { resource, resolvedApiPath } = parsed;
|
|
271
|
+
const resp = await this.request("GET", resolvedApiPath);
|
|
272
|
+
const data = await safeJson(resp);
|
|
273
|
+
return data === null ? [] : extractList(data, resource.listKey) ?? data;
|
|
274
|
+
}
|
|
275
|
+
if (parsed.type === "api-call") {
|
|
276
|
+
const endpoint = this.getEndpoint(parsed.endpoint);
|
|
277
|
+
const resp = await this.request(endpoint.method, endpoint.apiPath);
|
|
278
|
+
return safeJson(resp);
|
|
279
|
+
}
|
|
280
|
+
if (parsed.type === "passthrough") {
|
|
281
|
+
const resp = await this.request("GET", parsed.apiPath);
|
|
282
|
+
if (!resp.ok) throw new NotFoundError(path);
|
|
283
|
+
return safeJson(resp);
|
|
284
|
+
}
|
|
285
|
+
throw new NotFoundError(path);
|
|
286
|
+
}
|
|
287
|
+
async write(path, data) {
|
|
288
|
+
const parsed = this.parsePath(path);
|
|
289
|
+
if (parsed.type === "root" && this.resourceList.length === 0 && this.endpoints.size === 0) {
|
|
290
|
+
const resp = await this.request("POST", "/", data);
|
|
291
|
+
const result = await safeJson(resp) ?? {};
|
|
292
|
+
return { id: String(result.id ?? "ok") };
|
|
293
|
+
}
|
|
294
|
+
if (parsed.type === "resource-item" && parsed.id) {
|
|
295
|
+
const { resource, resolvedApiPath, id } = parsed;
|
|
296
|
+
let writeData = data;
|
|
297
|
+
if (resource.readBeforeWrite) {
|
|
298
|
+
try {
|
|
299
|
+
const readResp = await this.request(
|
|
300
|
+
"GET",
|
|
301
|
+
`${resolvedApiPath}/${id}`
|
|
302
|
+
);
|
|
303
|
+
if (readResp.ok) {
|
|
304
|
+
const readResult = await safeJson(readResp);
|
|
305
|
+
if (readResult) writeData = resource.readBeforeWrite.inject(readResult, writeData);
|
|
306
|
+
}
|
|
307
|
+
} catch {
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (resource.transform?.write) writeData = resource.transform.write(writeData);
|
|
311
|
+
const method = resource.updateMethod ?? "PUT";
|
|
312
|
+
const resp = await this.request(
|
|
313
|
+
method,
|
|
314
|
+
`${resolvedApiPath}/${id}`,
|
|
315
|
+
writeData
|
|
316
|
+
);
|
|
317
|
+
if (!resp.ok) throw new NotFoundError(path);
|
|
318
|
+
const result = await safeJson(resp) ?? {};
|
|
319
|
+
const idField = resource.idField ?? "id";
|
|
320
|
+
return { id: String(result[idField] ?? id) };
|
|
321
|
+
}
|
|
322
|
+
if (parsed.type === "resource-list") {
|
|
323
|
+
const { resource, resolvedApiPath } = parsed;
|
|
324
|
+
let writeData = data;
|
|
325
|
+
if (resource.transform?.write) writeData = resource.transform.write(writeData);
|
|
326
|
+
const resp = await this.request("POST", resolvedApiPath, writeData);
|
|
327
|
+
const result = await safeJson(resp) ?? {};
|
|
328
|
+
const idField = resource.idField ?? "id";
|
|
329
|
+
return { id: String(result[idField] ?? "unknown") };
|
|
330
|
+
}
|
|
331
|
+
if (parsed.type === "api-call") {
|
|
332
|
+
const endpoint = this.getEndpoint(parsed.endpoint);
|
|
333
|
+
const resp = await this.request(endpoint.method, endpoint.apiPath, data);
|
|
334
|
+
const result = await safeJson(resp) ?? {};
|
|
335
|
+
return { id: result.id ?? "ok" };
|
|
336
|
+
}
|
|
337
|
+
if (parsed.type === "passthrough") {
|
|
338
|
+
const method = parsed.apiPath === "/" ? "POST" : "PUT";
|
|
339
|
+
const resp = await this.request(method, parsed.apiPath, data);
|
|
340
|
+
const result = await safeJson(resp) ?? {};
|
|
341
|
+
return { id: String(result.id ?? "ok") };
|
|
342
|
+
}
|
|
343
|
+
throw new Error(`Cannot write to path: ${path}`);
|
|
344
|
+
}
|
|
345
|
+
async remove(path) {
|
|
346
|
+
const parsed = this.parsePath(path);
|
|
347
|
+
if (parsed.type === "resource-item" && parsed.id) {
|
|
348
|
+
const { resource, resolvedApiPath, id } = parsed;
|
|
349
|
+
let deleteBody;
|
|
350
|
+
if (resource.readBeforeWrite) {
|
|
351
|
+
const readResp = await this.request(
|
|
352
|
+
"GET",
|
|
353
|
+
`${resolvedApiPath}/${id}`
|
|
354
|
+
);
|
|
355
|
+
if (!readResp.ok) throw new NotFoundError(path);
|
|
356
|
+
const readResult = await safeJson(readResp);
|
|
357
|
+
if (resource.transform?.remove) {
|
|
358
|
+
deleteBody = resource.transform.remove(readResult);
|
|
359
|
+
}
|
|
360
|
+
} else if (resource.transform?.remove) {
|
|
361
|
+
deleteBody = resource.transform.remove(null);
|
|
362
|
+
}
|
|
363
|
+
const resp = await this.request(
|
|
364
|
+
"DELETE",
|
|
365
|
+
`${resolvedApiPath}/${id}`,
|
|
366
|
+
deleteBody
|
|
367
|
+
);
|
|
368
|
+
if (!resp.ok) throw new NotFoundError(path);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (parsed.type === "passthrough") {
|
|
372
|
+
const resp = await this.request("DELETE", parsed.apiPath);
|
|
373
|
+
if (!resp.ok) throw new NotFoundError(path);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
throw new Error(`Cannot remove path: ${path}`);
|
|
377
|
+
}
|
|
378
|
+
async search(path, pattern) {
|
|
379
|
+
const parsed = this.parsePath(path);
|
|
380
|
+
if (parsed.type === "resource-list" || parsed.type === "resource-item") {
|
|
381
|
+
const { resource, resolvedApiPath } = parsed;
|
|
382
|
+
const resp = await this.request(
|
|
383
|
+
"GET",
|
|
384
|
+
`${resolvedApiPath}?q=${encodeURIComponent(pattern)}`
|
|
385
|
+
);
|
|
386
|
+
const data = await safeJson(resp) ?? {};
|
|
387
|
+
const items = extractList(data, resource.listKey);
|
|
388
|
+
if (Array.isArray(items)) return items;
|
|
389
|
+
const allResp = await this.request("GET", resolvedApiPath);
|
|
390
|
+
const allData = await safeJson(allResp) ?? {};
|
|
391
|
+
const allItems = extractList(allData, resource.listKey);
|
|
392
|
+
if (!Array.isArray(allItems)) return [];
|
|
393
|
+
return allItems.filter(
|
|
394
|
+
(item) => JSON.stringify(item).includes(pattern)
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
if (parsed.type === "passthrough") {
|
|
398
|
+
const resp = await this.request("GET", `${parsed.apiPath}?q=${encodeURIComponent(pattern)}`);
|
|
399
|
+
const data = await safeJson(resp);
|
|
400
|
+
return Array.isArray(data) ? data : [];
|
|
401
|
+
}
|
|
402
|
+
return [];
|
|
403
|
+
}
|
|
404
|
+
// --- Internal helpers ---
|
|
405
|
+
async request(method, apiPath, body, absoluteUrl) {
|
|
406
|
+
const url = absoluteUrl ? apiPath : `${this.baseUrl}${apiPath}`;
|
|
407
|
+
const useForm = this.bodyEncoding === "form";
|
|
408
|
+
const headers = {
|
|
409
|
+
"Content-Type": useForm ? "application/x-www-form-urlencoded" : "application/json",
|
|
410
|
+
Accept: "application/json",
|
|
411
|
+
"User-Agent": "nkmc-gateway/1.0"
|
|
412
|
+
};
|
|
413
|
+
if (this.auth) {
|
|
414
|
+
switch (this.auth.type) {
|
|
415
|
+
case "bearer": {
|
|
416
|
+
const prefix = this.auth.prefix ?? "Bearer";
|
|
417
|
+
headers["Authorization"] = `${prefix} ${this.auth.token}`;
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
case "api-key":
|
|
421
|
+
headers[this.auth.header] = this.auth.key;
|
|
422
|
+
break;
|
|
423
|
+
case "basic":
|
|
424
|
+
headers["Authorization"] = `Basic ${btoa(`${this.auth.username}:${this.auth.password}`)}`;
|
|
425
|
+
break;
|
|
426
|
+
case "oauth2": {
|
|
427
|
+
const token = await this.getOAuth2Token();
|
|
428
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
let encodedBody;
|
|
434
|
+
if (body !== void 0) {
|
|
435
|
+
encodedBody = useForm ? encodeFormBody(body) : JSON.stringify(body);
|
|
436
|
+
}
|
|
437
|
+
let lastResp;
|
|
438
|
+
for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
|
|
439
|
+
lastResp = await this._fetch(url, {
|
|
440
|
+
method,
|
|
441
|
+
headers,
|
|
442
|
+
body: encodedBody
|
|
443
|
+
});
|
|
444
|
+
if (lastResp.ok || lastResp.status >= 400 && lastResp.status < 500 && lastResp.status !== 429) {
|
|
445
|
+
return lastResp;
|
|
446
|
+
}
|
|
447
|
+
if (attempt === this.retryConfig.maxRetries) break;
|
|
448
|
+
let delayMs;
|
|
449
|
+
const retryAfter = lastResp.headers.get("retry-after");
|
|
450
|
+
if (retryAfter) {
|
|
451
|
+
const seconds = Number(retryAfter);
|
|
452
|
+
delayMs = isNaN(seconds) ? this.retryConfig.baseDelayMs : seconds * 1e3;
|
|
453
|
+
} else {
|
|
454
|
+
delayMs = this.retryConfig.baseDelayMs * Math.pow(2, attempt) * (0.5 + Math.random() * 0.5);
|
|
455
|
+
}
|
|
456
|
+
await sleep(delayMs);
|
|
457
|
+
}
|
|
458
|
+
return lastResp;
|
|
459
|
+
}
|
|
460
|
+
/** Obtain (or refresh) OAuth2 access token via client_credentials grant */
|
|
461
|
+
async getOAuth2Token() {
|
|
462
|
+
if (this.auth?.type !== "oauth2") throw new Error("Not OAuth2 auth");
|
|
463
|
+
if (this._oauth2Token && Date.now() < this._oauth2Token.expiresAt - 3e4) {
|
|
464
|
+
return this._oauth2Token.token;
|
|
465
|
+
}
|
|
466
|
+
const { tokenUrl, clientId, clientSecret, scope } = this.auth;
|
|
467
|
+
const params = new URLSearchParams({ grant_type: "client_credentials" });
|
|
468
|
+
if (scope) params.set("scope", scope);
|
|
469
|
+
const resp = await this._fetch(tokenUrl, {
|
|
470
|
+
method: "POST",
|
|
471
|
+
headers: {
|
|
472
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
473
|
+
Authorization: `Basic ${btoa(`${clientId}:${clientSecret}`)}`
|
|
474
|
+
},
|
|
475
|
+
body: params.toString()
|
|
476
|
+
});
|
|
477
|
+
if (!resp.ok) {
|
|
478
|
+
throw new Error(`OAuth2 token request failed: ${resp.status} ${resp.statusText}`);
|
|
479
|
+
}
|
|
480
|
+
const data = await resp.json();
|
|
481
|
+
const expiresIn = data.expires_in ?? 3600;
|
|
482
|
+
this._oauth2Token = {
|
|
483
|
+
token: data.access_token,
|
|
484
|
+
expiresAt: Date.now() + expiresIn * 1e3
|
|
485
|
+
};
|
|
486
|
+
return this._oauth2Token.token;
|
|
487
|
+
}
|
|
488
|
+
/** Parse Link header to extract rel="next" URL */
|
|
489
|
+
getNextPageUrl(resp) {
|
|
490
|
+
const link = resp.headers.get("link");
|
|
491
|
+
if (!link) return null;
|
|
492
|
+
const match = link.match(/<([^>]+)>;\s*rel="next"/);
|
|
493
|
+
return match ? match[1] : null;
|
|
494
|
+
}
|
|
495
|
+
/** Fetch all items with optional pagination */
|
|
496
|
+
async fetchAllItems(resource, apiPath) {
|
|
497
|
+
const resp = await this.request("GET", apiPath);
|
|
498
|
+
const data = await safeJson(resp);
|
|
499
|
+
if (data === null) return [];
|
|
500
|
+
let items = extractList(data, resource.listKey);
|
|
501
|
+
if (!Array.isArray(items)) return [];
|
|
502
|
+
if (this.pagination) {
|
|
503
|
+
const maxPages = this.pagination.maxPages;
|
|
504
|
+
let pages = 1;
|
|
505
|
+
if (this.pagination.type === "link-header") {
|
|
506
|
+
let nextUrl = this.getNextPageUrl(resp);
|
|
507
|
+
while (nextUrl && pages < maxPages) {
|
|
508
|
+
const nextResp = await this.request("GET", nextUrl, void 0, true);
|
|
509
|
+
const nextData = await safeJson(nextResp);
|
|
510
|
+
if (!nextData) break;
|
|
511
|
+
const nextItems = extractList(nextData, resource.listKey);
|
|
512
|
+
if (!Array.isArray(nextItems) || nextItems.length === 0) break;
|
|
513
|
+
items = items.concat(nextItems);
|
|
514
|
+
nextUrl = this.getNextPageUrl(nextResp);
|
|
515
|
+
pages++;
|
|
516
|
+
}
|
|
517
|
+
} else if (this.pagination.type === "cursor") {
|
|
518
|
+
const { cursorParam, cursorPath } = this.pagination;
|
|
519
|
+
let cursor = getNestedValue(data, cursorPath);
|
|
520
|
+
while (cursor && pages < maxPages) {
|
|
521
|
+
const sep = apiPath.includes("?") ? "&" : "?";
|
|
522
|
+
const nextResp = await this.request("GET", `${apiPath}${sep}${cursorParam}=${encodeURIComponent(String(cursor))}`);
|
|
523
|
+
const nextData = await safeJson(nextResp);
|
|
524
|
+
if (!nextData) break;
|
|
525
|
+
const nextItems = extractList(nextData, resource.listKey);
|
|
526
|
+
if (!Array.isArray(nextItems) || nextItems.length === 0) break;
|
|
527
|
+
items = items.concat(nextItems);
|
|
528
|
+
cursor = getNestedValue(nextData, cursorPath);
|
|
529
|
+
pages++;
|
|
530
|
+
}
|
|
531
|
+
} else if (this.pagination.type === "offset") {
|
|
532
|
+
const { offsetParam = "offset", limitParam = "limit", pageSize = 100 } = this.pagination;
|
|
533
|
+
let offset = items.length;
|
|
534
|
+
while (pages < maxPages) {
|
|
535
|
+
const sep = apiPath.includes("?") ? "&" : "?";
|
|
536
|
+
const nextResp = await this.request("GET", `${apiPath}${sep}${offsetParam}=${offset}&${limitParam}=${pageSize}`);
|
|
537
|
+
const nextData = await safeJson(nextResp);
|
|
538
|
+
if (!nextData) break;
|
|
539
|
+
const nextItems = extractList(nextData, resource.listKey);
|
|
540
|
+
if (!Array.isArray(nextItems) || nextItems.length === 0) break;
|
|
541
|
+
items = items.concat(nextItems);
|
|
542
|
+
offset += nextItems.length;
|
|
543
|
+
pages++;
|
|
544
|
+
}
|
|
545
|
+
} else if (this.pagination.type === "page") {
|
|
546
|
+
const { pageParam = "page" } = this.pagination;
|
|
547
|
+
let page = 2;
|
|
548
|
+
while (pages < maxPages) {
|
|
549
|
+
const sep = apiPath.includes("?") ? "&" : "?";
|
|
550
|
+
const nextResp = await this.request("GET", `${apiPath}${sep}${pageParam}=${page}`);
|
|
551
|
+
const nextData = await safeJson(nextResp);
|
|
552
|
+
if (!nextData) break;
|
|
553
|
+
const nextItems = extractList(nextData, resource.listKey);
|
|
554
|
+
if (!Array.isArray(nextItems) || nextItems.length === 0) break;
|
|
555
|
+
items = items.concat(nextItems);
|
|
556
|
+
page++;
|
|
557
|
+
pages++;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return items;
|
|
562
|
+
}
|
|
563
|
+
/** Format list items using transform.list or default idField + ".json" */
|
|
564
|
+
formatListItems(resource, items) {
|
|
565
|
+
if (resource.transform?.list) {
|
|
566
|
+
return items.map((item) => resource.transform.list(item));
|
|
567
|
+
}
|
|
568
|
+
const idField = resource.idField ?? "id";
|
|
569
|
+
return items.map(
|
|
570
|
+
(item) => String(item[idField]) + ".json"
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
getEndpoint(name) {
|
|
574
|
+
const endpoint = this.endpoints.get(name);
|
|
575
|
+
if (!endpoint) throw new NotFoundError(`Endpoint not found: ${name}`);
|
|
576
|
+
return endpoint;
|
|
577
|
+
}
|
|
578
|
+
/** Replace :param and {param} placeholders in API paths with values from config.params */
|
|
579
|
+
resolveTemplate(path) {
|
|
580
|
+
return path.replace(/:(\w+)/g, (_, key) => {
|
|
581
|
+
const value = this.params[key];
|
|
582
|
+
if (value === void 0) throw new Error(`Missing param: ${key}`);
|
|
583
|
+
return value;
|
|
584
|
+
}).replace(/\{(\w+)\}/g, (_, key) => {
|
|
585
|
+
const value = this.params[key];
|
|
586
|
+
if (value === void 0) throw new Error(`Missing param: ${key}`);
|
|
587
|
+
return value;
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
/** Recursively resolve a filesystem path against the resource tree */
|
|
591
|
+
resolveResourcePath(parts, pos, resources, parentApiPath) {
|
|
592
|
+
if (pos >= parts.length) return null;
|
|
593
|
+
const resourceName = parts[pos];
|
|
594
|
+
const resource = resources.find((r) => r.name === resourceName);
|
|
595
|
+
if (!resource) return null;
|
|
596
|
+
const rawSegment = resource.apiPath ?? `/${resource.name}`;
|
|
597
|
+
const resolvedSegment = this.resolveTemplate(rawSegment);
|
|
598
|
+
const baseApiPath = parentApiPath + resolvedSegment;
|
|
599
|
+
const remaining = parts.length - pos - 1;
|
|
600
|
+
if (resource.pathMode === "tree") {
|
|
601
|
+
if (remaining === 0) {
|
|
602
|
+
return { type: "resource-list", resource, resolvedApiPath: baseApiPath };
|
|
603
|
+
}
|
|
604
|
+
const id2 = parts.slice(pos + 1).join("/");
|
|
605
|
+
return { type: "resource-item", resource, resolvedApiPath: baseApiPath, id: id2 };
|
|
606
|
+
}
|
|
607
|
+
if (remaining === 0) {
|
|
608
|
+
return { type: "resource-list", resource, resolvedApiPath: baseApiPath };
|
|
609
|
+
}
|
|
610
|
+
const rawId = parts[pos + 1];
|
|
611
|
+
if (remaining >= 2) {
|
|
612
|
+
if (resource.children) {
|
|
613
|
+
const childResult = this.resolveResourcePath(
|
|
614
|
+
parts,
|
|
615
|
+
pos + 2,
|
|
616
|
+
resource.children,
|
|
617
|
+
baseApiPath + "/" + rawId
|
|
618
|
+
);
|
|
619
|
+
if (childResult) return childResult;
|
|
620
|
+
}
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
let id = rawId;
|
|
624
|
+
if (id.endsWith(".json")) id = id.slice(0, -5);
|
|
625
|
+
return { type: "resource-item", resource, resolvedApiPath: baseApiPath, id };
|
|
626
|
+
}
|
|
627
|
+
parsePath(path) {
|
|
628
|
+
const cleaned = path.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
629
|
+
if (!cleaned) return { type: "root" };
|
|
630
|
+
const parts = cleaned.split("/");
|
|
631
|
+
if (parts[0] === "_api") {
|
|
632
|
+
if (parts.length === 1) return { type: "api-list" };
|
|
633
|
+
return { type: "api-call", endpoint: parts[1] };
|
|
634
|
+
}
|
|
635
|
+
const result = this.resolveResourcePath(
|
|
636
|
+
parts,
|
|
637
|
+
0,
|
|
638
|
+
this.resourceList,
|
|
639
|
+
""
|
|
640
|
+
);
|
|
641
|
+
if (!result) {
|
|
642
|
+
return { type: "passthrough", apiPath: "/" + cleaned };
|
|
643
|
+
}
|
|
644
|
+
return result;
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
async function safeJson(resp) {
|
|
648
|
+
if (resp.status === 204) return null;
|
|
649
|
+
const text = await resp.text();
|
|
650
|
+
if (!text || !text.trim()) return null;
|
|
651
|
+
try {
|
|
652
|
+
return JSON.parse(text);
|
|
653
|
+
} catch {
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
function extractList(data, listKey) {
|
|
658
|
+
if (Array.isArray(data)) return data;
|
|
659
|
+
if (typeof data !== "object" || data === null) return data;
|
|
660
|
+
if (listKey) return data[listKey];
|
|
661
|
+
let firstEmpty = null;
|
|
662
|
+
for (const value of Object.values(data)) {
|
|
663
|
+
if (Array.isArray(value)) {
|
|
664
|
+
if (value.length > 0) return value;
|
|
665
|
+
if (!firstEmpty) firstEmpty = value;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
if (firstEmpty) return firstEmpty;
|
|
669
|
+
return data;
|
|
670
|
+
}
|
|
671
|
+
function getNestedValue(obj, path) {
|
|
672
|
+
let current = obj;
|
|
673
|
+
for (const key of path.split(".")) {
|
|
674
|
+
if (current === null || current === void 0 || typeof current !== "object") return void 0;
|
|
675
|
+
current = current[key];
|
|
676
|
+
}
|
|
677
|
+
return current;
|
|
678
|
+
}
|
|
679
|
+
function sleep(ms) {
|
|
680
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
681
|
+
}
|
|
682
|
+
function encodeFormBody(data, prefix) {
|
|
683
|
+
if (data === null || data === void 0) return "";
|
|
684
|
+
if (typeof data !== "object") {
|
|
685
|
+
return prefix ? `${encodeURIComponent(prefix)}=${encodeURIComponent(String(data))}` : "";
|
|
686
|
+
}
|
|
687
|
+
const parts = [];
|
|
688
|
+
const obj = data;
|
|
689
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
690
|
+
const fullKey = prefix ? `${prefix}[${key}]` : key;
|
|
691
|
+
if (Array.isArray(value)) {
|
|
692
|
+
for (const item of value) {
|
|
693
|
+
parts.push(`${encodeURIComponent(`${fullKey}[]`)}=${encodeURIComponent(String(item))}`);
|
|
694
|
+
}
|
|
695
|
+
} else if (value !== null && typeof value === "object") {
|
|
696
|
+
const nested = encodeFormBody(value, fullKey);
|
|
697
|
+
if (nested) parts.push(nested);
|
|
698
|
+
} else if (value !== void 0) {
|
|
699
|
+
parts.push(`${encodeURIComponent(fullKey)}=${encodeURIComponent(String(value))}`);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return parts.join("&");
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// src/backends/rpc.ts
|
|
706
|
+
var RpcBackend = class {
|
|
707
|
+
transport;
|
|
708
|
+
resources;
|
|
709
|
+
constructor(config) {
|
|
710
|
+
this.transport = config.transport;
|
|
711
|
+
this.resources = config.resources;
|
|
712
|
+
}
|
|
713
|
+
async list(path) {
|
|
714
|
+
const parsed = this.parsePath(path);
|
|
715
|
+
if (parsed.type === "root") {
|
|
716
|
+
return this.resources.filter((r) => r.methods.list || r.methods.read).map((r) => r.name + "/");
|
|
717
|
+
}
|
|
718
|
+
if (parsed.type === "resource-list") {
|
|
719
|
+
const { resource } = parsed;
|
|
720
|
+
if (!resource.methods.list) return [];
|
|
721
|
+
const { method, params } = resource.methods.list;
|
|
722
|
+
const result = await this.transport.call(method, params({}));
|
|
723
|
+
if (resource.transform?.list) {
|
|
724
|
+
const transformed = resource.transform.list(result);
|
|
725
|
+
return Array.isArray(transformed) ? transformed : [transformed];
|
|
726
|
+
}
|
|
727
|
+
if (Array.isArray(result)) {
|
|
728
|
+
const idField = resource.idField ?? "id";
|
|
729
|
+
return result.map(
|
|
730
|
+
(item) => String(item[idField]) + ".json"
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
return [];
|
|
734
|
+
}
|
|
735
|
+
return [];
|
|
736
|
+
}
|
|
737
|
+
async read(path) {
|
|
738
|
+
const parsed = this.parsePath(path);
|
|
739
|
+
if (parsed.type === "resource-item") {
|
|
740
|
+
const { resource, id } = parsed;
|
|
741
|
+
if (!resource.methods.read) {
|
|
742
|
+
throw new NotFoundError(path);
|
|
743
|
+
}
|
|
744
|
+
const { method, params } = resource.methods.read;
|
|
745
|
+
const result = await this.transport.call(method, params({ id }));
|
|
746
|
+
if (result === null || result === void 0) {
|
|
747
|
+
throw new NotFoundError(path);
|
|
748
|
+
}
|
|
749
|
+
return resource.transform?.read ? resource.transform.read(result) : result;
|
|
750
|
+
}
|
|
751
|
+
if (parsed.type === "resource-list") {
|
|
752
|
+
const { resource } = parsed;
|
|
753
|
+
if (!resource.methods.list) {
|
|
754
|
+
throw new NotFoundError(path);
|
|
755
|
+
}
|
|
756
|
+
const { method, params } = resource.methods.list;
|
|
757
|
+
return this.transport.call(method, params({}));
|
|
758
|
+
}
|
|
759
|
+
throw new NotFoundError(path);
|
|
760
|
+
}
|
|
761
|
+
async write(path, data) {
|
|
762
|
+
const parsed = this.parsePath(path);
|
|
763
|
+
if (parsed.type === "resource-item") {
|
|
764
|
+
const { resource, id } = parsed;
|
|
765
|
+
const rpcMethod = resource.methods.write ?? resource.methods.create;
|
|
766
|
+
if (!rpcMethod) throw new Error(`Cannot write to path: ${path}`);
|
|
767
|
+
const writeData = resource.transform?.write ? resource.transform.write(data) : data;
|
|
768
|
+
const { method, params } = rpcMethod;
|
|
769
|
+
const result = await this.transport.call(method, params({ id, data: writeData }));
|
|
770
|
+
return { id: String(result ?? id) };
|
|
771
|
+
}
|
|
772
|
+
if (parsed.type === "resource-list") {
|
|
773
|
+
const { resource } = parsed;
|
|
774
|
+
const rpcMethod = resource.methods.create ?? resource.methods.write;
|
|
775
|
+
if (!rpcMethod) throw new Error(`Cannot write to path: ${path}`);
|
|
776
|
+
const writeData = resource.transform?.write ? resource.transform.write(data) : data;
|
|
777
|
+
const { method, params } = rpcMethod;
|
|
778
|
+
const result = await this.transport.call(method, params({ data: writeData }));
|
|
779
|
+
return { id: String(result ?? "unknown") };
|
|
780
|
+
}
|
|
781
|
+
throw new Error(`Cannot write to path: ${path}`);
|
|
782
|
+
}
|
|
783
|
+
async remove(path) {
|
|
784
|
+
const parsed = this.parsePath(path);
|
|
785
|
+
if (parsed.type === "resource-item") {
|
|
786
|
+
const { resource, id } = parsed;
|
|
787
|
+
if (!resource.methods.remove) {
|
|
788
|
+
throw new Error(`Cannot remove path: ${path}`);
|
|
789
|
+
}
|
|
790
|
+
const { method, params } = resource.methods.remove;
|
|
791
|
+
await this.transport.call(method, params({ id }));
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
throw new Error(`Cannot remove path: ${path}`);
|
|
795
|
+
}
|
|
796
|
+
async search(path, pattern) {
|
|
797
|
+
const parsed = this.parsePath(path);
|
|
798
|
+
if (parsed.type === "resource-list" || parsed.type === "resource-item") {
|
|
799
|
+
const { resource } = parsed;
|
|
800
|
+
if (resource.methods.search) {
|
|
801
|
+
const { method, params } = resource.methods.search;
|
|
802
|
+
const result = await this.transport.call(method, params({ pattern }));
|
|
803
|
+
if (Array.isArray(result)) return result;
|
|
804
|
+
return [result];
|
|
805
|
+
}
|
|
806
|
+
if (resource.methods.list) {
|
|
807
|
+
const { method, params } = resource.methods.list;
|
|
808
|
+
const result = await this.transport.call(method, params({}));
|
|
809
|
+
if (Array.isArray(result)) {
|
|
810
|
+
return result.filter(
|
|
811
|
+
(item) => JSON.stringify(item).includes(pattern)
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
if (JSON.stringify(result).includes(pattern)) {
|
|
815
|
+
return [result];
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return [];
|
|
820
|
+
}
|
|
821
|
+
// --- Internal ---
|
|
822
|
+
parsePath(path) {
|
|
823
|
+
const cleaned = path.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
824
|
+
if (!cleaned) return { type: "root" };
|
|
825
|
+
const parts = cleaned.split("/");
|
|
826
|
+
const resourceName = parts[0];
|
|
827
|
+
const resource = this.resources.find((r) => r.name === resourceName);
|
|
828
|
+
if (!resource) throw new NotFoundError(`Invalid path: ${path}`);
|
|
829
|
+
if (parts.length === 1) {
|
|
830
|
+
return { type: "resource-list", resource };
|
|
831
|
+
}
|
|
832
|
+
let id = parts.slice(1).join("/");
|
|
833
|
+
if (id.endsWith(".json")) id = id.slice(0, -5);
|
|
834
|
+
return { type: "resource-item", resource, id };
|
|
835
|
+
}
|
|
836
|
+
};
|
|
837
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
838
|
+
0 && (module.exports = {
|
|
839
|
+
HttpBackend,
|
|
840
|
+
MemoryBackend,
|
|
841
|
+
RpcBackend
|
|
842
|
+
});
|