apixa 0.0.0-stage → 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.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +257 -2
  3. package/dist/chunk-6ZX7GAWG.js +10 -0
  4. package/dist/chunk-A3QU5MFP.js +907 -0
  5. package/dist/chunk-DGSIZXRI.js +247 -0
  6. package/dist/chunk-GWZTJ7LM.js +2410 -0
  7. package/dist/chunk-IAQ5YOVE.js +10 -0
  8. package/dist/chunk-KE7UM4VW.js +707 -0
  9. package/dist/chunk-LARJYKRJ.js +117 -0
  10. package/dist/chunk-MLPW6EET.js +10 -0
  11. package/dist/chunk-QVKMZELC.js +10 -0
  12. package/dist/chunk-QY5YMHHN.js +607 -0
  13. package/dist/chunk-QZIU7J3G.js +10 -0
  14. package/dist/chunk-R4FXTZWU.js +10 -0
  15. package/dist/chunk-SIFGLE22.js +794 -0
  16. package/dist/chunk-WNJUPR6P.js +707 -0
  17. package/dist/chunk-ZSLMMDUX.js +727 -0
  18. package/dist/data-3HVQ54F3.js +2550 -0
  19. package/dist/data-ADLKYCN6.js +2211 -0
  20. package/dist/data-B47EIZXW.js +6 -0
  21. package/dist/data-DL4IKJCX.js +6 -0
  22. package/dist/data-GNCSLYM4.js +6 -0
  23. package/dist/data-GTTIEVFI.js +6 -0
  24. package/dist/data-HWX62BTJ.js +4772 -0
  25. package/dist/data-JR6F6HSR.js +6 -0
  26. package/dist/data-OKVHGTXW.js +3544 -0
  27. package/dist/data-PQPSE56A.js +1308 -0
  28. package/dist/data-QF32LRM4.js +3506 -0
  29. package/dist/data-UIVRJ77R.js +2456 -0
  30. package/dist/data-UZVNJUNM.js +2256 -0
  31. package/dist/data-WRWVXGYD.js +6 -0
  32. package/dist/data-Y6374BIT.js +3350 -0
  33. package/dist/index.d.ts +305 -0
  34. package/dist/index.js +80 -0
  35. package/dist/inference-BGj3DlzE.d.ts +1220 -0
  36. package/dist/loader-hhMdppVZ.d.ts +32 -0
  37. package/dist/mock-datasets-Xa9NXK9n.d.ts +178 -0
  38. package/dist/mocking/datasets/comments.d.ts +6 -0
  39. package/dist/mocking/datasets/comments.js +7 -0
  40. package/dist/mocking/datasets/companies.d.ts +6 -0
  41. package/dist/mocking/datasets/companies.js +7 -0
  42. package/dist/mocking/datasets/index.d.ts +18 -0
  43. package/dist/mocking/datasets/index.js +265 -0
  44. package/dist/mocking/datasets/orders.d.ts +6 -0
  45. package/dist/mocking/datasets/orders.js +7 -0
  46. package/dist/mocking/datasets/posts.d.ts +6 -0
  47. package/dist/mocking/datasets/posts.js +7 -0
  48. package/dist/mocking/datasets/products.d.ts +6 -0
  49. package/dist/mocking/datasets/products.js +7 -0
  50. package/dist/mocking/datasets/users.d.ts +6 -0
  51. package/dist/mocking/datasets/users.js +7 -0
  52. package/dist/mocking/index.d.ts +317 -0
  53. package/dist/mocking/index.js +366 -0
  54. package/dist/mocking/node.d.ts +33 -0
  55. package/dist/mocking/node.js +189 -0
  56. package/dist/mocking/register.d.ts +2 -0
  57. package/dist/mocking/register.js +9 -0
  58. package/dist/mutation-controller-BjCOn42y.d.ts +476 -0
  59. package/dist/react/advanced.d.ts +111 -0
  60. package/dist/react/advanced.js +119 -0
  61. package/dist/react/index.d.ts +283 -0
  62. package/dist/react/index.js +303 -0
  63. package/dist/types-mSoi1qGP.d.ts +46 -0
  64. package/package.json +128 -4
