@craftainer/crud-sdk 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.
- package/LICENSE +674 -0
- package/README.md +176 -2
- package/dist/auth-DwtNEKrw.mjs +22 -0
- package/dist/auth-DwtNEKrw.mjs.map +1 -0
- package/dist/cache/index.d.mts +166 -0
- package/dist/cache/index.mjs +972 -0
- package/dist/cache/index.mjs.map +1 -0
- package/dist/client-BLd126Jt.d.mts +303 -0
- package/dist/client-D0drumeA.mjs +913 -0
- package/dist/client-D0drumeA.mjs.map +1 -0
- package/dist/codegen/cli.d.mts +1 -0
- package/dist/codegen/cli.mjs +131 -0
- package/dist/codegen/cli.mjs.map +1 -0
- package/dist/events-Cqj9Rbon.mjs +616 -0
- package/dist/events-Cqj9Rbon.mjs.map +1 -0
- package/dist/events-DpSWzSN9.d.mts +88 -0
- package/dist/index.d.mts +98 -0
- package/dist/index.mjs +3 -0
- package/dist/interfaces-s73cVfIF.d.mts +109 -0
- package/dist/tanstack/index.d.mts +182 -0
- package/dist/tanstack/index.mjs +226 -0
- package/dist/tanstack/index.mjs.map +1 -0
- package/package.json +80 -4
|
@@ -0,0 +1,913 @@
|
|
|
1
|
+
import { n as cacheScope } from "./auth-DwtNEKrw.mjs";
|
|
2
|
+
//#region src/interfaces.ts
|
|
3
|
+
/**
|
|
4
|
+
* Folds `chain` (outermost first) around `terminal`, usually the network
|
|
5
|
+
* `fetch`, into one `Next`. Each middleware's `next` is the rest of the
|
|
6
|
+
* chain; an empty chain is `terminal`. A throwing middleware yields a
|
|
7
|
+
* rejected promise.
|
|
8
|
+
*/
|
|
9
|
+
function composeMiddleware(chain, terminal) {
|
|
10
|
+
const dispatch = async (index, ctx) => {
|
|
11
|
+
const middleware = chain[index];
|
|
12
|
+
if (middleware === void 0) return terminal(ctx);
|
|
13
|
+
return middleware(ctx, (next) => dispatch(index + 1, next));
|
|
14
|
+
};
|
|
15
|
+
return (ctx) => dispatch(0, ctx);
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/errors.ts
|
|
19
|
+
/**
|
|
20
|
+
* Error hierarchy: client-side errors and RFC 9457 problem details (ADR-0004).
|
|
21
|
+
* Mirrors the Python SDK class for class.
|
|
22
|
+
*/
|
|
23
|
+
/** Base class of every error this package throws on purpose. */
|
|
24
|
+
var CrudError = class extends Error {
|
|
25
|
+
constructor(message, options) {
|
|
26
|
+
super(message, options);
|
|
27
|
+
this.name = new.target.name;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/** The call was refused before any request was sent. */
|
|
31
|
+
var ClientError = class extends CrudError {};
|
|
32
|
+
/** The resource does not support the operation (manifest capability or path missing). */
|
|
33
|
+
var CapabilityError = class extends ClientError {};
|
|
34
|
+
/** A filter, sort or paging argument is not valid for the resource. */
|
|
35
|
+
var InvalidQueryError = class extends ClientError {};
|
|
36
|
+
/** The discovery document or the manifest is missing, unusable or of an unknown version. */
|
|
37
|
+
var ManifestError = class extends ClientError {};
|
|
38
|
+
/** The manifest lists no such resource for this caller. */
|
|
39
|
+
var UnknownResourceError = class extends ClientError {};
|
|
40
|
+
/** A successful response whose body is not the JSON the client expects; `cause` holds the parse error. */
|
|
41
|
+
var ResponseError = class extends CrudError {};
|
|
42
|
+
/** The request never produced a response; `cause` holds the transport error. */
|
|
43
|
+
var ConnectionFailed = class extends CrudError {};
|
|
44
|
+
const NO_TYPE = "about:blank";
|
|
45
|
+
/** An error response, as RFC 9457 problem details. */
|
|
46
|
+
var ProblemError = class extends CrudError {
|
|
47
|
+
status;
|
|
48
|
+
title;
|
|
49
|
+
detail;
|
|
50
|
+
type;
|
|
51
|
+
instance;
|
|
52
|
+
errors;
|
|
53
|
+
headers;
|
|
54
|
+
response;
|
|
55
|
+
constructor(init) {
|
|
56
|
+
const errors = init.errors ?? [];
|
|
57
|
+
let message = `${init.status} ${init.title}`;
|
|
58
|
+
if (init.detail) message += `: ${init.detail}`;
|
|
59
|
+
else if (errors.length > 0) message += `: ${errors.map((e) => `${e.loc.join(".")}: ${e.msg}`).join("; ")}`;
|
|
60
|
+
super(message);
|
|
61
|
+
this.status = init.status;
|
|
62
|
+
this.title = init.title;
|
|
63
|
+
this.detail = init.detail;
|
|
64
|
+
this.type = init.type ?? NO_TYPE;
|
|
65
|
+
this.instance = init.instance;
|
|
66
|
+
this.errors = errors;
|
|
67
|
+
this.headers = init.headers ?? new Headers();
|
|
68
|
+
this.response = init.response;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
/** 400. */
|
|
72
|
+
var BadRequest = class extends ProblemError {};
|
|
73
|
+
/** 401; `wwwAuthenticate` keeps the challenge. */
|
|
74
|
+
var Unauthorized = class extends ProblemError {
|
|
75
|
+
get wwwAuthenticate() {
|
|
76
|
+
return this.headers.get("www-authenticate") ?? void 0;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
/** 403. */
|
|
80
|
+
var Forbidden = class extends ProblemError {};
|
|
81
|
+
/** 404. */
|
|
82
|
+
var NotFound = class extends ProblemError {};
|
|
83
|
+
/** 412: a stale `If-Match`. */
|
|
84
|
+
var PreconditionFailed = class extends ProblemError {};
|
|
85
|
+
/** 422; `errors` holds normalized `{loc, msg, type}` entries. */
|
|
86
|
+
var ValidationFailed = class extends ProblemError {};
|
|
87
|
+
/** 423. */
|
|
88
|
+
var Locked = class extends ProblemError {};
|
|
89
|
+
/** 429; `retryAfter` is the `Retry-After` header in seconds, when it is a number. */
|
|
90
|
+
var RateLimited = class extends ProblemError {
|
|
91
|
+
get retryAfter() {
|
|
92
|
+
const raw = this.headers.get("retry-after");
|
|
93
|
+
if (raw === null || raw.trim() === "") return;
|
|
94
|
+
const seconds = Number(raw);
|
|
95
|
+
return Number.isNaN(seconds) ? void 0 : seconds;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
/** 5xx. */
|
|
99
|
+
var ServerError = class extends ProblemError {};
|
|
100
|
+
const BY_STATUS = /* @__PURE__ */ new Map([
|
|
101
|
+
[400, BadRequest],
|
|
102
|
+
[401, Unauthorized],
|
|
103
|
+
[403, Forbidden],
|
|
104
|
+
[404, NotFound],
|
|
105
|
+
[412, PreconditionFailed],
|
|
106
|
+
[422, ValidationFailed],
|
|
107
|
+
[423, Locked],
|
|
108
|
+
[429, RateLimited]
|
|
109
|
+
]);
|
|
110
|
+
const PHRASES = /* @__PURE__ */ new Map([
|
|
111
|
+
[100, "Continue"],
|
|
112
|
+
[101, "Switching Protocols"],
|
|
113
|
+
[102, "Processing"],
|
|
114
|
+
[103, "Early Hints"],
|
|
115
|
+
[200, "OK"],
|
|
116
|
+
[201, "Created"],
|
|
117
|
+
[202, "Accepted"],
|
|
118
|
+
[203, "Non-Authoritative Information"],
|
|
119
|
+
[204, "No Content"],
|
|
120
|
+
[205, "Reset Content"],
|
|
121
|
+
[206, "Partial Content"],
|
|
122
|
+
[207, "Multi-Status"],
|
|
123
|
+
[208, "Already Reported"],
|
|
124
|
+
[226, "IM Used"],
|
|
125
|
+
[300, "Multiple Choices"],
|
|
126
|
+
[301, "Moved Permanently"],
|
|
127
|
+
[302, "Found"],
|
|
128
|
+
[303, "See Other"],
|
|
129
|
+
[304, "Not Modified"],
|
|
130
|
+
[305, "Use Proxy"],
|
|
131
|
+
[307, "Temporary Redirect"],
|
|
132
|
+
[308, "Permanent Redirect"],
|
|
133
|
+
[400, "Bad Request"],
|
|
134
|
+
[401, "Unauthorized"],
|
|
135
|
+
[402, "Payment Required"],
|
|
136
|
+
[403, "Forbidden"],
|
|
137
|
+
[404, "Not Found"],
|
|
138
|
+
[405, "Method Not Allowed"],
|
|
139
|
+
[406, "Not Acceptable"],
|
|
140
|
+
[407, "Proxy Authentication Required"],
|
|
141
|
+
[408, "Request Timeout"],
|
|
142
|
+
[409, "Conflict"],
|
|
143
|
+
[410, "Gone"],
|
|
144
|
+
[411, "Length Required"],
|
|
145
|
+
[412, "Precondition Failed"],
|
|
146
|
+
[413, "Content Too Large"],
|
|
147
|
+
[414, "URI Too Long"],
|
|
148
|
+
[415, "Unsupported Media Type"],
|
|
149
|
+
[416, "Range Not Satisfiable"],
|
|
150
|
+
[417, "Expectation Failed"],
|
|
151
|
+
[418, "I'm a Teapot"],
|
|
152
|
+
[421, "Misdirected Request"],
|
|
153
|
+
[422, "Unprocessable Content"],
|
|
154
|
+
[423, "Locked"],
|
|
155
|
+
[424, "Failed Dependency"],
|
|
156
|
+
[425, "Too Early"],
|
|
157
|
+
[426, "Upgrade Required"],
|
|
158
|
+
[428, "Precondition Required"],
|
|
159
|
+
[429, "Too Many Requests"],
|
|
160
|
+
[431, "Request Header Fields Too Large"],
|
|
161
|
+
[451, "Unavailable For Legal Reasons"],
|
|
162
|
+
[500, "Internal Server Error"],
|
|
163
|
+
[501, "Not Implemented"],
|
|
164
|
+
[502, "Bad Gateway"],
|
|
165
|
+
[503, "Service Unavailable"],
|
|
166
|
+
[504, "Gateway Timeout"],
|
|
167
|
+
[505, "HTTP Version Not Supported"],
|
|
168
|
+
[506, "Variant Also Negotiates"],
|
|
169
|
+
[507, "Insufficient Storage"],
|
|
170
|
+
[508, "Loop Detected"],
|
|
171
|
+
[510, "Not Extended"],
|
|
172
|
+
[511, "Network Authentication Required"]
|
|
173
|
+
]);
|
|
174
|
+
const isObject$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
175
|
+
function normalizeErrors(items) {
|
|
176
|
+
return items.map((item) => {
|
|
177
|
+
if (isObject$1(item)) return {
|
|
178
|
+
loc: Array.isArray(item["loc"]) ? item["loc"] : [],
|
|
179
|
+
msg: String(item["msg"] ?? ""),
|
|
180
|
+
type: String(item["type"] ?? "")
|
|
181
|
+
};
|
|
182
|
+
return {
|
|
183
|
+
loc: [],
|
|
184
|
+
msg: String(item),
|
|
185
|
+
type: ""
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Builds the matching `ProblemError` subclass from an error response. A body
|
|
191
|
+
* that is not a JSON object yields a synthesized problem from the status
|
|
192
|
+
* phrase. Reads a clone, so `response.body` stays readable.
|
|
193
|
+
*/
|
|
194
|
+
async function problemFromResponse(response) {
|
|
195
|
+
const status = response.status;
|
|
196
|
+
const phrase = PHRASES.get(status) ?? "Error";
|
|
197
|
+
let parsed;
|
|
198
|
+
try {
|
|
199
|
+
parsed = JSON.parse(await response.clone().text());
|
|
200
|
+
} catch {
|
|
201
|
+
parsed = void 0;
|
|
202
|
+
}
|
|
203
|
+
const body = isObject$1(parsed) ? parsed : {};
|
|
204
|
+
const rawDetail = body["detail"];
|
|
205
|
+
const rawErrors = Array.isArray(rawDetail) ? rawDetail : body["errors"];
|
|
206
|
+
const Cls = BY_STATUS.get(status) ?? (status >= 500 ? ServerError : ProblemError);
|
|
207
|
+
const instance = body["instance"];
|
|
208
|
+
return new Cls({
|
|
209
|
+
status,
|
|
210
|
+
title: String(body["title"] || phrase),
|
|
211
|
+
detail: typeof rawDetail === "string" ? rawDetail : void 0,
|
|
212
|
+
type: String(body["type"] || NO_TYPE),
|
|
213
|
+
instance: typeof instance === "string" ? instance : void 0,
|
|
214
|
+
errors: Array.isArray(rawErrors) ? normalizeErrors(rawErrors) : [],
|
|
215
|
+
headers: response.headers,
|
|
216
|
+
response
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
//#endregion
|
|
220
|
+
//#region src/manifest.ts
|
|
221
|
+
/**
|
|
222
|
+
* Typed models of the CRUD manifest (v1) and discovery via the api-catalog.
|
|
223
|
+
* Members keep their wire names (`skip_param`, `max_age`, ...). Members this
|
|
224
|
+
* version does not know are ignored, as the manifest spec requires.
|
|
225
|
+
*/
|
|
226
|
+
const PROFILE = "https://craftainer.github.io/schemas/crud-manifest/v1";
|
|
227
|
+
const CATALOG_PATH = "/.well-known/api-catalog";
|
|
228
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
229
|
+
function bad(where, key, what) {
|
|
230
|
+
throw new ManifestError(`${where}.${key}: ${what}`);
|
|
231
|
+
}
|
|
232
|
+
function str(obj, key, where) {
|
|
233
|
+
const value = obj[key];
|
|
234
|
+
return typeof value === "string" ? value : bad(where, key, "missing or not a string");
|
|
235
|
+
}
|
|
236
|
+
function bool(obj, key, where) {
|
|
237
|
+
const value = obj[key];
|
|
238
|
+
return typeof value === "boolean" ? value : bad(where, key, "missing or not a boolean");
|
|
239
|
+
}
|
|
240
|
+
function num(obj, key, where) {
|
|
241
|
+
const value = obj[key];
|
|
242
|
+
return typeof value === "number" && Number.isFinite(value) ? value : bad(where, key, "missing or not a number");
|
|
243
|
+
}
|
|
244
|
+
function int(obj, key, where) {
|
|
245
|
+
const value = num(obj, key, where);
|
|
246
|
+
return Number.isInteger(value) ? value : bad(where, key, "not an integer");
|
|
247
|
+
}
|
|
248
|
+
function obj(parent, key, where) {
|
|
249
|
+
const value = parent[key];
|
|
250
|
+
return isObject(value) ? value : bad(where, key, "missing or not an object");
|
|
251
|
+
}
|
|
252
|
+
function list(parent, key, where) {
|
|
253
|
+
const value = parent[key];
|
|
254
|
+
return Array.isArray(value) ? value : bad(where, key, "missing or not a list");
|
|
255
|
+
}
|
|
256
|
+
function strs(parent, key, where) {
|
|
257
|
+
const items = list(parent, key, where);
|
|
258
|
+
return items.every((item) => typeof item === "string") ? items : bad(where, key, "expected a list of strings");
|
|
259
|
+
}
|
|
260
|
+
/** A member that may be absent or `null`. */
|
|
261
|
+
function opt(parent, key, where, read) {
|
|
262
|
+
return parent[key] === void 0 || parent[key] === null ? null : read(parent, key, where);
|
|
263
|
+
}
|
|
264
|
+
function stringMap(parent, key, where) {
|
|
265
|
+
const out = {};
|
|
266
|
+
for (const [k, v] of Object.entries(obj(parent, key, where))) if (typeof v === "string") out[k] = v;
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
function objects(parent, key, where) {
|
|
270
|
+
return list(parent, key, where).map((item, i) => isObject(item) ? item : bad(`${where}.${key}`, `[${i}]`, "not an object"));
|
|
271
|
+
}
|
|
272
|
+
function parseField(o, w) {
|
|
273
|
+
return {
|
|
274
|
+
name: str(o, "name", w),
|
|
275
|
+
value_type: str(o, "value_type", w),
|
|
276
|
+
filter_ops: strs(o, "filter_ops", w),
|
|
277
|
+
sortable: bool(o, "sortable", w),
|
|
278
|
+
choices: opt(o, "choices", w, strs),
|
|
279
|
+
numeric: bool(o, "numeric", w),
|
|
280
|
+
categorical: bool(o, "categorical", w),
|
|
281
|
+
read_only: bool(o, "read_only", w),
|
|
282
|
+
required: bool(o, "required", w)
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function parseEvents(o, w) {
|
|
286
|
+
return {
|
|
287
|
+
transport: str(o, "transport", w),
|
|
288
|
+
path: str(o, "path", w),
|
|
289
|
+
resource: str(o, "resource", w),
|
|
290
|
+
resume_param: str(o, "resume_param", w),
|
|
291
|
+
replay: str(o, "replay", w),
|
|
292
|
+
keepalive_seconds: num(o, "keepalive_seconds", w),
|
|
293
|
+
actions: strs(o, "actions", w)
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function parseCapabilities(o, w) {
|
|
297
|
+
return {
|
|
298
|
+
bulk_update: bool(o, "bulk_update", w),
|
|
299
|
+
bulk_delete: bool(o, "bulk_delete", w),
|
|
300
|
+
clone: bool(o, "clone", w),
|
|
301
|
+
draft_publish: bool(o, "draft_publish", w),
|
|
302
|
+
archive_restore: bool(o, "archive_restore", w),
|
|
303
|
+
revisions: bool(o, "revisions", w),
|
|
304
|
+
events: opt(o, "events", w, (p, k, ww) => parseEvents(obj(p, k, ww), `${ww}.${k}`)),
|
|
305
|
+
stats: bool(o, "stats", w),
|
|
306
|
+
predict: bool(o, "predict", w),
|
|
307
|
+
conditional_requests: bool(o, "conditional_requests", w),
|
|
308
|
+
owner_scoped: bool(o, "owner_scoped", w),
|
|
309
|
+
schedulable: bool(o, "schedulable", w)
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function parseResource(o, w) {
|
|
313
|
+
const schemas = obj(o, "schemas", w);
|
|
314
|
+
const pagination = obj(o, "pagination", w);
|
|
315
|
+
const auth = obj(o, "auth", w);
|
|
316
|
+
const cache = obj(o, "cache", w);
|
|
317
|
+
const sw = `${w}.schemas`;
|
|
318
|
+
return {
|
|
319
|
+
name: str(o, "name", w),
|
|
320
|
+
title: str(o, "title", w),
|
|
321
|
+
api_version: str(o, "api_version", w),
|
|
322
|
+
base_path: str(o, "base_path", w),
|
|
323
|
+
formats: stringMap(o, "formats", w),
|
|
324
|
+
id_field: str(o, "id_field", w),
|
|
325
|
+
schemas: {
|
|
326
|
+
read: obj(schemas, "read", sw),
|
|
327
|
+
create: obj(schemas, "create", sw),
|
|
328
|
+
update: obj(schemas, "update", sw),
|
|
329
|
+
draft: opt(schemas, "draft", sw, obj)
|
|
330
|
+
},
|
|
331
|
+
fields: objects(o, "fields", w).map((f, i) => parseField(f, `${w}.fields[${i}]`)),
|
|
332
|
+
pagination: {
|
|
333
|
+
style: str(pagination, "style", w),
|
|
334
|
+
skip_param: str(pagination, "skip_param", w),
|
|
335
|
+
limit_param: str(pagination, "limit_param", w),
|
|
336
|
+
sort_param: str(pagination, "sort_param", w),
|
|
337
|
+
default_limit: int(pagination, "default_limit", w),
|
|
338
|
+
max_limit: int(pagination, "max_limit", w),
|
|
339
|
+
total: bool(pagination, "total", w)
|
|
340
|
+
},
|
|
341
|
+
capabilities: parseCapabilities(obj(o, "capabilities", w), `${w}.capabilities`),
|
|
342
|
+
paths: stringMap(o, "paths", w),
|
|
343
|
+
bulk: { max_matched: int(obj(o, "bulk", w), "max_matched", `${w}.bulk`) },
|
|
344
|
+
auth: {
|
|
345
|
+
scheme: str(auth, "scheme", w),
|
|
346
|
+
oidc_discovery_url: opt(auth, "oidc_discovery_url", w, str),
|
|
347
|
+
public_read: bool(auth, "public_read", w),
|
|
348
|
+
read_roles: strs(auth, "read_roles", w),
|
|
349
|
+
write_roles: strs(auth, "write_roles", w),
|
|
350
|
+
delete_roles: strs(auth, "delete_roles", w)
|
|
351
|
+
},
|
|
352
|
+
cache: {
|
|
353
|
+
etag: bool(cache, "etag", w),
|
|
354
|
+
max_age: int(cache, "max_age", w),
|
|
355
|
+
stats_ttl: opt(cache, "stats_ttl", w, int),
|
|
356
|
+
stale_while_revalidate: int(cache, "stale_while_revalidate", w),
|
|
357
|
+
list_etag: opt(cache, "list_etag", w, str),
|
|
358
|
+
record_etag: opt(cache, "record_etag", w, str),
|
|
359
|
+
revalidate_only: bool(cache, "revalidate_only", w)
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
/** Parses a manifest document; unknown members are ignored, unknown versions refused. */
|
|
364
|
+
function parseManifest(document) {
|
|
365
|
+
if (!isObject(document)) throw new ManifestError("the manifest is not a JSON object");
|
|
366
|
+
const version = str(document, "manifest_version", "manifest");
|
|
367
|
+
if (version !== "1") throw new ManifestError(`unsupported manifest_version "${version}"`);
|
|
368
|
+
const service = obj(document, "service", "manifest");
|
|
369
|
+
return {
|
|
370
|
+
manifest_version: version,
|
|
371
|
+
service: {
|
|
372
|
+
name: str(service, "name", "manifest.service"),
|
|
373
|
+
version: str(service, "version", "manifest.service")
|
|
374
|
+
},
|
|
375
|
+
resources: objects(document, "resources", "manifest").map((r, i) => parseResource(r, `resources[${i}]`))
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* The resource called `name`: the one with `apiVersion` when given, else the
|
|
380
|
+
* highest `api_version` (`v2` beats `v1`); `undefined` when there is none.
|
|
381
|
+
*/
|
|
382
|
+
function findResource(manifest, name, apiVersion) {
|
|
383
|
+
const rank = (r) => Number.parseInt(r.api_version.slice(1), 10) || 0;
|
|
384
|
+
let best;
|
|
385
|
+
for (const r of manifest.resources) if (r.name === name && (apiVersion === void 0 || r.api_version === apiVersion)) {
|
|
386
|
+
if (best === void 0 || rank(r) > rank(best)) best = r;
|
|
387
|
+
}
|
|
388
|
+
return best;
|
|
389
|
+
}
|
|
390
|
+
/** The absolute manifest URL: the first `describedby` link with the manifest profile. */
|
|
391
|
+
function manifestUrlFromCatalog(linkset, catalogUrl) {
|
|
392
|
+
const entries = isObject(linkset) ? linkset["linkset"] : void 0;
|
|
393
|
+
for (const entry of Array.isArray(entries) ? entries : []) {
|
|
394
|
+
const links = isObject(entry) ? entry["describedby"] : void 0;
|
|
395
|
+
for (const link of Array.isArray(links) ? links : []) if (isObject(link) && link["profile"] === "https://craftainer.github.io/schemas/crud-manifest/v1" && typeof link["href"] === "string") {
|
|
396
|
+
const url = new URL(link["href"], catalogUrl);
|
|
397
|
+
if (url.origin !== new URL(catalogUrl).origin) throw new ManifestError(`the api-catalog points at another origin (${url.origin}); pass manifestUrl to use it`);
|
|
398
|
+
return url.href;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
throw new ManifestError(`the api-catalog has no describedby link with profile ${PROFILE}`);
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Loads and parses the manifest, via api-catalog discovery from `baseUrl` or
|
|
405
|
+
* a direct `manifestUrl`.
|
|
406
|
+
*/
|
|
407
|
+
async function loadManifest(getJson, baseUrl, manifestUrl) {
|
|
408
|
+
let url = manifestUrl;
|
|
409
|
+
if (url === void 0) {
|
|
410
|
+
const catalogUrl = new URL(CATALOG_PATH, baseUrl).href;
|
|
411
|
+
url = manifestUrlFromCatalog(await getJson(catalogUrl, true), catalogUrl);
|
|
412
|
+
}
|
|
413
|
+
return parseManifest(await getJson(url, false));
|
|
414
|
+
}
|
|
415
|
+
//#endregion
|
|
416
|
+
//#region src/query.ts
|
|
417
|
+
/**
|
|
418
|
+
* Query-string builder with `crud_query` semantics, validated against the
|
|
419
|
+
* manifest fields. Produces the same bytes as the Python SDK.
|
|
420
|
+
*/
|
|
421
|
+
const MAX_REGEX_LENGTH = 200;
|
|
422
|
+
/** Percent-encodes everything but the RFC 3986 unreserved characters `A-Za-z0-9-._~`. */
|
|
423
|
+
function quote(text) {
|
|
424
|
+
return encodeURIComponent(text).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
425
|
+
}
|
|
426
|
+
/** Joins already-stringified pairs, encoding keys and values. */
|
|
427
|
+
function encodeParams(pairs) {
|
|
428
|
+
return pairs.map(([k, v]) => `${quote(k)}=${quote(v)}`).join("&");
|
|
429
|
+
}
|
|
430
|
+
function formatDatetime(value) {
|
|
431
|
+
if (Number.isNaN(value.getTime())) return;
|
|
432
|
+
const iso = value.toISOString();
|
|
433
|
+
if (iso.length !== 24) return;
|
|
434
|
+
const millis = iso.slice(20, 23).replace(/0+$/, "");
|
|
435
|
+
return millis === "" ? `${iso.slice(0, 19)}Z` : `${iso.slice(0, 19)}.${millis}Z`;
|
|
436
|
+
}
|
|
437
|
+
function formatDate(value) {
|
|
438
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return;
|
|
439
|
+
const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
|
|
440
|
+
return Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== value ? void 0 : value;
|
|
441
|
+
}
|
|
442
|
+
function scalarText(kind, value) {
|
|
443
|
+
switch (kind) {
|
|
444
|
+
case "integer": return typeof value === "number" && Number.isSafeInteger(value) ? String(value) : void 0;
|
|
445
|
+
case "number": return typeof value === "number" && Number.isFinite(value) ? String(value) : void 0;
|
|
446
|
+
case "boolean": return typeof value === "boolean" ? String(value) : void 0;
|
|
447
|
+
case "date": return typeof value === "string" ? formatDate(value) : void 0;
|
|
448
|
+
case "datetime": return value instanceof Date ? formatDatetime(value) : void 0;
|
|
449
|
+
default: return typeof value === "string" ? value : void 0;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Wire form of one value for `field`; throws `InvalidQueryError` on a type
|
|
454
|
+
* mismatch. Dates are `YYYY-MM-DD` strings, datetimes `Date`s (UTC, with
|
|
455
|
+
* millisecond precision, trailing zeros trimmed).
|
|
456
|
+
*/
|
|
457
|
+
function formatScalar(field, value) {
|
|
458
|
+
const text = scalarText(field.value_type, value);
|
|
459
|
+
if (text === void 0) throw new InvalidQueryError(`field "${field.name}" (${field.value_type}) cannot take ${String(value)}`);
|
|
460
|
+
if (field.value_type === "enum" && field.choices !== null && !field.choices.includes(text)) throw new InvalidQueryError(`field "${field.name}" accepts only ${JSON.stringify(field.choices)}, not "${text}"`);
|
|
461
|
+
return text;
|
|
462
|
+
}
|
|
463
|
+
function filterPair(field, op, value) {
|
|
464
|
+
const key = op === "eq" ? field.name : `${field.name}__${op}`;
|
|
465
|
+
if (op === "in") {
|
|
466
|
+
if (!Array.isArray(value) || value.length === 0) throw new InvalidQueryError(`"${key}" takes a non-empty array`);
|
|
467
|
+
const parts = value.map((item) => formatScalar(field, item));
|
|
468
|
+
if (parts.some((part) => part.includes(","))) throw new InvalidQueryError(`"${key}": a value contains a comma, which separates the list`);
|
|
469
|
+
return [key, parts.map(quote).join(",")];
|
|
470
|
+
}
|
|
471
|
+
const text = formatScalar(field, value);
|
|
472
|
+
if (op === "regex" && [...text].length > MAX_REGEX_LENGTH) throw new InvalidQueryError(`"${key}": a regex is limited to ${MAX_REGEX_LENGTH} characters`);
|
|
473
|
+
return [key, quote(text)];
|
|
474
|
+
}
|
|
475
|
+
const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
476
|
+
function filterPairs(fields, filters) {
|
|
477
|
+
const parsed = /* @__PURE__ */ new Map();
|
|
478
|
+
for (const [rawKey, value] of Object.entries(filters)) {
|
|
479
|
+
const at = rawKey.lastIndexOf("__");
|
|
480
|
+
const name = at < 0 ? rawKey : rawKey.slice(0, at);
|
|
481
|
+
const op = at < 0 ? "eq" : rawKey.slice(at + 2);
|
|
482
|
+
const field = fields.get(name);
|
|
483
|
+
if (field === void 0) throw new InvalidQueryError(`unknown filter field "${name}"`);
|
|
484
|
+
if (!field.filter_ops.includes(op)) throw new InvalidQueryError(`field "${name}" does not support the "${op}" operator`);
|
|
485
|
+
if (name === "id" && op === "eq") throw new InvalidQueryError("filter on id with recordId, not an equality filter");
|
|
486
|
+
const id = `${name}\u0000${op}`;
|
|
487
|
+
if (parsed.has(id)) throw new InvalidQueryError(`filter "${name}" "${op}" given twice`);
|
|
488
|
+
parsed.set(id, {
|
|
489
|
+
name,
|
|
490
|
+
op,
|
|
491
|
+
pair: filterPair(field, op, value)
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
return [...parsed.values()].sort((a, b) => compare(a.name, b.name) || compare(a.op, b.op)).map((entry) => entry.pair);
|
|
495
|
+
}
|
|
496
|
+
function sortValue(fields, sort) {
|
|
497
|
+
const out = (typeof sort === "string" ? sort.split(",") : sort).map((part) => {
|
|
498
|
+
const name = part.startsWith("-") ? part.slice(1) : part;
|
|
499
|
+
const field = fields.get(name);
|
|
500
|
+
if (field === void 0 || !field.sortable) throw new InvalidQueryError(`cannot sort by "${name}"`);
|
|
501
|
+
return quote(part);
|
|
502
|
+
});
|
|
503
|
+
if (out.length === 0) throw new InvalidQueryError("sort is empty");
|
|
504
|
+
return out.join(",");
|
|
505
|
+
}
|
|
506
|
+
function intArg(name, value, minimum) {
|
|
507
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum) throw new InvalidQueryError(`${name} must be an integer >= ${minimum}`);
|
|
508
|
+
return String(value);
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* The query string (no leading `?`) for a list, get or bulk call. Order of
|
|
512
|
+
* parameters: `id`, filters sorted by (field, operator), sort, skip, limit,
|
|
513
|
+
* `include_archived`, `include_unpublished`. The sort/skip/limit names are
|
|
514
|
+
* `sortParam`/`skipParam`/`limitParam` (the manifest's `pagination`); the
|
|
515
|
+
* `id` parameter is always `id`. Flags are emitted whenever they are not
|
|
516
|
+
* `undefined`.
|
|
517
|
+
*/
|
|
518
|
+
function buildQuery(fields, params = {}) {
|
|
519
|
+
const byName = new Map(fields.map((f) => [f.name, f]));
|
|
520
|
+
const parts = [];
|
|
521
|
+
if (params.recordId !== void 0) parts.push(`id=${intArg("recordId", params.recordId, 0)}`);
|
|
522
|
+
for (const [key, value] of filterPairs(byName, params.filters ?? {})) parts.push(`${quote(key)}=${value}`);
|
|
523
|
+
if (params.sort !== void 0) parts.push(`${quote(params.sortParam ?? "sort")}=${sortValue(byName, params.sort)}`);
|
|
524
|
+
if (params.skip !== void 0) parts.push(`${quote(params.skipParam ?? "skip")}=${intArg("skip", params.skip, 0)}`);
|
|
525
|
+
if (params.limit !== void 0) {
|
|
526
|
+
const text = intArg("limit", params.limit, 1);
|
|
527
|
+
if (params.maxLimit !== void 0 && params.limit > params.maxLimit) throw new InvalidQueryError(`limit ${params.limit} exceeds the server maximum ${params.maxLimit}`);
|
|
528
|
+
parts.push(`${quote(params.limitParam ?? "limit")}=${text}`);
|
|
529
|
+
}
|
|
530
|
+
if (params.includeArchived !== void 0) parts.push(`include_archived=${params.includeArchived}`);
|
|
531
|
+
if (params.includeUnpublished !== void 0) parts.push(`include_unpublished=${params.includeUnpublished}`);
|
|
532
|
+
return parts.join("&");
|
|
533
|
+
}
|
|
534
|
+
//#endregion
|
|
535
|
+
//#region src/client.ts
|
|
536
|
+
/** Fetch client: manifest-driven resource handles over a middleware chain. */
|
|
537
|
+
const joinUrl = (base, path) => new URL(path, base).href;
|
|
538
|
+
/** Builds the requests of one resource. */
|
|
539
|
+
var Plan = class {
|
|
540
|
+
#baseUrl;
|
|
541
|
+
resource;
|
|
542
|
+
constructor(baseUrl, resource) {
|
|
543
|
+
this.#baseUrl = baseUrl;
|
|
544
|
+
this.resource = resource;
|
|
545
|
+
}
|
|
546
|
+
#call(method, op, path, query = "", extra = {}) {
|
|
547
|
+
return {
|
|
548
|
+
method,
|
|
549
|
+
url: joinUrl(this.#baseUrl, path) + (query === "" ? "" : `?${query}`),
|
|
550
|
+
op,
|
|
551
|
+
resource: this.resource.name,
|
|
552
|
+
recordId: extra.recordId,
|
|
553
|
+
apiVersion: this.resource.api_version,
|
|
554
|
+
...extra.json === void 0 ? {} : { json: extra.json },
|
|
555
|
+
...extra.ifMatch === void 0 ? {} : { headers: { "If-Match": extra.ifMatch } }
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
#query(params = {}) {
|
|
559
|
+
const paging = this.resource.pagination;
|
|
560
|
+
return buildQuery(this.resource.fields, {
|
|
561
|
+
maxLimit: paging.max_limit,
|
|
562
|
+
skipParam: paging.skip_param,
|
|
563
|
+
limitParam: paging.limit_param,
|
|
564
|
+
sortParam: paging.sort_param,
|
|
565
|
+
...params
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
#subPath(key, supported) {
|
|
569
|
+
const path = this.resource.paths[key];
|
|
570
|
+
if (!supported || path === void 0) throw new CapabilityError(`resource "${this.resource.name}" does not support ${key}`);
|
|
571
|
+
return path;
|
|
572
|
+
}
|
|
573
|
+
#need(supported, what) {
|
|
574
|
+
if (!supported) throw new CapabilityError(`resource "${this.resource.name}" does not support ${what}`);
|
|
575
|
+
}
|
|
576
|
+
#bulkFilters(filters) {
|
|
577
|
+
if (Object.keys(filters).length === 0) throw new InvalidQueryError("a bulk operation needs at least one filter");
|
|
578
|
+
return this.#query({ filters });
|
|
579
|
+
}
|
|
580
|
+
get #base() {
|
|
581
|
+
return this.resource.base_path;
|
|
582
|
+
}
|
|
583
|
+
eventsUrl() {
|
|
584
|
+
const events = this.resource.capabilities.events;
|
|
585
|
+
if (events === null) throw new CapabilityError(`resource "${this.resource.name}" does not support events`);
|
|
586
|
+
return joinUrl(this.#baseUrl, events.path);
|
|
587
|
+
}
|
|
588
|
+
list(o) {
|
|
589
|
+
const query = this.#query({
|
|
590
|
+
filters: o.filters,
|
|
591
|
+
sort: o.sort,
|
|
592
|
+
skip: o.skip,
|
|
593
|
+
limit: o.limit,
|
|
594
|
+
includeArchived: o.includeArchived,
|
|
595
|
+
includeUnpublished: o.includeUnpublished
|
|
596
|
+
});
|
|
597
|
+
return this.#call("GET", "list", this.#base, query);
|
|
598
|
+
}
|
|
599
|
+
get(id) {
|
|
600
|
+
return this.#call("GET", "record", this.#base, this.#query({ recordId: id }), { recordId: id });
|
|
601
|
+
}
|
|
602
|
+
create(data) {
|
|
603
|
+
return this.#call("POST", "create", this.#base, "", { json: data });
|
|
604
|
+
}
|
|
605
|
+
update(id, data, ifMatch) {
|
|
606
|
+
return this.#call("PATCH", "update", this.#base, this.#query({ recordId: id }), {
|
|
607
|
+
recordId: id,
|
|
608
|
+
json: data,
|
|
609
|
+
ifMatch
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
updateMany(filters, data) {
|
|
613
|
+
this.#need(this.resource.capabilities.bulk_update, "bulk_update");
|
|
614
|
+
return this.#call("PATCH", "bulk_update", this.#base, this.#bulkFilters(filters), { json: data });
|
|
615
|
+
}
|
|
616
|
+
delete(id, ifMatch) {
|
|
617
|
+
return this.#call("DELETE", "delete", this.#base, this.#query({ recordId: id }), {
|
|
618
|
+
recordId: id,
|
|
619
|
+
ifMatch
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
deleteMany(filters) {
|
|
623
|
+
this.#need(this.resource.capabilities.bulk_delete, "bulk_delete");
|
|
624
|
+
return this.#call("DELETE", "bulk_delete", this.#base, this.#bulkFilters(filters));
|
|
625
|
+
}
|
|
626
|
+
restore(id, filters) {
|
|
627
|
+
const path = this.#subPath("restore", this.resource.capabilities.archive_restore);
|
|
628
|
+
if (id === void 0 === (filters === void 0)) throw new InvalidQueryError("restore takes exactly one of id and filters");
|
|
629
|
+
const query = filters === void 0 ? this.#query({ recordId: id }) : this.#bulkFilters(filters);
|
|
630
|
+
return this.#call("POST", "restore", path, query, { recordId: id });
|
|
631
|
+
}
|
|
632
|
+
stats(o) {
|
|
633
|
+
const path = this.#subPath("stats", this.resource.capabilities.stats);
|
|
634
|
+
const pairs = o.bucket === void 0 ? [] : [["bucket", o.bucket]];
|
|
635
|
+
if (o.includeArchived !== void 0) pairs.push(["include_archived", String(o.includeArchived)]);
|
|
636
|
+
if (o.includeUnpublished !== void 0) pairs.push(["include_unpublished", String(o.includeUnpublished)]);
|
|
637
|
+
return this.#call("GET", "stats", path, encodeParams(pairs));
|
|
638
|
+
}
|
|
639
|
+
predict(o) {
|
|
640
|
+
const path = this.#subPath("predict", this.resource.capabilities.predict);
|
|
641
|
+
const pairs = [];
|
|
642
|
+
if (o.field !== void 0) {
|
|
643
|
+
const spec = this.resource.fields.find((f) => f.name === o.field);
|
|
644
|
+
if (spec === void 0 || !spec.numeric) throw new InvalidQueryError(`cannot predict "${o.field}": not a numeric field`);
|
|
645
|
+
pairs.push(["field", o.field]);
|
|
646
|
+
}
|
|
647
|
+
if (o.periods !== void 0) {
|
|
648
|
+
if (!Number.isSafeInteger(o.periods) || o.periods < 1) throw new InvalidQueryError("periods must be an integer >= 1");
|
|
649
|
+
pairs.push(["periods", String(o.periods)]);
|
|
650
|
+
}
|
|
651
|
+
if (o.bucket !== void 0) pairs.push(["bucket", o.bucket]);
|
|
652
|
+
return this.#call("GET", "predict", path, encodeParams(pairs));
|
|
653
|
+
}
|
|
654
|
+
clone(id) {
|
|
655
|
+
const path = this.#subPath("clone", this.resource.capabilities.clone);
|
|
656
|
+
return this.#call("POST", "clone", path, this.#query({ recordId: id }), { recordId: id });
|
|
657
|
+
}
|
|
658
|
+
draft(data) {
|
|
659
|
+
const path = this.#subPath("draft", this.resource.capabilities.draft_publish);
|
|
660
|
+
return this.#call("POST", "draft", path, "", { json: data });
|
|
661
|
+
}
|
|
662
|
+
publish(id) {
|
|
663
|
+
const path = this.#subPath("publish", this.resource.capabilities.draft_publish);
|
|
664
|
+
return this.#call("POST", "publish", path, this.#query({ recordId: id }), { recordId: id });
|
|
665
|
+
}
|
|
666
|
+
filters() {
|
|
667
|
+
return this.#call("GET", "filters", this.#subPath("filters", true));
|
|
668
|
+
}
|
|
669
|
+
revisions(id) {
|
|
670
|
+
const path = this.#subPath("revisions", this.resource.capabilities.revisions);
|
|
671
|
+
return this.#call("GET", "revisions", path, this.#query({ recordId: id }), { recordId: id });
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
/**
|
|
675
|
+
* Operations on one resource. The type parameters are the record views;
|
|
676
|
+
* `crud-sdk-codegen` writes interfaces to pass for them (they are not
|
|
677
|
+
* checked against the manifest at runtime).
|
|
678
|
+
*/
|
|
679
|
+
var ResourceClient = class {
|
|
680
|
+
/** The manifest entry this handle is bound to. */
|
|
681
|
+
resource;
|
|
682
|
+
#plan;
|
|
683
|
+
#transport;
|
|
684
|
+
/** Use `CrudClient.resource`. */
|
|
685
|
+
constructor(baseUrl, resource, transport) {
|
|
686
|
+
this.resource = resource;
|
|
687
|
+
this.#plan = new Plan(baseUrl, resource);
|
|
688
|
+
this.#transport = transport;
|
|
689
|
+
}
|
|
690
|
+
async #one(call, options) {
|
|
691
|
+
return await this.#transport.json(call, options.signal);
|
|
692
|
+
}
|
|
693
|
+
/** One page of records. */
|
|
694
|
+
async list(options = {}) {
|
|
695
|
+
return this.#one(this.#plan.list(options), options);
|
|
696
|
+
}
|
|
697
|
+
/** Every record, paging with `skip += limit` until a page shorter than `limit`. */
|
|
698
|
+
async *iterAll(options = {}) {
|
|
699
|
+
const size = options.limit ?? this.resource.pagination.default_limit;
|
|
700
|
+
let skip = options.skip ?? 0;
|
|
701
|
+
for (;;) {
|
|
702
|
+
const page = await this.list({
|
|
703
|
+
...options,
|
|
704
|
+
skip,
|
|
705
|
+
limit: size
|
|
706
|
+
});
|
|
707
|
+
yield* page;
|
|
708
|
+
if (page.length < size) return;
|
|
709
|
+
skip += size;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
/** One record. */
|
|
713
|
+
async get(id, options = {}) {
|
|
714
|
+
return this.#one(this.#plan.get(id), options);
|
|
715
|
+
}
|
|
716
|
+
/** Creates a record. */
|
|
717
|
+
async create(data, options = {}) {
|
|
718
|
+
return this.#one(this.#plan.create(data), options);
|
|
719
|
+
}
|
|
720
|
+
/** Patches one record; `ifMatch` is an ETag (`412` when stale). */
|
|
721
|
+
async update(id, data, options = {}) {
|
|
722
|
+
return this.#one(this.#plan.update(id, data, options.ifMatch), options);
|
|
723
|
+
}
|
|
724
|
+
/** Patches every record matching `filters`. */
|
|
725
|
+
async updateMany(filters, data, options = {}) {
|
|
726
|
+
return this.#one(this.#plan.updateMany(filters, data), options);
|
|
727
|
+
}
|
|
728
|
+
/** Deletes one record; `ifMatch` as for `update`. */
|
|
729
|
+
async delete(id, options = {}) {
|
|
730
|
+
await this.#transport.none(this.#plan.delete(id, options.ifMatch), options.signal);
|
|
731
|
+
}
|
|
732
|
+
/** Deletes every record matching `filters`. */
|
|
733
|
+
async deleteMany(filters, options = {}) {
|
|
734
|
+
return this.#one(this.#plan.deleteMany(filters), options);
|
|
735
|
+
}
|
|
736
|
+
/** Restores archived records, by `id` or by `filters`. */
|
|
737
|
+
async restore(options) {
|
|
738
|
+
return this.#one(this.#plan.restore(options.id, options.filters), options);
|
|
739
|
+
}
|
|
740
|
+
/** Aggregates over the resource. */
|
|
741
|
+
async stats(options = {}) {
|
|
742
|
+
return this.#one(this.#plan.stats(options), options);
|
|
743
|
+
}
|
|
744
|
+
/** A forecast. */
|
|
745
|
+
async predict(options = {}) {
|
|
746
|
+
return this.#one(this.#plan.predict(options), options);
|
|
747
|
+
}
|
|
748
|
+
/** Copies a record; returns the copy. */
|
|
749
|
+
async clone(id, options = {}) {
|
|
750
|
+
return this.#one(this.#plan.clone(id), options);
|
|
751
|
+
}
|
|
752
|
+
/** Creates a draft (needs `draft_publish`). */
|
|
753
|
+
async createDraft(data, options = {}) {
|
|
754
|
+
return this.#one(this.#plan.draft(data), options);
|
|
755
|
+
}
|
|
756
|
+
/** Publishes a draft (needs `draft_publish`). */
|
|
757
|
+
async publish(id, options = {}) {
|
|
758
|
+
return this.#one(this.#plan.publish(id), options);
|
|
759
|
+
}
|
|
760
|
+
/** The server's filter metadata (`kind` instead of `value_type`). */
|
|
761
|
+
async filters(options = {}) {
|
|
762
|
+
return this.#one(this.#plan.filters(), options);
|
|
763
|
+
}
|
|
764
|
+
/** Revisions of one record, newest first. */
|
|
765
|
+
async revisions(id, options = {}) {
|
|
766
|
+
return this.#one(this.#plan.revisions(id), options);
|
|
767
|
+
}
|
|
768
|
+
/** Absolute URL of the resource's SSE stream. */
|
|
769
|
+
eventsUrl() {
|
|
770
|
+
return this.#plan.eventsUrl();
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
/** Discovers the manifest and hands out {@link ResourceClient}s. */
|
|
774
|
+
var CrudClient = class {
|
|
775
|
+
baseUrl;
|
|
776
|
+
#fetch;
|
|
777
|
+
#tokenProvider;
|
|
778
|
+
#middleware;
|
|
779
|
+
#manifestUrl;
|
|
780
|
+
#manifests = /* @__PURE__ */ new Map();
|
|
781
|
+
#transport;
|
|
782
|
+
constructor(options) {
|
|
783
|
+
this.baseUrl = options.baseUrl;
|
|
784
|
+
this.#fetch = options.fetch;
|
|
785
|
+
this.#tokenProvider = options.tokenProvider;
|
|
786
|
+
this.#middleware = [...options.middleware ?? []];
|
|
787
|
+
this.#manifestUrl = options.manifestUrl;
|
|
788
|
+
this.#transport = {
|
|
789
|
+
json: async (call, signal) => {
|
|
790
|
+
const response = await this.#send(call, signal);
|
|
791
|
+
if (response.status === 204) return;
|
|
792
|
+
try {
|
|
793
|
+
return await response.json();
|
|
794
|
+
} catch (error) {
|
|
795
|
+
throw new ResponseError(`${call.method} ${call.url}: the ${response.status} response body is not valid JSON`, { cause: error });
|
|
796
|
+
}
|
|
797
|
+
},
|
|
798
|
+
none: async (call, signal) => {
|
|
799
|
+
await (await this.#send(call, signal)).body?.cancel();
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
/** Adds middleware after the existing ones (innermost); it sees every later request. */
|
|
804
|
+
use(middleware) {
|
|
805
|
+
this.#middleware.push(middleware);
|
|
806
|
+
return this;
|
|
807
|
+
}
|
|
808
|
+
async #token(refresh) {
|
|
809
|
+
return await this.#tokenProvider?.({ refresh }) || null;
|
|
810
|
+
}
|
|
811
|
+
/** The current bearer token, or `null` when anonymous. */
|
|
812
|
+
token(refresh = false) {
|
|
813
|
+
return this.#token(refresh);
|
|
814
|
+
}
|
|
815
|
+
/** The cache scope of the current caller (`"anon"` or a hash of the token). */
|
|
816
|
+
async scope() {
|
|
817
|
+
return cacheScope(await this.#token(false));
|
|
818
|
+
}
|
|
819
|
+
async #dispatch(call, token, signal) {
|
|
820
|
+
const headers = new Headers({
|
|
821
|
+
Accept: "application/json",
|
|
822
|
+
...call.headers
|
|
823
|
+
});
|
|
824
|
+
if (token !== null) headers.set("Authorization", `Bearer ${token}`);
|
|
825
|
+
const init = {
|
|
826
|
+
method: call.method,
|
|
827
|
+
headers,
|
|
828
|
+
signal: signal ?? null
|
|
829
|
+
};
|
|
830
|
+
if (call.json !== void 0) {
|
|
831
|
+
headers.set("Content-Type", "application/json");
|
|
832
|
+
init.body = JSON.stringify(call.json);
|
|
833
|
+
}
|
|
834
|
+
const request = new Request(call.url, init);
|
|
835
|
+
const terminal = async (ctx) => {
|
|
836
|
+
try {
|
|
837
|
+
return await (this.#fetch ?? globalThis.fetch)(ctx.request);
|
|
838
|
+
} catch (error) {
|
|
839
|
+
if (ctx.request.signal.aborted) throw error;
|
|
840
|
+
throw new ConnectionFailed(error instanceof Error ? error.message : String(error), { cause: error });
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
return composeMiddleware(this.#middleware, terminal)({
|
|
844
|
+
request,
|
|
845
|
+
resource: call.resource,
|
|
846
|
+
op: call.op,
|
|
847
|
+
recordId: call.recordId,
|
|
848
|
+
scope: await cacheScope(token),
|
|
849
|
+
apiVersion: call.apiVersion,
|
|
850
|
+
signal
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
/** Sends `call`, retrying once with a refreshed token after a `401`; throws on `>= 400`. */
|
|
854
|
+
async #send(call, signal) {
|
|
855
|
+
const token = await this.#token(false);
|
|
856
|
+
let response = await this.#dispatch(call, token, signal);
|
|
857
|
+
if (response.status === 401 && this.#tokenProvider !== void 0) {
|
|
858
|
+
const fresh = await this.#token(true);
|
|
859
|
+
if (fresh !== null && fresh !== token) {
|
|
860
|
+
await response.body?.cancel();
|
|
861
|
+
response = await this.#dispatch(call, fresh, signal);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
if (response.status >= 400) throw await problemFromResponse(response);
|
|
865
|
+
return response;
|
|
866
|
+
}
|
|
867
|
+
async #getJson(url, catalog) {
|
|
868
|
+
const call = {
|
|
869
|
+
method: "GET",
|
|
870
|
+
url,
|
|
871
|
+
op: "manifest",
|
|
872
|
+
resource: void 0,
|
|
873
|
+
recordId: void 0,
|
|
874
|
+
apiVersion: null,
|
|
875
|
+
...catalog ? { headers: { "Cache-Control": "no-store" } } : {}
|
|
876
|
+
};
|
|
877
|
+
const response = await this.#send(call, void 0);
|
|
878
|
+
try {
|
|
879
|
+
return await response.json();
|
|
880
|
+
} catch (error) {
|
|
881
|
+
throw new ManifestError(`${url} is not JSON`, { cause: error });
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
/** The manifest for this caller, loaded once per caller scope (`refresh: true` reloads). */
|
|
885
|
+
async manifest(options = {}) {
|
|
886
|
+
const scope = await this.scope();
|
|
887
|
+
let loading = this.#manifests.get(scope);
|
|
888
|
+
if (loading === void 0 || options.refresh === true) {
|
|
889
|
+
const started = loadManifest((url, catalog) => this.#getJson(url, catalog), this.baseUrl, this.#manifestUrl);
|
|
890
|
+
loading = started;
|
|
891
|
+
if (!this.#manifests.has(scope)) this.#manifests.clear();
|
|
892
|
+
this.#manifests.set(scope, started);
|
|
893
|
+
started.catch(() => {
|
|
894
|
+
if (this.#manifests.get(scope) === started) this.#manifests.delete(scope);
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
return loading;
|
|
898
|
+
}
|
|
899
|
+
/** A handle for `name` (the highest `api_version` unless given). */
|
|
900
|
+
async resource(name, apiVersion) {
|
|
901
|
+
const resource = findResource(await this.manifest(), name, apiVersion);
|
|
902
|
+
if (resource === void 0) throw new UnknownResourceError(`the manifest has no resource "${name}"${apiVersion === void 0 ? "" : ` ${apiVersion}`} for this caller`);
|
|
903
|
+
return new ResourceClient(this.baseUrl, resource, this.#transport);
|
|
904
|
+
}
|
|
905
|
+
};
|
|
906
|
+
/** Creates a client; the manifest loads on first use. */
|
|
907
|
+
function createClient(options) {
|
|
908
|
+
return new CrudClient(options);
|
|
909
|
+
}
|
|
910
|
+
//#endregion
|
|
911
|
+
export { UnknownResourceError as A, NotFound as C, ResponseError as D, RateLimited as E, problemFromResponse as M, composeMiddleware as N, ServerError as O, ManifestError as S, ProblemError as T, ConnectionFailed as _, encodeParams as a, InvalidQueryError as b, CATALOG_PATH as c, loadManifest as d, manifestUrlFromCatalog as f, ClientError as g, CapabilityError as h, buildQuery as i, ValidationFailed as j, Unauthorized as k, PROFILE as l, BadRequest as m, ResourceClient as n, formatScalar as o, parseManifest as p, createClient as r, quote as s, CrudClient as t, findResource as u, CrudError as v, PreconditionFailed as w, Locked as x, Forbidden as y };
|
|
912
|
+
|
|
913
|
+
//# sourceMappingURL=client-D0drumeA.mjs.map
|