@@ -0,0 +1,2410 @@
1
+ // packages/core/src/errors/api-error.ts
2
+ var ApiError = class extends Error {
3
+ status;
4
+ code;
5
+ data;
6
+ request;
7
+ response;
8
+ retryable;
9
+ attempt;
10
+ cause;
11
+ constructor(message, options) {
12
+ super(message, options?.cause !== void 0 ? { cause: options.cause } : void 0);
13
+ this.name = "ApiError";
14
+ this.status = options?.status;
15
+ this.code = options?.code ?? "NETWORK";
16
+ this.data = options?.data;
17
+ this.request = options?.request;
18
+ this.response = options?.response;
19
+ this.retryable = options?.retryable ?? false;
20
+ this.attempt = options?.attempt;
21
+ this.cause = options?.cause;
22
+ }
23
+ };
24
+
25
+ // packages/core/src/errors/network-error.ts
26
+ var NetworkError = class extends ApiError {
27
+ constructor(message = "Network request failed", options) {
28
+ super(message, { code: "NETWORK", retryable: true, ...options });
29
+ this.name = "NetworkError";
30
+ }
31
+ };
32
+
33
+ // packages/core/src/errors/http-error.ts
34
+ var HttpError = class extends ApiError {
35
+ constructor(message, options) {
36
+ super(message, { code: "HTTP", retryable: false, ...options });
37
+ this.name = "HttpError";
38
+ }
39
+ };
40
+
41
+ // packages/core/src/errors/parse-error.ts
42
+ var ParseError = class extends ApiError {
43
+ constructor(message = "Failed to parse response", options) {
44
+ super(message, { code: "PARSE", retryable: false, ...options });
45
+ this.name = "ParseError";
46
+ }
47
+ };
48
+
49
+ // packages/core/src/errors/validation-error.ts
50
+ var ValidationError = class extends ApiError {
51
+ constructor(message = "Response validation failed", options) {
52
+ super(message, { code: "VALIDATION", retryable: false, ...options });
53
+ this.name = "ValidationError";
54
+ }
55
+ };
56
+
57
+ // packages/core/src/errors/timeout-error.ts
58
+ var TimeoutError = class extends ApiError {
59
+ constructor(message = "Request timed out", options) {
60
+ super(message, { code: "TIMEOUT", retryable: true, ...options });
61
+ this.name = "TimeoutError";
62
+ }
63
+ };
64
+
65
+ // packages/core/src/errors/abort-error.ts
66
+ var AbortError = class extends ApiError {
67
+ constructor(message = "Request was aborted", options) {
68
+ super(message, { code: "ABORT", retryable: false, ...options });
69
+ this.name = "AbortError";
70
+ }
71
+ };
72
+
73
+ // packages/core/src/errors/config-error.ts
74
+ var ConfigError = class extends ApiError {
75
+ constructor(message, options) {
76
+ super(message, { code: "CONFIG", retryable: false, ...options });
77
+ this.name = "ConfigError";
78
+ }
79
+ };
80
+
81
+ // packages/core/src/errors/security-error.ts
82
+ var SecurityError = class extends ApiError {
83
+ constructor(message, options) {
84
+ super(message, { code: "SECURITY", retryable: false, ...options });
85
+ this.name = "SecurityError";
86
+ }
87
+ };
88
+
89
+ // packages/core/src/errors/transform-error.ts
90
+ var TransformError = class extends ApiError {
91
+ /** Dotted location of the object that failed, e.g. `$.items[0].profile`. */
92
+ path;
93
+ keys;
94
+ constructor(message, details, options) {
95
+ super(message, { code: "TRANSFORM", retryable: false, ...options });
96
+ this.name = "TransformError";
97
+ this.path = details.path;
98
+ this.keys = details.keys;
99
+ }
100
+ };
101
+
102
+ // packages/core/src/errors/mock-error.ts
103
+ var STATUS = {
104
+ MOCK_NOT_FOUND: 404,
105
+ MOCK_DUPLICATE_ID: 409,
106
+ MOCK_INVALID_INPUT: 400,
107
+ MOCK_UNSUPPORTED: 501,
108
+ MOCK_LOAD: void 0
109
+ };
110
+ var MockError = class extends ApiError {
111
+ constructor(code, message, options) {
112
+ super(message, { code, status: STATUS[code], retryable: false, cause: options?.cause });
113
+ this.name = "MockError";
114
+ }
115
+ };
116
+
117
+ // packages/core/src/errors/guards.ts
118
+ function isApiError(error) {
119
+ return error instanceof ApiError;
120
+ }
121
+ function isHttpError(error) {
122
+ return error instanceof HttpError;
123
+ }
124
+ function isTimeoutError(error) {
125
+ return error instanceof TimeoutError;
126
+ }
127
+ function isMockError(error) {
128
+ return error instanceof MockError;
129
+ }
130
+
131
+ // packages/core/src/http/url.ts
132
+ var ABSOLUTE = /^https?:\/\//i;
133
+ function isAbsoluteUrl(value) {
134
+ return ABSOLUTE.test(value);
135
+ }
136
+ function parseBaseURL(value) {
137
+ if (typeof value !== "string" || value.trim() === "") {
138
+ throw new ConfigError(
139
+ `baseURL must be a non-empty string (got ${value === "" ? '""' : typeof value}).`
140
+ );
141
+ }
142
+ if (isAbsoluteUrl(value)) {
143
+ let url;
144
+ try {
145
+ url = new URL(value);
146
+ } catch (cause) {
147
+ throw new ConfigError(`baseURL "${value}" is not a valid URL.`, { cause });
148
+ }
149
+ if (url.search || url.hash) {
150
+ throw new ConfigError(`baseURL "${value}" must not contain a query string or fragment.`);
151
+ }
152
+ return { raw: value, origin: url.origin, path: url.pathname.replace(/\/+$/, "") };
153
+ }
154
+ if (value.startsWith("/") && !value.startsWith("//")) {
155
+ if (/[?#]/.test(value)) {
156
+ throw new ConfigError(`baseURL "${value}" must not contain a query string or fragment.`);
157
+ }
158
+ return { raw: value, origin: null, path: value.replace(/\/+$/, "") };
159
+ }
160
+ throw new ConfigError(
161
+ `baseURL "${value}" must be an absolute http(s) URL (e.g. "https://api.example.com") or a browser-relative path starting with "/" (e.g. "/api").`
162
+ );
163
+ }
164
+ function requestOrigin(base) {
165
+ if (base.origin) return base.origin;
166
+ const origin = globalThis.location?.origin;
167
+ if (typeof origin === "string" && origin !== "null" && origin !== "") return origin;
168
+ throw new ConfigError(
169
+ `baseURL "${base.raw}" is relative, but this environment has no browser origin to resolve it against. Use an absolute URL (e.g. "http://localhost:3000${base.raw}") outside the browser.`
170
+ );
171
+ }
172
+ function buildUrl(base, path, queryString, origin) {
173
+ if (isAbsoluteUrl(path)) return `${path}${queryString}`;
174
+ const normalized = path === "" || path === "/" ? "" : path.startsWith("/") ? path : `/${path}`;
175
+ return `${origin ?? ""}${base.path}${normalized}${queryString}`;
176
+ }
177
+ function assertSameOrigin(url, origin) {
178
+ let parsed;
179
+ try {
180
+ parsed = new URL(url, origin);
181
+ } catch (cause) {
182
+ throw new SecurityError(`Request URL "${url}" is not a valid URL.`, { cause });
183
+ }
184
+ if (parsed.origin !== origin || parsed.protocol !== "http:" && parsed.protocol !== "https:") {
185
+ throw new SecurityError(
186
+ `Request URL ${parsed.href} resolves outside the API origin ${origin}; it was not sent so credentials cannot leak. Use a separate client (or defineApis) for another origin.`
187
+ );
188
+ }
189
+ return parsed;
190
+ }
191
+
192
+ // packages/core/src/http/body.ts
193
+ function isJsonContentType(headers) {
194
+ const contentType = headers.get("content-type") ?? "";
195
+ return contentType.includes("application/json") || contentType.includes("+json");
196
+ }
197
+ function serializeBody(body, headers) {
198
+ if (body === void 0 || body === null) {
199
+ return body;
200
+ }
201
+ if (typeof body === "string" || body instanceof FormData || body instanceof URLSearchParams || body instanceof Blob || body instanceof ArrayBuffer || ArrayBuffer.isView(body) || typeof ReadableStream !== "undefined" && body instanceof ReadableStream) {
202
+ return body;
203
+ }
204
+ if (!headers.has("content-type")) {
205
+ headers.set("content-type", "application/json");
206
+ }
207
+ return JSON.stringify(body);
208
+ }
209
+
210
+ // packages/core/src/transport/read-body.ts
211
+ var RAW_TEXT_PREVIEW_LENGTH = 200;
212
+ function parseJson(text) {
213
+ try {
214
+ return JSON.parse(text);
215
+ } catch (cause) {
216
+ const preview = text.length > RAW_TEXT_PREVIEW_LENGTH ? `${text.slice(0, RAW_TEXT_PREVIEW_LENGTH)}\u2026` : text;
217
+ throw new ParseError(`Failed to parse response body as JSON: ${preview}`, { cause });
218
+ }
219
+ }
220
+ async function readBody(response, responseType = "json") {
221
+ if (response.status === 204 || response.status === 205) {
222
+ return null;
223
+ }
224
+ if (responseType === "stream") {
225
+ return response.body;
226
+ }
227
+ if (responseType === "blob") {
228
+ return response.blob();
229
+ }
230
+ if (responseType === "arrayBuffer") {
231
+ return response.arrayBuffer();
232
+ }
233
+ if (responseType === "text") {
234
+ return response.text();
235
+ }
236
+ const contentType = response.headers.get("content-type") ?? "";
237
+ if (isJsonContentType(response.headers)) {
238
+ const text2 = await response.text();
239
+ if (!text2) return null;
240
+ return parseJson(text2);
241
+ }
242
+ if (contentType.startsWith("text/")) {
243
+ return response.text();
244
+ }
245
+ const text = await response.text();
246
+ if (!text) return null;
247
+ try {
248
+ return JSON.parse(text);
249
+ } catch {
250
+ return text;
251
+ }
252
+ }
253
+
254
+ // packages/core/src/transport/fetch.ts
255
+ function createFetchTransport(fetchImpl = fetch) {
256
+ return {
257
+ async request(input) {
258
+ let response;
259
+ try {
260
+ response = await fetchImpl(input.url, {
261
+ method: input.method,
262
+ headers: input.headers,
263
+ body: input.body,
264
+ signal: input.signal
265
+ });
266
+ } catch (error) {
267
+ if (error instanceof DOMException && error.name === "AbortError") {
268
+ throw new AbortError("Request was aborted", { cause: error });
269
+ }
270
+ if (error instanceof DOMException && (error.name === "TimeoutError" || error.message.includes("timed out"))) {
271
+ throw new TimeoutError("Request timed out", { cause: error });
272
+ }
273
+ if (error instanceof Error && error.name === "AbortError") {
274
+ const reason = error.cause;
275
+ if (reason instanceof DOMException && (reason.name === "TimeoutError" || reason.message.includes("timed out"))) {
276
+ throw new TimeoutError("Request timed out", { cause: error });
277
+ }
278
+ throw new AbortError("Request was aborted", { cause: error });
279
+ }
280
+ throw new NetworkError(error instanceof Error ? error.message : "Network request failed", {
281
+ cause: error
282
+ });
283
+ }
284
+ const body = await readBody(response, input.responseType ?? "json");
285
+ return {
286
+ status: response.status,
287
+ headers: response.headers,
288
+ raw: response,
289
+ body
290
+ };
291
+ }
292
+ };
293
+ }
294
+
295
+ // packages/core/src/http/path.ts
296
+ var PARAM = /:([A-Za-z0-9_]+)/g;
297
+ function interpolatePath(path, params) {
298
+ const absolute = /^(https?:\/\/[^/]+)(.*)$/i.exec(path);
299
+ const head = absolute ? absolute[1] : "";
300
+ const tail = absolute ? absolute[2] : path;
301
+ return head + tail.replace(PARAM, (_, key) => {
302
+ const value = params?.[key];
303
+ if (value === void 0 || value === null || value === "") {
304
+ throw new ValidationError(`Missing path parameter "${key}" for ${path}`);
305
+ }
306
+ return encodeURIComponent(String(value));
307
+ });
308
+ }
309
+ function joinPath(base, relative) {
310
+ if (!relative || relative === "/") {
311
+ return base.replace(/\/+$/, "") || "/";
312
+ }
313
+ if (/^https?:\/\//i.test(relative)) {
314
+ return relative;
315
+ }
316
+ const normalizedBase = base.replace(/\/+$/, "");
317
+ const normalizedRelative = relative.startsWith("/") ? relative : `/${relative}`;
318
+ return `${normalizedBase}${normalizedRelative}`;
319
+ }
320
+ function extractPathParamNames(path) {
321
+ if (/^https?:\/\//i.test(path)) path = new URL(path).pathname;
322
+ const names = [];
323
+ const re = /:([A-Za-z0-9_]+)/g;
324
+ let match;
325
+ while ((match = re.exec(path)) !== null) {
326
+ names.push(match[1]);
327
+ }
328
+ return names;
329
+ }
330
+
331
+ // packages/core/src/mock/datasets.ts
332
+ var MOCK_DATASETS = {
333
+ "users-mock": "id",
334
+ "products-mock": "id",
335
+ "companies-mock": "id",
336
+ "orders-mock": "id",
337
+ "posts-mock": "id",
338
+ "comments-mock": "id",
339
+ "countries-csc": "id",
340
+ "countries-mledoze": "id",
341
+ "countries-parseapi": "country",
342
+ "country-codes-iso": "id",
343
+ "cities-capitals": "id",
344
+ "currencies-parseapi": "currency",
345
+ "languages-parseapi": "language",
346
+ "timezones-parseapi": "timezone",
347
+ "area-codes-parseapi": "area_code"
348
+ };
349
+ function isMockDatasetId(value) {
350
+ return typeof value === "string" && Object.prototype.hasOwnProperty.call(MOCK_DATASETS, value);
351
+ }
352
+ function listMockDatasetIds() {
353
+ return Object.keys(MOCK_DATASETS);
354
+ }
355
+ var LOADER_SLOT = /* @__PURE__ */ Symbol.for("@apixa/core.mockDatasetLoader");
356
+ function registerMockDatasetLoader(loader) {
357
+ globalThis[LOADER_SLOT] = loader;
358
+ }
359
+ async function loadNamedDataset(id) {
360
+ const datasetLoader = globalThis[LOADER_SLOT];
361
+ if (!datasetLoader) {
362
+ throw new MockError(
363
+ "MOCK_LOAD",
364
+ `mock: "${id}" needs the optional "@apixa/mocking" integration. Install it and import "@apixa/mocking/register" once (before the first call), or pass inline records / a provider instead.`
365
+ );
366
+ }
367
+ return datasetLoader(id);
368
+ }
369
+
370
+ // packages/core/src/mock/source.ts
371
+ var MOCK_OBJECT_KEYS = /* @__PURE__ */ new Set(["data", "idKey", "seed"]);
372
+ var SEED_KEYS = /* @__PURE__ */ new Set(["limit", "offset"]);
373
+ function isPlainObject(value) {
374
+ return value !== null && typeof value === "object" && !Array.isArray(value);
375
+ }
376
+ function describe(value) {
377
+ return Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
378
+ }
379
+ function normalizeMockSource(resource, value) {
380
+ const label = `resources.${resource}.mock`;
381
+ let data = value;
382
+ let idKey;
383
+ let seed;
384
+ if (isPlainObject(value)) {
385
+ for (const key of Object.keys(value)) {
386
+ if (!MOCK_OBJECT_KEYS.has(key)) {
387
+ throw new ConfigError(
388
+ `${label} has unknown option "${key}". Supported: data, idKey, seed. Operation behavior belongs in resources.${resource}.operations, not in mock.`
389
+ );
390
+ }
391
+ }
392
+ if (!("data" in value)) throw new ConfigError(`${label} object needs a "data" source.`);
393
+ data = value.data;
394
+ if (value.idKey !== void 0) {
395
+ if (typeof value.idKey !== "string" || value.idKey === "") {
396
+ throw new ConfigError(`${label}.idKey must be a non-empty string.`);
397
+ }
398
+ idKey = value.idKey;
399
+ }
400
+ if (value.seed !== void 0) seed = normalizeSeed(label, value.seed);
401
+ }
402
+ if (typeof data === "string") {
403
+ if (!isMockDatasetId(data)) {
404
+ throw new ConfigError(
405
+ `${label}: unknown dataset "${data}". Available: ${listMockDatasetIds().join(", ")}.`
406
+ );
407
+ }
408
+ const id = data;
409
+ const datasetIdKey = MOCK_DATASETS[id];
410
+ if (idKey !== void 0 && idKey !== datasetIdKey) {
411
+ throw new ConfigError(
412
+ `${label}.idKey "${idKey}" does not match dataset "${id}" (uses "${datasetIdKey}").`
413
+ );
414
+ }
415
+ return { label, idKey: datasetIdKey, load: () => loadNamedDataset(id), seed, immutable: true };
416
+ }
417
+ if (Array.isArray(data)) {
418
+ const records = data;
419
+ const key = idKey ?? "id";
420
+ validateRecords(label, records, key);
421
+ return { label, idKey: key, load: () => records, seed };
422
+ }
423
+ if (typeof data === "function") {
424
+ const provider = data;
425
+ return {
426
+ label,
427
+ idKey: idKey ?? "id",
428
+ seed,
429
+ load: () => {
430
+ const result = provider();
431
+ return typeof result?.then === "function" ? Promise.resolve(result).then((records) => assertArray(label, records)) : assertArray(label, result);
432
+ }
433
+ };
434
+ }
435
+ if (typeof value === "boolean") {
436
+ throw new ConfigError(
437
+ `${label} must be a dataset id, an array of records, a provider function, or { data, idKey?, seed? } (got ${String(value)}). mock: true was removed \u2014 use mock: "<dataset-id>".`
438
+ );
439
+ }
440
+ throw new ConfigError(
441
+ `${label} must be a dataset id, an array of records, a provider function, or { data, idKey?, seed? } (got ${describe(data)}).`
442
+ );
443
+ }
444
+ function normalizeSeed(label, value) {
445
+ if (!isPlainObject(value)) throw new ConfigError(`${label}.seed must be { limit?, offset? }.`);
446
+ for (const key of Object.keys(value)) {
447
+ if (!SEED_KEYS.has(key))
448
+ throw new ConfigError(`${label}.seed has unknown option "${key}". Supported: limit, offset.`);
449
+ }
450
+ const seed = {};
451
+ for (const key of ["limit", "offset"]) {
452
+ const n = value[key];
453
+ if (n === void 0) continue;
454
+ if (typeof n !== "number" || !Number.isInteger(n) || n < 0) {
455
+ throw new ConfigError(
456
+ `${label}.seed.${key} must be a non-negative integer (got ${String(n)}).`
457
+ );
458
+ }
459
+ seed[key] = n;
460
+ }
461
+ return seed;
462
+ }
463
+ function assertArray(label, records) {
464
+ if (!Array.isArray(records)) {
465
+ throw new ConfigError(
466
+ `${label} provider must return an array of records (got ${describe(records)}).`
467
+ );
468
+ }
469
+ return records;
470
+ }
471
+ function validateRecords(label, records, idKey) {
472
+ const seen = /* @__PURE__ */ new Set();
473
+ records.forEach((record, i) => {
474
+ if (!isPlainObject(record)) {
475
+ throw new ConfigError(`${label}[${i}] must be a plain object (got ${describe(record)}).`);
476
+ }
477
+ const id = record[idKey];
478
+ if (typeof id !== "string" && typeof id !== "number" || id === "") {
479
+ throw new ConfigError(
480
+ `${label}[${i}] is missing identity field "${idKey}" (string or number).`
481
+ );
482
+ }
483
+ const key = String(id);
484
+ if (seen.has(key)) throw new ConfigError(`${label}: duplicate ${idKey} "${key}".`);
485
+ seen.add(key);
486
+ });
487
+ }
488
+
489
+ // packages/core/src/mock/store.ts
490
+ var clone = (value) => structuredClone(value);
491
+ var own = (row, key) => Object.hasOwn(row, key) ? row[key] : void 0;
492
+ var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
493
+ function findUnsafeKey(root) {
494
+ const stack = [root];
495
+ const seen = /* @__PURE__ */ new Set();
496
+ while (stack.length > 0) {
497
+ const value = stack.pop();
498
+ if (value === null || typeof value !== "object" || seen.has(value)) continue;
499
+ seen.add(value);
500
+ for (const key of Object.keys(value)) {
501
+ if (UNSAFE_KEYS.has(key)) return key;
502
+ stack.push(value[key]);
503
+ }
504
+ }
505
+ return void 0;
506
+ }
507
+ var RESERVED_QUERY_KEYS = /* @__PURE__ */ new Set(["page", "pageSize", "cursor", "id", "id[]"]);
508
+ function filterEntries(query) {
509
+ if (!query) return [];
510
+ return Object.entries(query).filter(([k, v]) => !RESERVED_QUERY_KEYS.has(k) && v !== void 0);
511
+ }
512
+ function matchesExact(row, entries) {
513
+ return entries.every(
514
+ ([key, value]) => Array.isArray(value) ? value.map(String).includes(String(own(row, key))) : String(own(row, key)) === String(value)
515
+ );
516
+ }
517
+ function matchesContains(row, entries) {
518
+ return entries.every(
519
+ ([key, value]) => String(own(row, key) ?? "").toLowerCase().includes(String(value).toLowerCase())
520
+ );
521
+ }
522
+ var MockStore = class {
523
+ idKey;
524
+ source;
525
+ initial;
526
+ /**
527
+ * Working rows. They share row objects with `initial` until a row is replaced: every write
528
+ * builds a new row object, and nothing hands out a reference (reads clone). `liveRows()` is the
529
+ * one exception, so it first makes the working rows private (`shared = false`).
530
+ */
531
+ rows = [];
532
+ shared = true;
533
+ index = /* @__PURE__ */ new Map();
534
+ loading;
535
+ numericIds = true;
536
+ numberTyped = true;
537
+ /** Monotonic id counter: generated ids are never reused, even after deletes. */
538
+ counter = 0;
539
+ constructor(source) {
540
+ this.source = source;
541
+ this.idKey = source.idKey;
542
+ }
543
+ fail(code, message) {
544
+ return new MockError(code, `${this.source.label}: ${message}`);
545
+ }
546
+ /** Load + validate + seed once. Concurrent first calls share one load; a failure is retried next call. */
547
+ ready() {
548
+ if (this.initial) return Promise.resolve();
549
+ this.loading ??= (async () => {
550
+ let records;
551
+ try {
552
+ records = await this.source.load();
553
+ validateRecords(this.source.label, records, this.idKey);
554
+ } catch (cause) {
555
+ if (cause instanceof MockError) throw cause;
556
+ const detail = cause instanceof Error ? cause.message : String(cause);
557
+ throw new MockError(
558
+ "MOCK_LOAD",
559
+ `${this.source.label}: failed to load mock data \u2014 ${detail}`,
560
+ { cause }
561
+ );
562
+ }
563
+ const { limit, offset = 0 } = this.source.seed ?? {};
564
+ const seeded = records.slice(
565
+ offset,
566
+ limit === void 0 ? void 0 : offset + limit
567
+ );
568
+ this.initial = this.source.immutable ? seeded : seeded.map(clone);
569
+ this.applyInitial();
570
+ })().finally(() => {
571
+ this.loading = void 0;
572
+ });
573
+ return this.loading;
574
+ }
575
+ applyInitial() {
576
+ this.rows = this.initial.slice();
577
+ this.shared = true;
578
+ this.index = new Map(this.rows.map((row) => [String(row[this.idKey]), row]));
579
+ this.numericIds = this.rows.every((row) => /^\d+$/.test(String(row[this.idKey])));
580
+ this.numberTyped = this.rows.length === 0 || typeof this.rows[0][this.idKey] === "number";
581
+ this.counter = this.numericIds ? Math.max(0, ...this.rows.map((row) => Number(row[this.idKey]))) : 0;
582
+ }
583
+ /**
584
+ * Live rows for custom `defineMockApi` operations that edit records in place. Call
585
+ * `reindex()` afterwards if ids were added, removed, or changed.
586
+ */
587
+ liveRows() {
588
+ if (this.shared) {
589
+ this.rows = this.rows.map(clone);
590
+ this.shared = false;
591
+ this.reindex();
592
+ }
593
+ return this.rows;
594
+ }
595
+ reindex() {
596
+ this.index = new Map(this.rows.map((row) => [String(row[this.idKey]), row]));
597
+ }
598
+ /** Back to the seeded state. A store that never loaded stays unloaded (next call seeds it). */
599
+ reset() {
600
+ if (this.initial) this.applyInitial();
601
+ }
602
+ key(id) {
603
+ if (typeof id !== "string" && typeof id !== "number" || id === "") {
604
+ throw this.fail(
605
+ "MOCK_INVALID_INPUT",
606
+ `invalid ${this.idKey} ${JSON.stringify(id) ?? String(id)} (expected a non-empty string or number)`
607
+ );
608
+ }
609
+ return String(id);
610
+ }
611
+ find(id) {
612
+ const key = this.key(id);
613
+ const row = this.index.get(key);
614
+ if (!row) throw this.fail("MOCK_NOT_FOUND", `no record with ${this.idKey} "${key}"`);
615
+ return row;
616
+ }
617
+ input(value, what) {
618
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
619
+ throw this.fail("MOCK_INVALID_INPUT", `${what} must be a plain object`);
620
+ }
621
+ let copy;
622
+ try {
623
+ copy = clone(value);
624
+ } catch (cause) {
625
+ throw new MockError(
626
+ "MOCK_INVALID_INPUT",
627
+ `${this.source.label}: ${what} must be structured-cloneable`,
628
+ { cause }
629
+ );
630
+ }
631
+ const unsafe = findUnsafeKey(copy);
632
+ if (unsafe !== void 0) {
633
+ throw this.fail("MOCK_INVALID_INPUT", `${what} must not contain the key "${unsafe}"`);
634
+ }
635
+ return copy;
636
+ }
637
+ nextId() {
638
+ if (!this.numericIds) {
639
+ throw this.fail(
640
+ "MOCK_INVALID_INPUT",
641
+ `create() needs "${this.idKey}": this data has non-numeric ids`
642
+ );
643
+ }
644
+ do
645
+ this.counter++;
646
+ while (this.index.has(String(this.counter)));
647
+ return this.numberTyped ? this.counter : String(this.counter);
648
+ }
649
+ assertSameId(input, current) {
650
+ if (input[this.idKey] !== void 0 && String(input[this.idKey]) !== String(current[this.idKey])) {
651
+ throw this.fail(
652
+ "MOCK_INVALID_INPUT",
653
+ `cannot change ${this.idKey} from "${String(current[this.idKey])}"`
654
+ );
655
+ }
656
+ }
657
+ insert(row) {
658
+ if (row[this.idKey] === void 0) row[this.idKey] = this.nextId();
659
+ const key = this.key(row[this.idKey]);
660
+ if (this.index.has(key))
661
+ throw this.fail("MOCK_DUPLICATE_ID", `a record with ${this.idKey} "${key}" already exists`);
662
+ this.rows.push(row);
663
+ this.index.set(key, row);
664
+ return row;
665
+ }
666
+ replaceRow(current, next) {
667
+ const i = this.rows.indexOf(current);
668
+ this.rows[i] = next;
669
+ this.index.set(String(current[this.idKey]), next);
670
+ return next;
671
+ }
672
+ // ---- reads (each independent; none calls another public operation) ----
673
+ /**
674
+ * Matching rows by reference, for the engine only: callers must clone (`cloneRows`) whatever
675
+ * they return. Lets a paginated read copy one page instead of the whole dataset.
676
+ */
677
+ select(query) {
678
+ const entries = filterEntries(query);
679
+ const ids = query?.id ?? query?.["id[]"];
680
+ let rows = this.rows;
681
+ if (ids !== void 0) {
682
+ const wanted = new Set((Array.isArray(ids) ? ids : [ids]).map(String));
683
+ rows = rows.filter((row) => wanted.has(String(own(row, this.idKey))));
684
+ }
685
+ return entries.length ? rows.filter((row) => matchesExact(row, entries)) : rows;
686
+ }
687
+ cloneRows(rows) {
688
+ return rows.map(clone);
689
+ }
690
+ /** Exact-match filter over non-paging query keys; `id` values select those ids. */
691
+ list(query) {
692
+ return this.cloneRows(this.select(query));
693
+ }
694
+ get(id) {
695
+ return clone(this.find(id));
696
+ }
697
+ /** Requested order, missing ids omitted, duplicates collapsed. */
698
+ getMany(ids) {
699
+ if (!Array.isArray(ids))
700
+ throw this.fail("MOCK_INVALID_INPUT", "getMany expects an array of ids");
701
+ const out = [];
702
+ const seen = /* @__PURE__ */ new Set();
703
+ for (const id of ids) {
704
+ const key = this.key(id);
705
+ if (seen.has(key)) continue;
706
+ seen.add(key);
707
+ const row = this.index.get(key);
708
+ if (row) out.push(clone(row));
709
+ }
710
+ return out;
711
+ }
712
+ exists(id) {
713
+ return this.index.has(this.key(id));
714
+ }
715
+ getBy(name, value) {
716
+ const row = this.rows.find((r) => String(own(r, name)) === String(value));
717
+ if (!row) throw this.fail("MOCK_NOT_FOUND", `no record with ${name} "${String(value)}"`);
718
+ return clone(row);
719
+ }
720
+ findFirst(query) {
721
+ const entries = filterEntries(query);
722
+ const row = this.rows.find((r) => matchesExact(r, entries));
723
+ return row ? clone(row) : null;
724
+ }
725
+ /** Case-insensitive substring match on every given key. */
726
+ search(query) {
727
+ return this.cloneRows(this.selectContaining(query));
728
+ }
729
+ /** Substring matches by reference (see `select`). */
730
+ selectContaining(query) {
731
+ const entries = filterEntries(query);
732
+ return entries.length ? this.rows.filter((row) => matchesContains(row, entries)) : this.rows;
733
+ }
734
+ count(query) {
735
+ const entries = filterEntries(query);
736
+ return entries.length ? this.rows.filter((row) => matchesExact(row, entries)).length : this.rows.length;
737
+ }
738
+ // ---- writes ----
739
+ create(body) {
740
+ return clone(this.insert(this.input(body, "create() input")));
741
+ }
742
+ /** Validates every item first, then inserts all (no partial writes on duplicate ids). */
743
+ createMany(bodies) {
744
+ if (!Array.isArray(bodies))
745
+ throw this.fail("MOCK_INVALID_INPUT", "createMany expects an array");
746
+ const rows = bodies.map((body, i) => this.input(body, `createMany[${i}]`));
747
+ const keys = /* @__PURE__ */ new Set();
748
+ for (const row of rows) {
749
+ if (row[this.idKey] === void 0) continue;
750
+ const key = this.key(row[this.idKey]);
751
+ if (this.index.has(key) || keys.has(key)) {
752
+ throw this.fail("MOCK_DUPLICATE_ID", `a record with ${this.idKey} "${key}" already exists`);
753
+ }
754
+ keys.add(key);
755
+ }
756
+ return rows.map((row) => clone(this.insert(row)));
757
+ }
758
+ update(id, body) {
759
+ const changes = this.input(body, "update() input");
760
+ const current = this.find(id);
761
+ this.assertSameId(changes, current);
762
+ const { [this.idKey]: _ignored, ...fields } = changes;
763
+ return clone(this.replaceRow(current, { [this.idKey]: current[this.idKey], ...fields }));
764
+ }
765
+ patch(id, body) {
766
+ const changes = this.input(body, "patch() input");
767
+ const current = this.find(id);
768
+ this.assertSameId(changes, current);
769
+ const next = { ...current };
770
+ for (const [key, value] of Object.entries(changes)) {
771
+ if (value !== void 0 && key !== this.idKey) next[key] = value;
772
+ }
773
+ return clone(this.replaceRow(current, next));
774
+ }
775
+ /** Replace when the id exists, otherwise insert with that id. */
776
+ upsert(id, body) {
777
+ const key = this.key(id);
778
+ const changes = this.input(body, "upsert() input");
779
+ const current = this.index.get(key);
780
+ if (current) {
781
+ this.assertSameId(changes, current);
782
+ const { [this.idKey]: _ignored, ...fields } = changes;
783
+ return clone(this.replaceRow(current, { [this.idKey]: current[this.idKey], ...fields }));
784
+ }
785
+ if (changes[this.idKey] !== void 0 && String(changes[this.idKey]) !== key) {
786
+ throw this.fail("MOCK_INVALID_INPUT", `body ${this.idKey} does not match "${key}"`);
787
+ }
788
+ return clone(this.insert({ ...changes, [this.idKey]: id }));
789
+ }
790
+ delete(id) {
791
+ const row = this.find(id);
792
+ this.rows.splice(this.rows.indexOf(row), 1);
793
+ this.index.delete(String(row[this.idKey]));
794
+ }
795
+ /** Removes the listed ids that exist; missing ids are ignored (idempotent bulk delete). */
796
+ deleteMany(ids) {
797
+ if (!Array.isArray(ids))
798
+ throw this.fail("MOCK_INVALID_INPUT", "deleteMany expects an array of ids");
799
+ const keys = new Set(ids.map((id) => this.key(id)));
800
+ this.rows = this.rows.filter((row) => !keys.has(String(row[this.idKey])));
801
+ for (const key of keys) this.index.delete(key);
802
+ }
803
+ };
804
+
805
+ // packages/core/src/mock/execute.ts
806
+ var DEFAULT_PAGE_SIZE = 20;
807
+ function positiveInt(store, name, raw, fallback, min) {
808
+ if (raw === void 0 || raw === null || raw === "") return fallback;
809
+ const n = typeof raw === "number" ? raw : Number(raw);
810
+ if (!Number.isInteger(n) || n < min) {
811
+ throw new MockError(
812
+ "MOCK_INVALID_INPUT",
813
+ `${store}: ${name} must be an integer >= ${min} (got ${String(raw)})`
814
+ );
815
+ }
816
+ return n;
817
+ }
818
+ function paginate(store, label, rows, mode, query = {}) {
819
+ if (!mode) return store.cloneRows(rows);
820
+ const pageSize = positiveInt(label, "pageSize", query.pageSize, DEFAULT_PAGE_SIZE, 1);
821
+ if (mode === "page") {
822
+ const page = positiveInt(label, "page", query.page, 1, 1);
823
+ const start2 = (page - 1) * pageSize;
824
+ return {
825
+ data: store.cloneRows(rows.slice(start2, start2 + pageSize)),
826
+ total: rows.length,
827
+ page,
828
+ pageSize
829
+ };
830
+ }
831
+ const start = positiveInt(label, "cursor", query.cursor, 0, 0);
832
+ const end = start + pageSize;
833
+ return {
834
+ data: store.cloneRows(rows.slice(start, end)),
835
+ nextCursor: end < rows.length ? String(end) : null
836
+ };
837
+ }
838
+ async function runBuiltin(store, source, intent, pagination) {
839
+ await store.ready();
840
+ const q = intent.query;
841
+ switch (intent.op) {
842
+ case "getAll":
843
+ return { status: 200, data: paginate(store, source.label, store.select(q), pagination, q) };
844
+ case "getById":
845
+ return { status: 200, data: store.get(intent.id) };
846
+ case "getBySlug":
847
+ return { status: 200, data: store.getBy("slug", intent.slug) };
848
+ case "getMany":
849
+ return { status: 200, data: store.getMany(intent.ids) };
850
+ case "getBy":
851
+ return { status: 200, data: store.getBy(intent.field, intent.value) };
852
+ case "findFirst":
853
+ return { status: 200, data: store.findFirst(q) };
854
+ case "search":
855
+ return { status: 200, data: store.search(q) };
856
+ case "exists":
857
+ return { status: 200, data: store.exists(intent.id) };
858
+ case "count":
859
+ return { status: 200, data: store.count(q) };
860
+ case "create":
861
+ return { status: 201, data: store.create(intent.body) };
862
+ case "update":
863
+ return { status: 200, data: store.update(intent.id, intent.body) };
864
+ case "patch":
865
+ return { status: 200, data: store.patch(intent.id, intent.body) };
866
+ case "upsert":
867
+ return { status: 200, data: store.upsert(intent.id, intent.body) };
868
+ case "delete":
869
+ store.delete(intent.id);
870
+ return { status: 204, data: null };
871
+ case "createMany":
872
+ return { status: 201, data: store.createMany(intent.body) };
873
+ case "deleteMany":
874
+ store.deleteMany(intent.body);
875
+ return { status: 204, data: null };
876
+ }
877
+ }
878
+ function routeToIntent(method, relativePath, query, body, label) {
879
+ if (/^[a-z][a-z0-9+.-]*:/i.test(relativePath)) {
880
+ throw new MockError(
881
+ "MOCK_UNSUPPORTED",
882
+ `${label}: cannot simulate absolute URL "${relativePath}" on a mocked resource`
883
+ );
884
+ }
885
+ const path = relativePath.split("?")[0].replace(/\/+$/, "");
886
+ const segments = path.split("/").filter(Boolean).map(decodeURIComponent);
887
+ const m = method.toUpperCase();
888
+ if (segments.length === 0) {
889
+ if (m === "GET") return { op: "getAll", query };
890
+ if (m === "POST") return { op: "create", body };
891
+ } else if (segments.length === 1) {
892
+ const [seg] = segments;
893
+ if (seg === "count" && m === "GET") return { op: "count", query };
894
+ if (seg === "search" && m === "GET") return { op: "search", query };
895
+ if (seg === "bulk" && m === "POST") return { op: "createMany", body };
896
+ if (seg === "bulk" && m === "DELETE") return { op: "deleteMany", body };
897
+ if (m === "GET") return { op: "getById", id: seg };
898
+ if (m === "PUT") return { op: "update", id: seg, body };
899
+ if (m === "PATCH") return { op: "patch", id: seg, body };
900
+ if (m === "DELETE") return { op: "delete", id: seg };
901
+ } else if (segments.length === 2 && segments[0] === "slug" && m === "GET") {
902
+ return { op: "getBySlug", slug: segments[1] };
903
+ }
904
+ throw new MockError(
905
+ "MOCK_UNSUPPORTED",
906
+ `${label}: cannot simulate ${m} "${relativePath || "/"}" against in-memory records. Supported: GET/POST "", GET/PUT/PATCH/DELETE "/:id", GET "/count", GET "/search", GET "/slug/:slug", POST/DELETE "/bulk".`
907
+ );
908
+ }
909
+
910
+ // packages/core/src/mock/handler.ts
911
+ function createStoreHandle(store) {
912
+ const handle = {
913
+ get: (id) => store.get(id),
914
+ exists: (id) => store.exists(id),
915
+ list: (query) => store.list(query),
916
+ create: (input) => store.create(input),
917
+ update: (id, input) => store.update(id, input),
918
+ patch: (id, input) => store.patch(id, input),
919
+ delete: (id) => store.delete(id)
920
+ };
921
+ return Object.freeze(handle);
922
+ }
923
+ async function runMockHandler(handler, store, call) {
924
+ await store.ready();
925
+ if (call.signal.aborted) throw call.signal.reason ?? new AbortError();
926
+ const handle = createStoreHandle(store);
927
+ const context = {
928
+ get records() {
929
+ return store.list();
930
+ },
931
+ store: handle,
932
+ params: { ...call.params },
933
+ query: { ...call.query ?? {} },
934
+ body: call.body,
935
+ signal: call.signal,
936
+ resource: call.resource,
937
+ operation: call.operation
938
+ };
939
+ const pending = Promise.resolve().then(() => handler(context));
940
+ const data = await raceAbort(pending, call.signal);
941
+ const noContent = call.method === "DELETE" && (data === void 0 || data === null);
942
+ return { status: noContent ? 204 : 200, data: noContent ? null : data };
943
+ }
944
+ function raceAbort(work, signal) {
945
+ if (signal.aborted) {
946
+ work.catch(() => void 0);
947
+ return Promise.reject(signal.reason ?? new AbortError());
948
+ }
949
+ return new Promise((resolve, reject) => {
950
+ const onAbort = () => reject(signal.reason ?? new AbortError());
951
+ signal.addEventListener("abort", onAbort, { once: true });
952
+ work.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
953
+ });
954
+ }
955
+
956
+ // packages/core/src/operation/conventions.ts
957
+ var BUILTIN_CONVENTIONS = {
958
+ getAll: { method: "GET", path: "", access: "read", extended: false, many: false },
959
+ getById: { method: "GET", path: "/:id", access: "read", extended: false, many: false },
960
+ create: { method: "POST", path: "", access: "write", extended: false, many: false },
961
+ update: { method: "PUT", path: "/:id", access: "write", extended: false, many: false },
962
+ delete: { method: "DELETE", path: "/:id", access: "write", extended: false, many: false },
963
+ getBySlug: { method: "GET", path: "/slug/:slug", access: "read", extended: true, many: false },
964
+ patch: { method: "PATCH", path: "/:id", access: "write", extended: true, many: false },
965
+ upsert: { method: "PUT", path: "/:id", access: "write", extended: true, many: false },
966
+ count: { method: "GET", path: "/count", access: "read", extended: true, many: false },
967
+ search: { method: "GET", path: "/search", access: "read", extended: true, many: false },
968
+ createMany: { method: "POST", path: "/bulk", access: "write", extended: true, many: true },
969
+ deleteMany: {
970
+ method: "DELETE",
971
+ path: "/bulk",
972
+ hasBody: true,
973
+ access: "write",
974
+ extended: true,
975
+ many: true
976
+ },
977
+ getMany: {
978
+ method: "GET",
979
+ path: "",
980
+ access: "read",
981
+ extended: true,
982
+ many: true,
983
+ strategy: "getMany"
984
+ },
985
+ exists: {
986
+ method: "GET",
987
+ path: "/:id",
988
+ access: "read",
989
+ extended: true,
990
+ many: false,
991
+ strategy: "exists"
992
+ },
993
+ findFirst: {
994
+ method: "GET",
995
+ path: "",
996
+ access: "read",
997
+ extended: true,
998
+ many: false,
999
+ strategy: "findFirst"
1000
+ },
1001
+ getBy: {
1002
+ method: "GET",
1003
+ path: "",
1004
+ access: "read",
1005
+ extended: true,
1006
+ many: false,
1007
+ strategy: "getBy"
1008
+ }
1009
+ };
1010
+ function isBuiltinName(name) {
1011
+ return Object.prototype.hasOwnProperty.call(BUILTIN_CONVENTIONS, name);
1012
+ }
1013
+
1014
+ // packages/core/src/operation/expand.ts
1015
+ var GROUP_ORDER = ["extended", "read", "write", "many"];
1016
+ function expandOperations(operations, options = {}) {
1017
+ const enabled = /* @__PURE__ */ new Map();
1018
+ for (const [name, convention] of Object.entries(BUILTIN_CONVENTIONS)) {
1019
+ let on = options.defaultOperations !== false;
1020
+ if (options.readOnly && convention.access === "write") on = false;
1021
+ if (options.writeOnly && convention.access === "read") on = false;
1022
+ for (const group of GROUP_ORDER) {
1023
+ const choice = options.operationGroups?.[group];
1024
+ if (choice === void 0) continue;
1025
+ if (group === "extended" && convention.extended || group === "many" && convention.many || group === convention.access) {
1026
+ on = choice;
1027
+ }
1028
+ }
1029
+ enabled.set(name, on);
1030
+ }
1031
+ const expanded = {};
1032
+ for (const name of Object.keys(BUILTIN_CONVENTIONS)) {
1033
+ const override = operations?.[name];
1034
+ if (override === false || override === void 0 && !enabled.get(name)) continue;
1035
+ expanded[name] = typeof override === "function" ? override : override === true || override === void 0 ? {} : override;
1036
+ }
1037
+ for (const [name, definition] of Object.entries(operations ?? {})) {
1038
+ if (isBuiltinName(name) || definition === false || definition === void 0 || definition === true)
1039
+ continue;
1040
+ expanded[name] = definition;
1041
+ }
1042
+ return expanded;
1043
+ }
1044
+
1045
+ // packages/core/src/operation/args.ts
1046
+ function parseOperationArgs(pathParamNames, hasBody, args) {
1047
+ let index = 0;
1048
+ let params;
1049
+ if (pathParamNames.length === 1) {
1050
+ const name = pathParamNames[0];
1051
+ params = { [name]: args[index++] };
1052
+ } else if (pathParamNames.length > 1) {
1053
+ params = args[index++];
1054
+ }
1055
+ const body = hasBody ? args[index++] : void 0;
1056
+ const options = args[index];
1057
+ return { params, body, options };
1058
+ }
1059
+
1060
+ // packages/core/src/http/headers.ts
1061
+ function apply(target, source) {
1062
+ if (!source) return;
1063
+ if (source instanceof Headers) {
1064
+ source.forEach((value, key) => target.set(key, value));
1065
+ return;
1066
+ }
1067
+ if (Array.isArray(source)) {
1068
+ for (const [key, value] of source) target.set(key, value);
1069
+ return;
1070
+ }
1071
+ for (const [key, value] of Object.entries(source)) {
1072
+ if (value === void 0 || value === null) target.delete(key);
1073
+ else target.set(key, String(value));
1074
+ }
1075
+ }
1076
+ async function resolveHeaders(sources, request) {
1077
+ const headers = new Headers();
1078
+ for (const source of sources) {
1079
+ if (typeof source === "function") {
1080
+ let value;
1081
+ try {
1082
+ value = await source();
1083
+ } catch (cause) {
1084
+ const detail = cause instanceof Error ? cause.message : String(cause);
1085
+ throw new ApiError(`Header provider failed: ${detail}`, { code: "HEADERS", cause });
1086
+ }
1087
+ apply(headers, value);
1088
+ } else {
1089
+ apply(headers, source);
1090
+ }
1091
+ }
1092
+ apply(headers, request);
1093
+ return headers;
1094
+ }
1095
+
1096
+ // packages/core/src/http/query.ts
1097
+ function buildQueryString(query, format = "repeat") {
1098
+ if (!query) return "";
1099
+ const parts = [];
1100
+ for (const [key, value] of Object.entries(query)) {
1101
+ if (value === void 0 || value === null) continue;
1102
+ if (Array.isArray(value)) {
1103
+ if (format === "comma") {
1104
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value.map(String).join(","))}`);
1105
+ } else if (format === "brackets") {
1106
+ for (const item of value) {
1107
+ parts.push(`${encodeURIComponent(key)}[]=${encodeURIComponent(String(item))}`);
1108
+ }
1109
+ } else {
1110
+ for (const item of value) {
1111
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(item))}`);
1112
+ }
1113
+ }
1114
+ } else {
1115
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
1116
+ }
1117
+ }
1118
+ return parts.length ? `?${parts.join("&")}` : "";
1119
+ }
1120
+
1121
+ // packages/core/src/transform/keys.ts
1122
+ var KEY_CASES = [
1123
+ "camelCase",
1124
+ "snake_case",
1125
+ "PascalCase",
1126
+ "kebab-case"
1127
+ ];
1128
+ function isKeyCase(value) {
1129
+ return typeof value === "string" && KEY_CASES.includes(value);
1130
+ }
1131
+ var isSeparator = (c) => c === "_" || c === "-";
1132
+ var isUpper = (c) => c !== c.toLowerCase() && c === c.toUpperCase();
1133
+ function toDelimited(key, sep) {
1134
+ let out = "";
1135
+ for (const c of key) {
1136
+ if (isSeparator(c)) {
1137
+ if (out !== "" && !out.endsWith(sep)) out += sep;
1138
+ } else if (isUpper(c)) {
1139
+ out += out === "" || out.endsWith(sep) ? c.toLowerCase() : sep + c.toLowerCase();
1140
+ } else {
1141
+ out += c;
1142
+ }
1143
+ }
1144
+ return out;
1145
+ }
1146
+ function toCamel(key) {
1147
+ let out = "";
1148
+ let upperNext = false;
1149
+ for (const c of key) {
1150
+ if (isSeparator(c)) {
1151
+ upperNext = out !== "";
1152
+ } else if (out === "") {
1153
+ out += c.toLowerCase();
1154
+ upperNext = false;
1155
+ } else {
1156
+ out += upperNext ? c.toUpperCase() : c;
1157
+ upperNext = false;
1158
+ }
1159
+ }
1160
+ return out;
1161
+ }
1162
+ function convertKey(key, to) {
1163
+ switch (to) {
1164
+ case "camelCase":
1165
+ return toCamel(key);
1166
+ case "PascalCase": {
1167
+ const camel = toCamel(key);
1168
+ return camel.charAt(0).toUpperCase() + camel.slice(1);
1169
+ }
1170
+ case "snake_case":
1171
+ return toDelimited(key, "_");
1172
+ case "kebab-case":
1173
+ return toDelimited(key, "-");
1174
+ }
1175
+ }
1176
+ function isPlainObject2(value) {
1177
+ if (value === null || typeof value !== "object") return false;
1178
+ const proto = Object.getPrototypeOf(value);
1179
+ return proto === Object.prototype || proto === null;
1180
+ }
1181
+ var MAX_DEPTH = 64;
1182
+ function transformKeys(value, to, cache) {
1183
+ return walk(value, to, cache ?? /* @__PURE__ */ new Map(), "$", 0);
1184
+ }
1185
+ function walk(value, to, cache, path, depth) {
1186
+ if (depth > MAX_DEPTH) {
1187
+ throw new TransformError(`Key transform exceeded max depth ${MAX_DEPTH} at ${path}`, {
1188
+ path,
1189
+ keys: []
1190
+ });
1191
+ }
1192
+ if (Array.isArray(value)) {
1193
+ return value.map((item, i) => walk(item, to, cache, `${path}[${i}]`, depth + 1));
1194
+ }
1195
+ if (!isPlainObject2(value)) return value;
1196
+ const out = {};
1197
+ const origin = /* @__PURE__ */ new Map();
1198
+ for (const key of Object.keys(value)) {
1199
+ let next = cache.get(key);
1200
+ if (next === void 0) {
1201
+ next = convertKey(key, to);
1202
+ cache.set(key, next);
1203
+ }
1204
+ const previous = origin.get(next);
1205
+ if (previous !== void 0) {
1206
+ throw new TransformError(
1207
+ `Key transform to ${to} collision at ${path}: "${previous}" and "${key}" both become "${next}"`,
1208
+ { path, keys: [previous, key] }
1209
+ );
1210
+ }
1211
+ origin.set(next, key);
1212
+ const converted = walk(value[key], to, cache, `${path}.${key}`, depth + 1);
1213
+ if (next === "__proto__") {
1214
+ Object.defineProperty(out, next, {
1215
+ value: converted,
1216
+ enumerable: true,
1217
+ writable: true,
1218
+ configurable: true
1219
+ });
1220
+ } else {
1221
+ out[next] = converted;
1222
+ }
1223
+ }
1224
+ return out;
1225
+ }
1226
+
1227
+ // packages/core/src/request/retry.ts
1228
+ var DEFAULT_METHODS = ["GET", "HEAD", "OPTIONS"];
1229
+ var RETRY_KEYS = /* @__PURE__ */ new Set([
1230
+ "retries",
1231
+ "methods",
1232
+ "statusCodes",
1233
+ "baseDelayMs",
1234
+ "maxDelayMs",
1235
+ "jitter",
1236
+ "respectRetryAfter",
1237
+ "shouldRetry",
1238
+ "onRetry"
1239
+ ]);
1240
+ function resolveRetryConfig(value, label = "retry") {
1241
+ if (value === void 0 || value === false) return void 0;
1242
+ const config = typeof value === "number" ? { retries: value } : value;
1243
+ if (config === null || typeof config !== "object" || Array.isArray(config)) {
1244
+ throw new ConfigError(`${label} must be a number, a retry config object, or false.`);
1245
+ }
1246
+ for (const key of Object.keys(config)) {
1247
+ if (!RETRY_KEYS.has(key)) throw new ConfigError(`${label} has unknown option "${key}".`);
1248
+ }
1249
+ if (typeof config.retries !== "number" || !Number.isInteger(config.retries) || config.retries < 0) {
1250
+ throw new ConfigError(
1251
+ `${label}.retries must be a non-negative integer (got ${String(config.retries)}).`
1252
+ );
1253
+ }
1254
+ for (const key of ["baseDelayMs", "maxDelayMs"]) {
1255
+ const n = config[key];
1256
+ if (n !== void 0 && (typeof n !== "number" || !Number.isFinite(n) || n < 0)) {
1257
+ throw new ConfigError(`${label}.${key} must be a non-negative number.`);
1258
+ }
1259
+ }
1260
+ return {
1261
+ retries: config.retries,
1262
+ methods: config.methods ?? DEFAULT_METHODS,
1263
+ statusCodes: config.statusCodes,
1264
+ baseDelayMs: config.baseDelayMs ?? 200,
1265
+ maxDelayMs: config.maxDelayMs ?? 2e3,
1266
+ jitter: config.jitter ?? true,
1267
+ respectRetryAfter: config.respectRetryAfter ?? true,
1268
+ shouldRetry: config.shouldRetry,
1269
+ onRetry: config.onRetry
1270
+ };
1271
+ }
1272
+ function isRetryableStatus(status, statusCodes) {
1273
+ if (statusCodes) return statusCodes.includes(status);
1274
+ return status === 408 || status === 429 || status >= 500 && status <= 599;
1275
+ }
1276
+ function defaultShouldRetry(error, method, retry, attempt) {
1277
+ if (attempt >= retry.retries || !retry.methods.includes(method)) return false;
1278
+ if (error instanceof NetworkError) return true;
1279
+ if (error instanceof HttpError) return isRetryableStatus(error.status ?? 0, retry.statusCodes);
1280
+ return false;
1281
+ }
1282
+ function decideRetry(retry, ctx, error) {
1283
+ if (ctx.attempt >= retry.retries) return false;
1284
+ if (retry.shouldRetry) return retry.shouldRetry({ ...ctx, error });
1285
+ return defaultShouldRetry(error, ctx.method, retry, ctx.attempt);
1286
+ }
1287
+ function parseRetryAfter(header, now = Date.now()) {
1288
+ if (header === null) return void 0;
1289
+ const trimmed = header.trim();
1290
+ if (/^\d+$/.test(trimmed)) return Number(trimmed) * 1e3;
1291
+ const date = Date.parse(trimmed);
1292
+ if (Number.isNaN(date)) return void 0;
1293
+ return Math.max(0, date - now);
1294
+ }
1295
+ function computeBackoffMs(attempt, retry, random = Math.random) {
1296
+ const capped = Math.min(retry.baseDelayMs * 2 ** attempt, retry.maxDelayMs);
1297
+ if (!retry.jitter) return capped;
1298
+ return Math.floor(capped / 2 + random() * (capped / 2));
1299
+ }
1300
+ function delay(ms, signal) {
1301
+ return new Promise((resolve, reject) => {
1302
+ if (signal.aborted) {
1303
+ reject(signal.reason);
1304
+ return;
1305
+ }
1306
+ const onAbort = () => {
1307
+ clearTimeout(timer);
1308
+ reject(signal.reason);
1309
+ };
1310
+ const timer = setTimeout(() => {
1311
+ signal.removeEventListener("abort", onAbort);
1312
+ resolve();
1313
+ }, ms);
1314
+ signal.addEventListener("abort", onAbort, { once: true });
1315
+ });
1316
+ }
1317
+
1318
+ // packages/core/src/request/pipeline.ts
1319
+ var REQUEST_KEYS = ["method", "url", "headers", "signal", "meta"];
1320
+ function transformQuery(query, to) {
1321
+ if (!query || !to) return query;
1322
+ return transformKeys(query, to);
1323
+ }
1324
+ function createCallSignal(timeout, external) {
1325
+ const controller = new AbortController();
1326
+ let timedOut = false;
1327
+ let timer;
1328
+ const onAbort = () => controller.abort(external.reason);
1329
+ if (external) {
1330
+ if (external.aborted) controller.abort(external.reason);
1331
+ else external.addEventListener("abort", onAbort, { once: true });
1332
+ }
1333
+ if (timeout !== void 0 && timeout > 0 && !controller.signal.aborted) {
1334
+ timer = setTimeout(() => {
1335
+ timedOut = true;
1336
+ controller.abort(new TimeoutError(`Request timed out after ${timeout}ms`));
1337
+ }, timeout);
1338
+ }
1339
+ return {
1340
+ signal: controller.signal,
1341
+ timedOut: () => timedOut,
1342
+ cleanup: () => {
1343
+ if (timer) clearTimeout(timer);
1344
+ external?.removeEventListener("abort", onAbort);
1345
+ }
1346
+ };
1347
+ }
1348
+ function classify(error, signal, timedOut, request) {
1349
+ if (signal.aborted && !(error instanceof HttpError)) {
1350
+ if (timedOut) {
1351
+ return error instanceof TimeoutError ? attach(error, request) : new TimeoutError(void 0, { request, cause: error });
1352
+ }
1353
+ return error instanceof AbortError ? attach(error, request) : new AbortError(void 0, { request, cause: signal.reason ?? error });
1354
+ }
1355
+ if (error instanceof ApiError) return attach(error, request);
1356
+ return error;
1357
+ }
1358
+ function attach(error, request) {
1359
+ error.request ??= request;
1360
+ return error;
1361
+ }
1362
+ function throwIfAborted(signal) {
1363
+ if (signal.aborted) throw signal.reason ?? new AbortError();
1364
+ }
1365
+ function assertRequest(value, where) {
1366
+ if (!value || typeof value !== "object")
1367
+ throw new ConfigError(`${where} must return a request object (or nothing).`);
1368
+ const req = value;
1369
+ for (const key of REQUEST_KEYS) {
1370
+ if (!(key in req)) throw new ConfigError(`${where} returned a request without "${key}".`);
1371
+ }
1372
+ if (!(req.headers instanceof Headers))
1373
+ throw new ConfigError(`${where} returned a request whose headers is not a Headers instance.`);
1374
+ if (typeof req.url !== "string")
1375
+ throw new ConfigError(`${where} returned a request with a non-string url.`);
1376
+ return req;
1377
+ }
1378
+ function assertResponse(value, where) {
1379
+ if (!value || typeof value !== "object" || !("data" in value) || typeof value.status !== "number") {
1380
+ throw new ConfigError(
1381
+ `${where} must return a response object { data, status, headers, request } (or nothing).`
1382
+ );
1383
+ }
1384
+ const res = value;
1385
+ if (!(res.headers instanceof Headers))
1386
+ throw new ConfigError(`${where} returned a response whose headers is not a Headers instance.`);
1387
+ return res;
1388
+ }
1389
+ async function runOperation(client, resource, op, call) {
1390
+ const options = call.options ?? {};
1391
+ const timeout = options.timeout ?? op.timeout;
1392
+ const { signal, timedOut, cleanup } = createCallSignal(timeout, options.signal);
1393
+ const mocked = resource.mock !== void 0;
1394
+ let request;
1395
+ let idempotencyKey = call.idempotencyKey;
1396
+ try {
1397
+ throwIfAborted(signal);
1398
+ const method = call.method ?? op.method;
1399
+ const pathTemplate = call.path ?? op.path;
1400
+ const path = interpolatePath(pathTemplate, call.params);
1401
+ const query = transformQuery(call.query ?? options.query, op.requestKeys);
1402
+ const qs = client.querySerializer ? normalizeQs(client.querySerializer(query ?? {})) : buildQueryString(query, client.queryArrayFormat);
1403
+ const origin = mocked ? client.base.origin : requestOrigin(client.base);
1404
+ const headers = await resolveHeaders(op.headers, options.headers);
1405
+ idempotencyKey = applyIdempotency(op, options, headers, idempotencyKey, resource.name);
1406
+ throwIfAborted(signal);
1407
+ request = {
1408
+ method,
1409
+ url: buildUrl(client.base, path, qs, origin),
1410
+ path,
1411
+ baseURL: client.base.raw,
1412
+ headers,
1413
+ params: call.params,
1414
+ query,
1415
+ body: call.body,
1416
+ signal,
1417
+ timeout,
1418
+ meta: { ...options.meta ?? {} },
1419
+ resource: resource.name,
1420
+ operation: op.name,
1421
+ mocked
1422
+ };
1423
+ for (const hook of op.onRequest) {
1424
+ const next = await hook(request);
1425
+ if (next !== void 0)
1426
+ request = assertRequest(next, `request middleware for ${resource.name}.${op.name}`);
1427
+ throwIfAborted(signal);
1428
+ }
1429
+ const hasBody = call.hasBody ?? op.hasBody;
1430
+ const wireBody = hasBody && op.requestKeys ? transformKeys(request.body, op.requestKeys) : request.body;
1431
+ let status;
1432
+ let responseHeaders;
1433
+ let raw;
1434
+ let data;
1435
+ if (resource.mock) {
1436
+ const { store, source } = resource.mock;
1437
+ const wire = {
1438
+ query: request.query,
1439
+ body: hasBody ? wireBody : void 0
1440
+ };
1441
+ const relative = interpolatePath(call.relativePath ?? op.relativePath, call.params);
1442
+ let result;
1443
+ if (!call.intent && !call.routed) {
1444
+ if (!op.mockHandler) {
1445
+ throw new MockError(
1446
+ "MOCK_UNSUPPORTED",
1447
+ `${source.label}: custom operation "${resource.name}.${op.name}" (${request.method} ${relative || "/"}) has no mock behavior. Add a \`mock\` handler to the operation, or implement it as a function operation ({ kind, run }) that uses ctx.request().`
1448
+ );
1449
+ }
1450
+ result = await runMockHandler(op.mockHandler, store, {
1451
+ resource: resource.name,
1452
+ operation: op.name,
1453
+ method: request.method,
1454
+ params: { ...call.params ?? {} },
1455
+ query: wire.query,
1456
+ body: wire.body,
1457
+ signal
1458
+ });
1459
+ } else {
1460
+ const intent = call.intent ? call.intent(wire) : routeToIntent(request.method, relative, wire.query, wire.body, source.label);
1461
+ result = await runBuiltin(store, source, intent, resource.pagination);
1462
+ }
1463
+ throwIfAborted(signal);
1464
+ status = result.status;
1465
+ responseHeaders = new Headers();
1466
+ data = result.data;
1467
+ } else {
1468
+ assertSameOrigin(request.url, origin);
1469
+ const response2 = await sendWithRetry(
1470
+ client,
1471
+ op,
1472
+ request,
1473
+ hasBody ? wireBody : void 0,
1474
+ options,
1475
+ signal,
1476
+ call.allow404 === true
1477
+ );
1478
+ status = response2.status;
1479
+ responseHeaders = response2.headers;
1480
+ raw = response2.raw;
1481
+ data = response2.body;
1482
+ if (op.responseKeys) data = transformKeys(data, op.responseKeys);
1483
+ if (call.fromHttp) data = call.fromHttp(data, status);
1484
+ }
1485
+ if (resource.mock && op.responseKeys) data = transformKeys(data, op.responseKeys);
1486
+ if (call.shape) data = call.shape(data);
1487
+ let response = { data, status, headers: responseHeaders, request, raw };
1488
+ for (const hook of op.onResponse) {
1489
+ const next = await hook(response);
1490
+ if (next !== void 0)
1491
+ response = assertResponse(next, `response middleware for ${resource.name}.${op.name}`);
1492
+ }
1493
+ return options.full ? response : response.data;
1494
+ } catch (thrown) {
1495
+ let error = classify(thrown, signal, timedOut(), request);
1496
+ const replay = call.replayed ? void 0 : () => runOperation(client, resource, op, { ...call, replayed: true, idempotencyKey });
1497
+ for (const hook of op.onError) {
1498
+ try {
1499
+ const recovered = await hook(error, { request, replay });
1500
+ if (recovered !== void 0) return recovered;
1501
+ } catch (next) {
1502
+ error = next;
1503
+ }
1504
+ }
1505
+ throw error;
1506
+ } finally {
1507
+ cleanup();
1508
+ }
1509
+ }
1510
+ function applyIdempotency(op, options, headers, current, resourceName) {
1511
+ if (!op.idempotency) {
1512
+ if (options.idempotencyKey !== void 0) {
1513
+ throw new ConfigError(
1514
+ `resources.${resourceName}.operations.${op.name}: idempotencyKey was passed, but the operation has no idempotency option.`
1515
+ );
1516
+ }
1517
+ return void 0;
1518
+ }
1519
+ const { header, generate } = op.idempotency;
1520
+ if (options.idempotencyKey !== void 0) {
1521
+ if (typeof options.idempotencyKey !== "string" || options.idempotencyKey === "") {
1522
+ throw new ConfigError("options.idempotencyKey must be a non-empty string.");
1523
+ }
1524
+ headers.set(header, options.idempotencyKey);
1525
+ return options.idempotencyKey;
1526
+ }
1527
+ if (current !== void 0) {
1528
+ headers.set(header, current);
1529
+ return current;
1530
+ }
1531
+ const existing = headers.get(header);
1532
+ if (existing !== null) return existing;
1533
+ const key = generate();
1534
+ if (typeof key !== "string" || key === "") {
1535
+ throw new ConfigError(
1536
+ `resources.${resourceName}.operations.${op.name}.idempotency.generate must return a non-empty string.`
1537
+ );
1538
+ }
1539
+ headers.set(header, key);
1540
+ return key;
1541
+ }
1542
+ function normalizeQs(value) {
1543
+ if (!value) return "";
1544
+ return value.startsWith("?") ? value : `?${value}`;
1545
+ }
1546
+ function isReplayable(body) {
1547
+ return !(typeof ReadableStream !== "undefined" && body instanceof ReadableStream);
1548
+ }
1549
+ async function sendWithRetry(client, op, request, wireBody, options, signal, allow404) {
1550
+ const retry = options.retry !== void 0 ? resolveRetryConfig(options.retry, "options.retry") : op.retry;
1551
+ const headers = new Headers(request.headers);
1552
+ const body = serializeBody(wireBody, headers);
1553
+ const replayable = isReplayable(body);
1554
+ for (let attempt = 0; ; attempt++) {
1555
+ throwIfAborted(signal);
1556
+ let failure;
1557
+ let failedResponse;
1558
+ try {
1559
+ const response = await client.transport.request({
1560
+ url: request.url,
1561
+ method: request.method,
1562
+ headers,
1563
+ body,
1564
+ signal,
1565
+ responseType: options.responseType
1566
+ });
1567
+ if (response.status >= 200 && response.status < 300 || allow404 && response.status === 404) {
1568
+ return response;
1569
+ }
1570
+ failedResponse = {
1571
+ data: response.body,
1572
+ status: response.status,
1573
+ headers: response.headers,
1574
+ request,
1575
+ raw: response.raw
1576
+ };
1577
+ failure = new HttpError(`Request failed with status ${response.status}`, {
1578
+ status: response.status,
1579
+ data: response.body,
1580
+ request,
1581
+ response: failedResponse,
1582
+ attempt
1583
+ });
1584
+ } catch (error) {
1585
+ if (signal.aborted) throw error;
1586
+ failure = error instanceof ApiError ? error : new NetworkError(error instanceof Error ? error.message : "Network request failed", {
1587
+ cause: error
1588
+ });
1589
+ failure.request ??= request;
1590
+ failure.attempt = attempt;
1591
+ }
1592
+ const ctx = {
1593
+ request,
1594
+ response: failedResponse,
1595
+ attempt,
1596
+ method: request.method,
1597
+ error: failure
1598
+ };
1599
+ const retryAfter = retry?.respectRetryAfter && failedResponse ? parseRetryAfter(failedResponse.headers.get("retry-after")) : void 0;
1600
+ const canRetry = retry !== void 0 && replayable && !signal.aborted && decideRetry(retry, ctx, failure) && (retryAfter === void 0 || retryAfter <= retry.maxDelayMs);
1601
+ failure.retryable = canRetry;
1602
+ if (!canRetry) throw failure;
1603
+ await retry.onRetry?.(ctx);
1604
+ await delay(retryAfter ?? computeBackoffMs(attempt, retry), signal);
1605
+ }
1606
+ }
1607
+
1608
+ // packages/core/src/utils/slice.ts
1609
+ function isPlainSliceRange(value) {
1610
+ return value !== null && typeof value === "object" && !Array.isArray(value) && Object.keys(value).every((k) => k === "start" || k === "end");
1611
+ }
1612
+ function assertResourceSlice(slice, label = "slice") {
1613
+ if (typeof slice === "number") {
1614
+ if (!Number.isInteger(slice) || slice < 0) {
1615
+ throw new Error(`${label}: must be a non-negative integer (got ${String(slice)}).`);
1616
+ }
1617
+ return;
1618
+ }
1619
+ if (!isPlainSliceRange(slice)) {
1620
+ throw new Error(`${label}: must be a non-negative integer or { start, end }.`);
1621
+ }
1622
+ const { start, end } = slice;
1623
+ if (typeof start !== "number" || !Number.isInteger(start) || start < 0) {
1624
+ throw new Error(`${label}.start must be a non-negative integer (got ${String(start)}).`);
1625
+ }
1626
+ if (typeof end !== "number" || !Number.isInteger(end) || end < 0) {
1627
+ throw new Error(`${label}.end must be a non-negative integer (got ${String(end)}).`);
1628
+ }
1629
+ if (end < start) {
1630
+ throw new Error(`${label}.end (${end}) must not be before ${label}.start (${start}).`);
1631
+ }
1632
+ }
1633
+ function applyResourceSlice(records, slice, label = "slice") {
1634
+ if (slice === void 0) return records;
1635
+ assertResourceSlice(slice, label);
1636
+ if (typeof slice === "number") {
1637
+ if (slice >= records.length) return records.slice();
1638
+ return records.slice(0, slice);
1639
+ }
1640
+ if (slice.end > records.length) return records.slice();
1641
+ return records.slice(slice.start, slice.end);
1642
+ }
1643
+ function applyResourceSliceToListResult(data, slice) {
1644
+ if (slice === void 0 || !Array.isArray(data)) return data;
1645
+ return applyResourceSlice(data, slice);
1646
+ }
1647
+
1648
+ // packages/core/src/api/validate.ts
1649
+ var ROOT_KEYS = /* @__PURE__ */ new Set([
1650
+ "baseURL",
1651
+ "headers",
1652
+ "transport",
1653
+ "timeout",
1654
+ "retry",
1655
+ "middleware",
1656
+ "requestMiddleware",
1657
+ "responseMiddleware",
1658
+ "transforms",
1659
+ "queryArrayFormat",
1660
+ "querySerializer",
1661
+ "resources"
1662
+ ]);
1663
+ var RESOURCE_KEYS = /* @__PURE__ */ new Set([
1664
+ "path",
1665
+ "headers",
1666
+ "timeout",
1667
+ "retry",
1668
+ "middleware",
1669
+ "requestMiddleware",
1670
+ "responseMiddleware",
1671
+ "transforms",
1672
+ "defaultOperations",
1673
+ "readOnly",
1674
+ "writeOnly",
1675
+ "operationGroups",
1676
+ "operations",
1677
+ "mock",
1678
+ "pagination",
1679
+ "slice"
1680
+ ]);
1681
+ var OPERATION_KEYS = /* @__PURE__ */ new Set([
1682
+ "kind",
1683
+ "idempotency",
1684
+ "method",
1685
+ "path",
1686
+ "hasBody",
1687
+ "headers",
1688
+ "timeout",
1689
+ "retry",
1690
+ "middleware",
1691
+ "requestMiddleware",
1692
+ "responseMiddleware",
1693
+ "transforms",
1694
+ "mock"
1695
+ ]);
1696
+ var RUN_KEYS = /* @__PURE__ */ new Set(["kind", "run"]);
1697
+ var METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
1698
+ function requiredKind(method) {
1699
+ return method === "GET" || method === "HEAD" || method === "OPTIONS" ? "read" : "write";
1700
+ }
1701
+ function validateMethodKind(where, kind, method) {
1702
+ if (!METHODS.has(method)) {
1703
+ throw new ConfigError(
1704
+ `${where}: HTTP method "${String(method)}" is invalid. Use GET, HEAD, OPTIONS, POST, PUT, PATCH, or DELETE.`
1705
+ );
1706
+ }
1707
+ const required = requiredKind(method);
1708
+ if (kind !== required) {
1709
+ throw new ConfigError(
1710
+ `${where}: Operation kind "${kind}" is incompatible with HTTP method "${method}". ${method} requires kind "${required}". Change the method or kind.`
1711
+ );
1712
+ }
1713
+ }
1714
+ var GROUPS = /* @__PURE__ */ new Set(["read", "write", "many", "extended"]);
1715
+ var HOOK_KEYS = /* @__PURE__ */ new Set(["onRequest", "onResponse", "onError"]);
1716
+ function isPlainObject3(value) {
1717
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
1718
+ const proto = Object.getPrototypeOf(value);
1719
+ return proto === Object.prototype || proto === null;
1720
+ }
1721
+ function typeName(value) {
1722
+ return Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
1723
+ }
1724
+ function unknownKey(where, key, allowed) {
1725
+ throw new ConfigError(
1726
+ `${where} has unknown option "${key}". Supported: ${[...allowed].join(", ")}.`
1727
+ );
1728
+ }
1729
+ function assertKnownKeys(where, value, allowed) {
1730
+ for (const key of Object.keys(value)) if (!allowed.has(key)) unknownKey(where, key, allowed);
1731
+ }
1732
+ function validateRoot(config) {
1733
+ if (!isPlainObject3(config)) {
1734
+ throw new ConfigError(`defineApi() expects a config object (got ${typeName(config)}).`);
1735
+ }
1736
+ for (const key of Object.keys(config)) {
1737
+ if (ROOT_KEYS.has(key)) continue;
1738
+ const value = config[key];
1739
+ if (isPlainObject3(value) && "path" in value) {
1740
+ throw new ConfigError(
1741
+ `"${key}" looks like a resource at the root of the config. Resources must be nested: defineApi({ baseURL, resources: { ${key}: { path: ${JSON.stringify(value.path)} } } }).`
1742
+ );
1743
+ }
1744
+ unknownKey("defineApi config", key, ROOT_KEYS);
1745
+ }
1746
+ if (!("resources" in config)) {
1747
+ throw new ConfigError(
1748
+ `defineApi config needs a "resources" object, e.g. resources: { users: { path: "/users" } }.`
1749
+ );
1750
+ }
1751
+ if (!isPlainObject3(config.resources)) {
1752
+ throw new ConfigError(
1753
+ `"resources" must be an object of resource definitions (got ${typeName(config.resources)}).`
1754
+ );
1755
+ }
1756
+ if (config.transport !== void 0) {
1757
+ const transport = config.transport;
1758
+ if (!transport || typeof transport !== "object" || typeof transport.request !== "function") {
1759
+ throw new ConfigError(`transport must be an object with a request(input) function.`);
1760
+ }
1761
+ }
1762
+ if (config.queryArrayFormat !== void 0 && !["repeat", "comma", "brackets"].includes(config.queryArrayFormat)) {
1763
+ throw new ConfigError(
1764
+ `queryArrayFormat must be "repeat", "comma", or "brackets" (got ${String(config.queryArrayFormat)}).`
1765
+ );
1766
+ }
1767
+ if (config.querySerializer !== void 0 && typeof config.querySerializer !== "function") {
1768
+ throw new ConfigError(`querySerializer must be a function.`);
1769
+ }
1770
+ validateLayer("defineApi config", config);
1771
+ }
1772
+ function validateLayer(where, layer) {
1773
+ const { headers, timeout, retry, middleware, requestMiddleware, responseMiddleware, transforms } = layer;
1774
+ resolveRetryConfig(retry, `${where}.retry`);
1775
+ if (headers !== void 0 && typeof headers !== "function" && typeof headers !== "object") {
1776
+ throw new ConfigError(
1777
+ `${where}.headers must be an object, Headers, entries array, or a (sync/async) function.`
1778
+ );
1779
+ }
1780
+ if (headers === null) throw new ConfigError(`${where}.headers must not be null.`);
1781
+ if (timeout !== void 0 && (typeof timeout !== "number" || !Number.isFinite(timeout) || timeout < 0)) {
1782
+ throw new ConfigError(
1783
+ `${where}.timeout must be a non-negative number of milliseconds (got ${String(timeout)}).`
1784
+ );
1785
+ }
1786
+ if (middleware !== void 0) validateHooks(`${where}.middleware`, middleware);
1787
+ if (requestMiddleware !== void 0)
1788
+ validateFnArray(`${where}.requestMiddleware`, requestMiddleware);
1789
+ if (responseMiddleware !== void 0)
1790
+ validateFnArray(`${where}.responseMiddleware`, responseMiddleware);
1791
+ if (transforms !== void 0) validateTransforms(`${where}.transforms`, transforms);
1792
+ }
1793
+ function validateHooks(where, value) {
1794
+ if (Array.isArray(value)) {
1795
+ throw new ConfigError(
1796
+ `${where} must be an object { onRequest?, onResponse?, onError? }. Around-style middleware arrays were removed; use requestMiddleware / responseMiddleware arrays instead.`
1797
+ );
1798
+ }
1799
+ if (!isPlainObject3(value))
1800
+ throw new ConfigError(`${where} must be an object { onRequest?, onResponse?, onError? }.`);
1801
+ for (const [key, hook] of Object.entries(value)) {
1802
+ if (!HOOK_KEYS.has(key)) unknownKey(where, key, HOOK_KEYS);
1803
+ if (hook !== void 0 && typeof hook !== "function")
1804
+ throw new ConfigError(`${where}.${key} must be a function.`);
1805
+ }
1806
+ }
1807
+ function validateFnArray(where, value) {
1808
+ if (!Array.isArray(value)) throw new ConfigError(`${where} must be an array of functions.`);
1809
+ value.forEach((fn, i) => {
1810
+ if (typeof fn !== "function")
1811
+ throw new ConfigError(`${where}[${i}] must be a function (got ${typeName(fn)}).`);
1812
+ });
1813
+ }
1814
+ function validateTransforms(where, value) {
1815
+ if (!isPlainObject3(value))
1816
+ throw new ConfigError(`${where} must be { request?: { keys }, response?: { keys } }.`);
1817
+ for (const [direction, rule] of Object.entries(value)) {
1818
+ if (direction !== "request" && direction !== "response")
1819
+ unknownKey(where, direction, /* @__PURE__ */ new Set(["request", "response"]));
1820
+ if (rule === void 0) continue;
1821
+ if (!isPlainObject3(rule)) throw new ConfigError(`${where}.${direction} must be { keys }.`);
1822
+ assertKnownKeys(`${where}.${direction}`, rule, /* @__PURE__ */ new Set(["keys"]));
1823
+ if (!isKeyCase(rule.keys)) {
1824
+ throw new ConfigError(
1825
+ `${where}.${direction}.keys must be one of ${KEY_CASES.join(", ")} (got ${JSON.stringify(rule.keys)}).`
1826
+ );
1827
+ }
1828
+ }
1829
+ }
1830
+ function validateResource(name, value) {
1831
+ const where = `resources.${name}`;
1832
+ if (!isPlainObject3(value))
1833
+ throw new ConfigError(`${where} must be a resource object like { path: "/${name}" }.`);
1834
+ assertKnownKeys(where, value, RESOURCE_KEYS);
1835
+ if (typeof value.path !== "string" || !value.path.startsWith("/")) {
1836
+ throw new ConfigError(`${where}.path must be a string starting with "/" (e.g. "/${name}").`);
1837
+ }
1838
+ for (const flag of ["defaultOperations", "readOnly", "writeOnly"]) {
1839
+ if (value[flag] !== void 0 && typeof value[flag] !== "boolean") {
1840
+ throw new ConfigError(`${where}.${flag} must be a boolean.`);
1841
+ }
1842
+ }
1843
+ if (value.readOnly === true && value.writeOnly === true) {
1844
+ throw new ConfigError(`${where} cannot set both readOnly and writeOnly to true.`);
1845
+ }
1846
+ if (value.operationGroups !== void 0) {
1847
+ if (!isPlainObject3(value.operationGroups))
1848
+ throw new ConfigError(`${where}.operationGroups must be an object.`);
1849
+ for (const [group, choice] of Object.entries(value.operationGroups)) {
1850
+ if (!GROUPS.has(group)) unknownKey(`${where}.operationGroups`, group, GROUPS);
1851
+ if (choice !== void 0 && typeof choice !== "boolean") {
1852
+ throw new ConfigError(`${where}.operationGroups.${group} must be a boolean.`);
1853
+ }
1854
+ }
1855
+ }
1856
+ if (value.pagination !== void 0 && value.pagination !== "page" && value.pagination !== "cursor") {
1857
+ throw new ConfigError(
1858
+ `${where}.pagination must be "page" or "cursor" (got ${JSON.stringify(value.pagination)}).` + (typeof value.pagination === "boolean" ? ` The mock pagination boolean was replaced by pagination: "page".` : "")
1859
+ );
1860
+ }
1861
+ if (value.slice !== void 0) {
1862
+ try {
1863
+ assertResourceSlice(value.slice, `${where}.slice`);
1864
+ } catch (cause) {
1865
+ throw new ConfigError(cause.message, { cause });
1866
+ }
1867
+ }
1868
+ validateLayer(where, value);
1869
+ if (value.operations !== void 0) {
1870
+ if (!isPlainObject3(value.operations))
1871
+ throw new ConfigError(`${where}.operations must be an object.`);
1872
+ for (const [op, definition] of Object.entries(value.operations))
1873
+ validateOperation(`${where}.operations.${op}`, op, definition);
1874
+ }
1875
+ }
1876
+ function validateOperation(where, name, value) {
1877
+ const builtin = isBuiltinName(name);
1878
+ if (value === void 0 || value === false) return;
1879
+ if (typeof value === "function") {
1880
+ if (!builtin)
1881
+ throw new ConfigError(
1882
+ `${where}: custom function operation requires { kind: "read" | "write", run }.`
1883
+ );
1884
+ return;
1885
+ }
1886
+ if (value === true) {
1887
+ if (!builtin) {
1888
+ throw new ConfigError(
1889
+ `${where}: true cannot create custom operation "${name}"; give it { method, path } or a function.`
1890
+ );
1891
+ }
1892
+ return;
1893
+ }
1894
+ if (!isPlainObject3(value)) {
1895
+ throw new ConfigError(
1896
+ `${where} must be true, false, a config object, or a function (got ${typeName(value)}).`
1897
+ );
1898
+ }
1899
+ if (builtin && "kind" in value) {
1900
+ throw new ConfigError(
1901
+ `${where}.kind: builtin "${name}" is always "${BUILTIN_CONVENTIONS[name].access}"; kind is for custom operations.`
1902
+ );
1903
+ }
1904
+ if (!builtin) {
1905
+ if (value.kind !== "read" && value.kind !== "write")
1906
+ throw new ConfigError(
1907
+ `${where}.kind must be explicitly set to "read" or "write" (got ${JSON.stringify(value.kind)}).`
1908
+ );
1909
+ }
1910
+ if ("run" in value) {
1911
+ assertKnownKeys(where, value, RUN_KEYS);
1912
+ if (typeof value.run !== "function")
1913
+ throw new ConfigError(`${where}.run must be a function (got ${typeName(value.run)}).`);
1914
+ return;
1915
+ }
1916
+ assertKnownKeys(where, value, OPERATION_KEYS);
1917
+ if (value.mock !== void 0) {
1918
+ if (builtin) {
1919
+ throw new ConfigError(
1920
+ `${where}.mock: builtin "${name}" is simulated by Core; \`mock\` is for custom operations.`
1921
+ );
1922
+ }
1923
+ if (typeof value.mock !== "function") {
1924
+ throw new ConfigError(
1925
+ `${where}.mock must be a function ({ records, store, params, query, body, signal }) => result (got ${typeName(value.mock)}).`
1926
+ );
1927
+ }
1928
+ }
1929
+ if (value.method !== void 0 && !METHODS.has(value.method)) {
1930
+ throw new ConfigError(
1931
+ `${where}.method must be one of ${[...METHODS].join(", ")} (got ${JSON.stringify(value.method)}).`
1932
+ );
1933
+ }
1934
+ if (!builtin && value.method === void 0) {
1935
+ throw new ConfigError(`${where}: custom operation "${name}" needs a method.`);
1936
+ }
1937
+ const method = value.method ?? (builtin ? BUILTIN_CONVENTIONS[name].method : void 0);
1938
+ validateMethodKind(
1939
+ where,
1940
+ builtin ? BUILTIN_CONVENTIONS[name].access : value.kind,
1941
+ method
1942
+ );
1943
+ if (value.path !== void 0 && typeof value.path !== "string")
1944
+ throw new ConfigError(`${where}.path must be a string.`);
1945
+ if (value.hasBody !== void 0 && typeof value.hasBody !== "boolean")
1946
+ throw new ConfigError(`${where}.hasBody must be a boolean.`);
1947
+ validateIdempotency(
1948
+ where,
1949
+ value.idempotency,
1950
+ builtin ? BUILTIN_CONVENTIONS[name].access : value.kind
1951
+ );
1952
+ validateLayer(where, value);
1953
+ }
1954
+ var IDEMPOTENCY_KEYS = /* @__PURE__ */ new Set(["header", "generate"]);
1955
+ function validateIdempotency(where, value, kind) {
1956
+ if (value === void 0 || value === false) return;
1957
+ if (kind !== "write") {
1958
+ throw new ConfigError(
1959
+ `${where}.idempotency: only write operations send an idempotency key (this operation is a "${kind}").`
1960
+ );
1961
+ }
1962
+ if (value === true) return;
1963
+ if (!isPlainObject3(value)) {
1964
+ throw new ConfigError(
1965
+ `${where}.idempotency must be true, false, or { header?, generate? } (got ${typeName(value)}).`
1966
+ );
1967
+ }
1968
+ assertKnownKeys(`${where}.idempotency`, value, IDEMPOTENCY_KEYS);
1969
+ if (value.header !== void 0 && (typeof value.header !== "string" || !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(value.header))) {
1970
+ throw new ConfigError(`${where}.idempotency.header must be a valid HTTP header name.`);
1971
+ }
1972
+ if (value.generate !== void 0 && typeof value.generate !== "function") {
1973
+ throw new ConfigError(`${where}.idempotency.generate must be a function.`);
1974
+ }
1975
+ }
1976
+
1977
+ // packages/core/src/api/metadata.ts
1978
+ var OPERATION_METADATA = /* @__PURE__ */ Symbol.for("apixa.operations");
1979
+ function getOperationMetadata(resource) {
1980
+ const value = resource[OPERATION_METADATA];
1981
+ return value && typeof value === "object" ? value : void 0;
1982
+ }
1983
+ var IDEMPOTENCY_GENERATORS = /* @__PURE__ */ new WeakMap();
1984
+ function registerIdempotencyGenerators(resource, generators) {
1985
+ IDEMPOTENCY_GENERATORS.set(resource, generators);
1986
+ }
1987
+ function createIdempotencyKey(resource, operation) {
1988
+ return IDEMPOTENCY_GENERATORS.get(resource)?.[operation]?.();
1989
+ }
1990
+
1991
+ // packages/core/src/api/resource-client.ts
1992
+ var BODY_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
1993
+ function pick(...values) {
1994
+ for (let i = values.length - 1; i >= 0; i--) if (values[i] !== void 0) return values[i];
1995
+ return void 0;
1996
+ }
1997
+ function prepareLayers(where, layers) {
1998
+ const present = layers.filter((l) => l !== void 0);
1999
+ const retry = pick(...present.map((l) => l.retry));
2000
+ return {
2001
+ headers: present.map((l) => l.headers).filter((h) => h !== void 0),
2002
+ timeout: pick(...present.map((l) => l.timeout)),
2003
+ retry: resolveRetryConfig(retry, `${where}.retry`),
2004
+ onRequest: present.flatMap((l) => [
2005
+ ...l.middleware?.onRequest ? [l.middleware.onRequest] : [],
2006
+ ...l.requestMiddleware ?? []
2007
+ ]),
2008
+ onResponse: present.flatMap((l) => [
2009
+ ...l.middleware?.onResponse ? [l.middleware.onResponse] : [],
2010
+ ...l.responseMiddleware ?? []
2011
+ ]),
2012
+ onError: present.flatMap((l) => l.middleware?.onError ? [l.middleware.onError] : []),
2013
+ requestKeys: pick(...present.map((l) => l.transforms?.request?.keys)),
2014
+ responseKeys: pick(...present.map((l) => l.transforms?.response?.keys))
2015
+ };
2016
+ }
2017
+ function isRecord(value) {
2018
+ return value !== null && typeof value === "object" && !Array.isArray(value);
2019
+ }
2020
+ function toArray(result) {
2021
+ if (Array.isArray(result)) return result;
2022
+ if (result && typeof result === "object" && Array.isArray(result.data)) {
2023
+ return result.data;
2024
+ }
2025
+ return [];
2026
+ }
2027
+ function splitTrailing(args, hasResourceParams, fixed) {
2028
+ const params = hasResourceParams ? args[fixed] : void 0;
2029
+ const options = args[hasResourceParams ? fixed + 1 : fixed];
2030
+ return { params, options };
2031
+ }
2032
+ function createResourceClient(client, input) {
2033
+ const { name, definition: def, root } = input;
2034
+ const resourcePath = def.path;
2035
+ const resourceLayer = def;
2036
+ const runtime = {
2037
+ name,
2038
+ path: resourcePath,
2039
+ pagination: def.pagination
2040
+ };
2041
+ if (def.mock !== void 0) {
2042
+ const source = normalizeMockSource(name, def.mock);
2043
+ runtime.mock = { source, store: new MockStore(source) };
2044
+ }
2045
+ const slice = def.slice;
2046
+ const resourceParamNames = extractPathParamNames(resourcePath);
2047
+ const hasResourceParams = resourceParamNames.length > 0;
2048
+ const operations = expandOperations(
2049
+ def.operations,
2050
+ {
2051
+ defaultOperations: def.defaultOperations,
2052
+ readOnly: def.readOnly,
2053
+ writeOnly: def.writeOnly,
2054
+ operationGroups: def.operationGroups
2055
+ }
2056
+ );
2057
+ const out = {};
2058
+ const metadata = {};
2059
+ const generators = {};
2060
+ for (const [opName, definition] of Object.entries(operations)) {
2061
+ const where = `resources.${name}.operations.${opName}`;
2062
+ const convention = isBuiltinName(opName) ? BUILTIN_CONVENTIONS[opName] : void 0;
2063
+ const fn = typeof definition === "function" ? definition : isRunDefinition(definition) ? definition.run : void 0;
2064
+ if (fn) {
2065
+ out[opName] = functionOperation(
2066
+ client,
2067
+ runtime,
2068
+ opName,
2069
+ fn,
2070
+ convention?.access ?? definition.kind,
2071
+ prepareLayers(where, [root, resourceLayer])
2072
+ );
2073
+ metadata[opName] = Object.freeze({
2074
+ kind: convention?.access ?? definition.kind,
2075
+ builtin: convention !== void 0,
2076
+ implementation: "function",
2077
+ optionsIndex: null,
2078
+ idempotency: false
2079
+ });
2080
+ continue;
2081
+ }
2082
+ const declared = definition;
2083
+ const method = declared.method ?? convention.method;
2084
+ const relativePath = declared.path ?? convention?.path ?? "";
2085
+ const hasBody = declared.hasBody ?? (convention && "hasBody" in convention ? convention.hasBody : void 0) ?? (convention && "strategy" in convention ? false : BODY_METHODS.has(method));
2086
+ const op = {
2087
+ name: opName,
2088
+ method,
2089
+ path: joinPath(resourcePath, relativePath),
2090
+ relativePath,
2091
+ hasBody,
2092
+ ...prepareLayers(where, [root, resourceLayer, declared])
2093
+ };
2094
+ if (declared.mock !== void 0) {
2095
+ op.mockHandler = declared.mock;
2096
+ }
2097
+ if (declared.idempotency !== void 0 && declared.idempotency !== false) {
2098
+ const config = declared.idempotency === true ? {} : declared.idempotency;
2099
+ op.idempotency = {
2100
+ header: config.header ?? "Idempotency-Key",
2101
+ generate: config.generate ?? defaultIdempotencyKey
2102
+ };
2103
+ generators[opName] = op.idempotency.generate;
2104
+ }
2105
+ if (convention && "strategy" in convention && hasBody) {
2106
+ throw new ConfigError(
2107
+ `${where}: hasBody is unsupported on ${opName} (it has no body mapping).`
2108
+ );
2109
+ }
2110
+ metadata[opName] = Object.freeze({
2111
+ kind: convention?.access ?? declared.kind,
2112
+ builtin: convention !== void 0,
2113
+ implementation: "request",
2114
+ optionsIndex: optionsIndexOf(opName, op, hasResourceParams),
2115
+ idempotency: op.idempotency !== void 0
2116
+ });
2117
+ const run = (call) => runOperation(client, runtime, op, call);
2118
+ out[opName] = convention ? builtinOperation(opName, op, run, {
2119
+ resourceParamNames,
2120
+ hasResourceParams,
2121
+ slice
2122
+ }) : declarativeOperation(op, run);
2123
+ }
2124
+ Object.defineProperty(out, OPERATION_METADATA, { value: Object.freeze(metadata) });
2125
+ if (Object.keys(generators).length > 0)
2126
+ registerIdempotencyGenerators(out, Object.freeze(generators));
2127
+ return { client: out, store: runtime.mock?.store };
2128
+ }
2129
+ function defaultIdempotencyKey() {
2130
+ const webCrypto = globalThis.crypto;
2131
+ if (typeof webCrypto?.randomUUID === "function") return webCrypto.randomUUID();
2132
+ if (typeof webCrypto?.getRandomValues !== "function") {
2133
+ throw new ConfigError("idempotency: no Web Crypto in this runtime; pass idempotency.generate.");
2134
+ }
2135
+ const bytes = webCrypto.getRandomValues(new Uint8Array(16));
2136
+ bytes[6] = bytes[6] & 15 | 64;
2137
+ bytes[8] = bytes[8] & 63 | 128;
2138
+ const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
2139
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
2140
+ }
2141
+ function isRunDefinition(value) {
2142
+ return isRecord(value) && typeof value.run === "function";
2143
+ }
2144
+ function optionsIndexOf(name, op, hasResourceParams) {
2145
+ const params = hasResourceParams ? 1 : 0;
2146
+ if (name === "exists" || name === "getMany" || name === "findFirst") return 1 + params;
2147
+ if (name === "getBy") return 2 + params;
2148
+ return (extractPathParamNames(op.path).length > 0 ? 1 : 0) + (op.hasBody ? 1 : 0);
2149
+ }
2150
+ function declarativeOperation(op, run) {
2151
+ const paramNames = extractPathParamNames(op.path);
2152
+ return (...args) => {
2153
+ const parsed = parseOperationArgs(paramNames, op.hasBody, args);
2154
+ return run({ params: parsed.params, body: parsed.body, options: parsed.options });
2155
+ };
2156
+ }
2157
+ function builtinOperation(name, op, run, ctx) {
2158
+ const paramNames = extractPathParamNames(op.path);
2159
+ const idParam = paramNames.find((p) => !ctx.resourceParamNames.includes(p));
2160
+ const keyName = (field) => op.requestKeys ? convertKey(field, op.requestKeys) : field;
2161
+ switch (name) {
2162
+ case "exists":
2163
+ return (id, ...rest) => {
2164
+ const { params, options } = splitTrailing(rest, ctx.hasResourceParams, 0);
2165
+ return run({
2166
+ params: { ...params, ...idParam ? { [idParam]: id } : {} },
2167
+ query: idParam ? options?.query : { ...options?.query ?? {}, id },
2168
+ options,
2169
+ allow404: true,
2170
+ fromHttp: (_data, status) => status !== 404,
2171
+ intent: () => ({ op: "exists", id })
2172
+ });
2173
+ };
2174
+ case "getMany":
2175
+ return (ids, ...rest) => {
2176
+ const { params, options } = splitTrailing(rest, ctx.hasResourceParams, 0);
2177
+ return run({
2178
+ params,
2179
+ query: { ...options?.query ?? {}, id: ids },
2180
+ options,
2181
+ fromHttp: toArray,
2182
+ intent: () => ({ op: "getMany", ids })
2183
+ });
2184
+ };
2185
+ case "findFirst":
2186
+ return (query, ...rest) => {
2187
+ const { params, options } = splitTrailing(rest, ctx.hasResourceParams, 0);
2188
+ return run({
2189
+ params,
2190
+ query,
2191
+ options,
2192
+ fromHttp: (data) => toArray(data)[0] ?? null,
2193
+ intent: (wire) => ({ op: "findFirst", query: wire.query })
2194
+ });
2195
+ };
2196
+ case "getBy":
2197
+ return (fieldOrPath, value, ...rest) => {
2198
+ const { params, options } = splitTrailing(rest, ctx.hasResourceParams, 0);
2199
+ const field = String(fieldOrPath);
2200
+ const encoded = encodeURIComponent(String(value));
2201
+ let path;
2202
+ if (/:field\b|:value\b/.test(op.path)) {
2203
+ path = op.path.replace(/:field\b/g, encodeURIComponent(field)).replace(/:value\b/g, encoded);
2204
+ } else {
2205
+ const dynamic = /:[A-Za-z0-9_]+/.test(field) ? field.replace(/:[A-Za-z0-9_]+/, encoded) : joinPath(field, encoded);
2206
+ path = joinPath(op.path, dynamic);
2207
+ }
2208
+ return run({
2209
+ params,
2210
+ path,
2211
+ options,
2212
+ intent: () => ({ op: "getBy", field: keyName(field), value })
2213
+ });
2214
+ };
2215
+ default:
2216
+ break;
2217
+ }
2218
+ return (...args) => {
2219
+ const parsed = parseOperationArgs(paramNames, op.hasBody, args);
2220
+ const params = parsed.params;
2221
+ const id = idParam ? params?.[idParam] : void 0;
2222
+ const call = { params, body: parsed.body, options: parsed.options };
2223
+ call.intent = (wire) => {
2224
+ switch (name) {
2225
+ case "getAll":
2226
+ case "count":
2227
+ case "search":
2228
+ return { op: name, query: wire.query ?? (isRecord(wire.body) ? wire.body : void 0) };
2229
+ case "getBySlug":
2230
+ return { op: name, slug: id };
2231
+ case "getById":
2232
+ case "delete":
2233
+ return { op: name, id };
2234
+ case "update":
2235
+ case "patch":
2236
+ case "upsert":
2237
+ return { op: name, id, body: wire.body };
2238
+ default:
2239
+ return { op: name, body: wire.body };
2240
+ }
2241
+ };
2242
+ if (name === "getAll" && ctx.slice !== void 0) {
2243
+ const slice = ctx.slice;
2244
+ call.shape = (data) => applyResourceSliceToListResult(data, slice);
2245
+ }
2246
+ return run(call);
2247
+ };
2248
+ }
2249
+ function functionOperation(client, runtime, opName, fn, kind, layers) {
2250
+ const base = {
2251
+ name: opName,
2252
+ method: "GET",
2253
+ path: runtime.path,
2254
+ relativePath: "",
2255
+ hasBody: false,
2256
+ ...layers
2257
+ };
2258
+ const ctx = {
2259
+ resource: runtime.name,
2260
+ operation: opName,
2261
+ request: (init = {}) => {
2262
+ const method = init.method ?? "GET";
2263
+ validateMethodKind(`resources.${runtime.name}.operations.${opName}`, kind, method);
2264
+ const options = {
2265
+ headers: init.headers,
2266
+ signal: init.signal,
2267
+ timeout: init.timeout,
2268
+ retry: init.retry,
2269
+ responseType: init.responseType,
2270
+ meta: init.meta,
2271
+ full: init.full
2272
+ };
2273
+ return runOperation(client, runtime, base, {
2274
+ method,
2275
+ path: joinPath(runtime.path, init.path ?? ""),
2276
+ relativePath: init.path ?? "",
2277
+ params: init.params,
2278
+ query: init.query,
2279
+ body: init.body,
2280
+ hasBody: init.body !== void 0,
2281
+ routed: true,
2282
+ options
2283
+ });
2284
+ }
2285
+ };
2286
+ return (...args) => Promise.resolve().then(() => fn(ctx, ...args));
2287
+ }
2288
+
2289
+ // packages/core/src/api/client.ts
2290
+ var stores = /* @__PURE__ */ new WeakMap();
2291
+ function buildClient(config) {
2292
+ validateRoot(config);
2293
+ const base = parseBaseURL(config.baseURL);
2294
+ const runtime = {
2295
+ base,
2296
+ transport: config.transport ?? createFetchTransport(),
2297
+ queryArrayFormat: config.queryArrayFormat ?? "repeat",
2298
+ querySerializer: config.querySerializer
2299
+ };
2300
+ const root = {
2301
+ headers: config.headers,
2302
+ timeout: config.timeout,
2303
+ retry: config.retry,
2304
+ middleware: config.middleware,
2305
+ requestMiddleware: config.requestMiddleware,
2306
+ responseMiddleware: config.responseMiddleware,
2307
+ transforms: config.transforms
2308
+ };
2309
+ const client = {};
2310
+ const all = [];
2311
+ for (const [name, definition] of Object.entries(config.resources)) {
2312
+ validateResource(name, definition);
2313
+ const built = createResourceClient(runtime, { name, definition, root });
2314
+ const resource = Object.freeze(built.client);
2315
+ if (built.store) {
2316
+ stores.set(resource, [built.store]);
2317
+ all.push(built.store);
2318
+ }
2319
+ client[name] = resource;
2320
+ }
2321
+ const frozen = Object.freeze(client);
2322
+ stores.set(frozen, all);
2323
+ return frozen;
2324
+ }
2325
+ function resetMockState(target) {
2326
+ const found = stores.get(target);
2327
+ if (!found || found.length === 0 && !Object.isFrozen(target)) {
2328
+ throw new ConfigError(
2329
+ "resetMock() expects a mocked resource (api.users) or a client created by defineApi()."
2330
+ );
2331
+ }
2332
+ for (const store of found) store.reset();
2333
+ }
2334
+
2335
+ // packages/core/src/api/define-api.ts
2336
+ function defineApi(config) {
2337
+ if (arguments.length === 0) return (curried) => buildClient(curried);
2338
+ return buildClient(config);
2339
+ }
2340
+
2341
+ // packages/core/src/api/define-apis.ts
2342
+ function build(configs) {
2343
+ if (!isPlainObject3(configs)) {
2344
+ throw new ConfigError(
2345
+ `defineApis() expects an object of named client configs (e.g. { backend: { baseURL, resources } }).`
2346
+ );
2347
+ }
2348
+ const out = {};
2349
+ for (const [name, config] of Object.entries(configs)) {
2350
+ try {
2351
+ out[name] = buildClient(config);
2352
+ } catch (error) {
2353
+ if (error instanceof ApiError && error.code === "CONFIG") {
2354
+ throw new ConfigError(`defineApis.${name}: ${error.message}`, { cause: error });
2355
+ }
2356
+ throw error;
2357
+ }
2358
+ }
2359
+ return Object.freeze(out);
2360
+ }
2361
+ function defineApis(configs) {
2362
+ if (arguments.length === 0) return (curried) => build(curried);
2363
+ return build(configs);
2364
+ }
2365
+
2366
+ // packages/core/src/api/reset.ts
2367
+ function resetMock(target) {
2368
+ resetMockState(target);
2369
+ }
2370
+
2371
+ export {
2372
+ ApiError,
2373
+ NetworkError,
2374
+ HttpError,
2375
+ ParseError,
2376
+ ValidationError,
2377
+ TimeoutError,
2378
+ AbortError,
2379
+ ConfigError,
2380
+ SecurityError,
2381
+ TransformError,
2382
+ MockError,
2383
+ isApiError,
2384
+ isHttpError,
2385
+ isTimeoutError,
2386
+ isMockError,
2387
+ createFetchTransport,
2388
+ MOCK_DATASETS,
2389
+ isMockDatasetId,
2390
+ listMockDatasetIds,
2391
+ registerMockDatasetLoader,
2392
+ normalizeMockSource,
2393
+ MockStore,
2394
+ runBuiltin,
2395
+ BUILTIN_CONVENTIONS,
2396
+ expandOperations,
2397
+ KEY_CASES,
2398
+ convertKey,
2399
+ transformKeys,
2400
+ isRetryableStatus,
2401
+ parseRetryAfter,
2402
+ computeBackoffMs,
2403
+ applyResourceSliceToListResult,
2404
+ OPERATION_METADATA,
2405
+ getOperationMetadata,
2406
+ createIdempotencyKey,
2407
+ defineApi,
2408
+ defineApis,
2409
+ resetMock
2410
+ };