@panneau/protocol 5.0.0-alpha.3 → 5.0.0-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/conformance/index.d.ts.map +1 -1
- package/dist/conformance/index.js +151 -1
- package/dist/conformance/index.js.map +1 -1
- package/dist/constants.d.ts +2 -2
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +3 -1
- package/dist/constants.js.map +1 -1
- package/dist/index.js +5 -2
- package/dist/libraries.d.ts +16 -0
- package/dist/libraries.d.ts.map +1 -0
- package/dist/libraries.js +15 -0
- package/dist/libraries.js.map +1 -0
- package/dist/lint.d.ts +2 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +34 -2
- package/dist/lint.js.map +1 -1
- package/dist/mock/example.d.ts +2 -1
- package/dist/mock/example.d.ts.map +1 -1
- package/dist/mock/example.js +227 -143
- package/dist/mock/example.js.map +1 -1
- package/dist/mock/index.d.ts +9 -1
- package/dist/mock/index.d.ts.map +1 -1
- package/dist/mock/index.js +257 -11
- package/dist/mock/index.js.map +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/openapi.js +55 -101
- package/dist/openapi.js.map +1 -1
- package/dist/rules.d.ts.map +1 -1
- package/dist/rules.js +17 -0
- package/dist/rules.js.map +1 -1
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +4 -1
- package/dist/schemas/action.d.ts +17 -1
- package/dist/schemas/action.d.ts.map +1 -1
- package/dist/schemas/api.d.ts +155 -37
- package/dist/schemas/api.d.ts.map +1 -1
- package/dist/schemas/api.js +89 -28
- package/dist/schemas/api.js.map +1 -1
- package/dist/schemas/definition.d.ts +39 -2
- package/dist/schemas/definition.d.ts.map +1 -1
- package/dist/schemas/definition.js +4 -3
- package/dist/schemas/definition.js.map +1 -1
- package/dist/schemas/field.d.ts +24 -1
- package/dist/schemas/field.d.ts.map +1 -1
- package/dist/schemas/field.js +22 -4
- package/dist/schemas/field.js.map +1 -1
- package/dist/schemas/index.d.ts +262 -40
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +13 -5
- package/dist/schemas/index.js.map +1 -1
- package/dist/schemas/resource.d.ts +37 -2
- package/dist/schemas/resource.d.ts.map +1 -1
- package/dist/schemas/resource.js +4 -1
- package/dist/schemas/resource.js.map +1 -1
- package/dist/search.d.ts +51 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +53 -0
- package/dist/search.js.map +1 -0
- package/dist/transforms.d.ts +17 -0
- package/dist/transforms.d.ts.map +1 -0
- package/dist/transforms.js +34 -0
- package/dist/transforms.js.map +1 -0
- package/package.json +1 -1
- package/schema/boot.schema.json +84 -1
- package/schema/definition.schema.json +84 -1
- package/schema/openapi.json +771 -605
package/dist/mock/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { PROBLEM_CONTENT_TYPE } from "../version.js";
|
|
2
2
|
import { isRecord } from "../values.js";
|
|
3
3
|
import { checkValues } from "../rules.js";
|
|
4
|
+
import { getMediaLibraries } from "../libraries.js";
|
|
4
5
|
import { fromSearchParams, isFilterRange, matchesFilterRange } from "../query.js";
|
|
6
|
+
import { readSearchQuery } from "../search.js";
|
|
5
7
|
import { createExampleDefinition } from "./example.js";
|
|
6
8
|
//#region src/mock/index.ts
|
|
7
9
|
var RESERVED_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -9,6 +11,15 @@ var RESERVED_KEYS = /* @__PURE__ */ new Set([
|
|
|
9
11
|
"version",
|
|
10
12
|
"abilities"
|
|
11
13
|
]);
|
|
14
|
+
function escapeHtml(text) {
|
|
15
|
+
return text.replace(/[&<>"']/g, (character) => `&#${character.charCodeAt(0)};`);
|
|
16
|
+
}
|
|
17
|
+
/** A plain page with the values of an item, as a `data:` URL, for iframe previews. */
|
|
18
|
+
function defaultPreviewUrl(item) {
|
|
19
|
+
const rows = Object.entries(item).filter(([key]) => !RESERVED_KEYS.has(key)).map(([key, value]) => `<dt>${escapeHtml(key)}</dt><dd>${escapeHtml(typeof value === "string" ? value : JSON.stringify(value))}</dd>`).join("");
|
|
20
|
+
const html = `<!doctype html><meta charset="utf-8"><title>Preview</title><body style="font-family:system-ui;margin:2rem"><h1>Item ${escapeHtml(item.id)}</h1><dl>${rows}</dl></body>`;
|
|
21
|
+
return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
|
|
22
|
+
}
|
|
12
23
|
var DEFAULT_MESSAGES = {
|
|
13
24
|
required: () => "This field is required.",
|
|
14
25
|
min: (failure) => `This value must be at least ${String(failure.value)}.`,
|
|
@@ -20,7 +31,11 @@ var DEFAULT_MESSAGES = {
|
|
|
20
31
|
accept: () => "This file type is not accepted.",
|
|
21
32
|
maxSize: () => "This file is too large.",
|
|
22
33
|
before: () => "This date is too late.",
|
|
23
|
-
after: () => "This date is too early."
|
|
34
|
+
after: () => "This date is too early.",
|
|
35
|
+
crop: (failure) => {
|
|
36
|
+
const ratios = isRecord(failure.value) ? failure.value.ratios : void 0;
|
|
37
|
+
return Array.isArray(ratios) && ratios.length > 0 ? `Crop this image to ${ratios.join(" or ")}.` : "Crop this image.";
|
|
38
|
+
}
|
|
24
39
|
};
|
|
25
40
|
function message(failure) {
|
|
26
41
|
return failure.message ?? DEFAULT_MESSAGES[failure.rule]?.(failure) ?? "This value is invalid.";
|
|
@@ -74,7 +89,7 @@ function applyQuery(items, query) {
|
|
|
74
89
|
}
|
|
75
90
|
const accepted = (Array.isArray(expected) ? expected : [expected]).map(String);
|
|
76
91
|
result = result.filter((item) => {
|
|
77
|
-
const value = item[key];
|
|
92
|
+
const value = key in item ? item[key] : isRecord(item.media) ? item.media[key] : void 0;
|
|
78
93
|
return (Array.isArray(value) ? value : [value]).some((candidate) => accepted.includes(String(candidate)));
|
|
79
94
|
});
|
|
80
95
|
});
|
|
@@ -94,13 +109,47 @@ function applyQuery(items, query) {
|
|
|
94
109
|
});
|
|
95
110
|
return result;
|
|
96
111
|
}
|
|
112
|
+
/** The type of a media, from its MIME type. */
|
|
113
|
+
function mediaType(mime) {
|
|
114
|
+
const family = (mime ?? "").split("/")[0] ?? "";
|
|
115
|
+
if (family === "image" || family === "video" || family === "audio") return family;
|
|
116
|
+
return mime === "application/pdf" || family === "text" ? "document" : "other";
|
|
117
|
+
}
|
|
118
|
+
/** What the mock computes about a stored file: no renditions, ready at once. */
|
|
119
|
+
function mediaOf(file) {
|
|
120
|
+
const type = mediaType(file.mime);
|
|
121
|
+
return {
|
|
122
|
+
type,
|
|
123
|
+
mime: file.mime ?? null,
|
|
124
|
+
size: file.size ?? null,
|
|
125
|
+
url: file.url ?? null,
|
|
126
|
+
thumbnailUrl: type === "image" ? file.url ?? null : null,
|
|
127
|
+
status: "ready"
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* A media item with its own transform (its `transform` field) in its `media`, where references
|
|
132
|
+
* give it to clients (decision 0031).
|
|
133
|
+
*/
|
|
134
|
+
function withOwnTransform(resource, item) {
|
|
135
|
+
if (resource.kind !== "media" || !isRecord(item.media)) return item;
|
|
136
|
+
const { transform: _previous, ...media } = item.media;
|
|
137
|
+
return {
|
|
138
|
+
...item,
|
|
139
|
+
media: isRecord(item.transform) ? {
|
|
140
|
+
...media,
|
|
141
|
+
transform: item.transform
|
|
142
|
+
} : media
|
|
143
|
+
};
|
|
144
|
+
}
|
|
97
145
|
/**
|
|
98
146
|
* An in-memory backend that implements the protocol's resource routes: definition, list, show,
|
|
99
147
|
* create, update (with `If-Match`), delete and the built-in `delete` action. Values are checked
|
|
100
148
|
* with the portable rules. Used to test the conformance kit, and to develop frontends without a
|
|
101
149
|
* real backend.
|
|
102
150
|
*/
|
|
103
|
-
function createMockBackend(
|
|
151
|
+
function createMockBackend(options) {
|
|
152
|
+
const { definition, items = {} } = options;
|
|
104
153
|
const store = /* @__PURE__ */ new Map();
|
|
105
154
|
let nextId = 1;
|
|
106
155
|
const abilitiesFor = (resource) => Object.fromEntries([
|
|
@@ -147,6 +196,150 @@ function createMockBackend({ definition, items = {} }) {
|
|
|
147
196
|
if (q !== void 0) query.q = q;
|
|
148
197
|
return applyQuery(list, query).filter((item) => !except.includes(item.id));
|
|
149
198
|
};
|
|
199
|
+
const uploads = /* @__PURE__ */ new Map();
|
|
200
|
+
let nextUpload = 1;
|
|
201
|
+
const handleUploads = async (segments, method, readBody) => {
|
|
202
|
+
if (definition.uploads?.mode !== "presigned") return problem(404, "Not found.");
|
|
203
|
+
if (method !== "POST") return problem(405, "Method not allowed.");
|
|
204
|
+
if (segments.length === 2 && segments[1] === "sign") {
|
|
205
|
+
const body = await readBody();
|
|
206
|
+
if (body.uploadId === void 0 && (body.method === "POST" || body.method === "PUT")) {
|
|
207
|
+
const id = `upl_${nextUpload++}`;
|
|
208
|
+
const upload = {
|
|
209
|
+
id,
|
|
210
|
+
key: `uploads/${id}`
|
|
211
|
+
};
|
|
212
|
+
uploads.set(id, upload);
|
|
213
|
+
return json(200, {
|
|
214
|
+
url: `https://storage.test/${upload.key}?signature=mock`,
|
|
215
|
+
key: upload.key,
|
|
216
|
+
upload: id
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
const upload = [...uploads.values()].find((candidate) => candidate.key === body.key);
|
|
220
|
+
if (upload === void 0) return problem(404, "Upload not found.");
|
|
221
|
+
return json(200, { url: `https://storage.test/${upload.key}?signature=mock` });
|
|
222
|
+
}
|
|
223
|
+
if (segments.length === 3 && segments[2] === "complete" && segments[1] !== void 0) {
|
|
224
|
+
const upload = uploads.get(segments[1]);
|
|
225
|
+
if (upload === void 0) return problem(404, "Upload not found.");
|
|
226
|
+
const { name } = await readBody();
|
|
227
|
+
if (typeof name === "string") upload.name = name;
|
|
228
|
+
return json(200, { data: upload });
|
|
229
|
+
}
|
|
230
|
+
return problem(404, "Not found.");
|
|
231
|
+
};
|
|
232
|
+
const resolveFiles = (resource, values) => {
|
|
233
|
+
const missing = [];
|
|
234
|
+
const resolved = { ...values };
|
|
235
|
+
const toFile = (value, path) => {
|
|
236
|
+
if (!isRecord(value) || typeof value.upload !== "string") return value;
|
|
237
|
+
const upload = uploads.get(value.upload);
|
|
238
|
+
if (upload === void 0) {
|
|
239
|
+
missing.push(path);
|
|
240
|
+
return value;
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
name: upload.name ?? (typeof value.name === "string" ? value.name : upload.id),
|
|
244
|
+
url: `https://storage.test/${upload.key ?? upload.id}`,
|
|
245
|
+
mime: upload.mime ?? (typeof value.mime === "string" ? value.mime : null),
|
|
246
|
+
size: upload.size ?? (typeof value.size === "number" ? value.size : null)
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
resource.fields.filter((field) => field.type === "file" && field.name in values).forEach((field) => {
|
|
250
|
+
const value = values[field.name];
|
|
251
|
+
resolved[field.name] = Array.isArray(value) ? value.map((entry, index) => toFile(entry, `${field.name}.${String(index)}`)) : toFile(value, field.name);
|
|
252
|
+
});
|
|
253
|
+
return {
|
|
254
|
+
values: resolved,
|
|
255
|
+
missing
|
|
256
|
+
};
|
|
257
|
+
};
|
|
258
|
+
const missingMedia = (paths) => problem(422, "The given data was invalid.", {
|
|
259
|
+
type: "urn:panneau:problem:validation",
|
|
260
|
+
errors: Object.fromEntries(Object.entries(paths).map(([path, message]) => [path, [message]]))
|
|
261
|
+
});
|
|
262
|
+
const missingUploads = (paths) => problem(422, "The given data was invalid.", {
|
|
263
|
+
type: "urn:panneau:problem:validation",
|
|
264
|
+
errors: Object.fromEntries(paths.map((path) => [path, ["This upload does not exist."]]))
|
|
265
|
+
});
|
|
266
|
+
const resolveMedia = (resource, values) => {
|
|
267
|
+
const missing = {};
|
|
268
|
+
const resolved = { ...values };
|
|
269
|
+
resource.fields.filter((field) => (field.type === "media" || field.type === "image") && field.name in values).forEach((field) => {
|
|
270
|
+
const allowed = getMediaLibraries(definition, field);
|
|
271
|
+
const toReference = (value, path) => {
|
|
272
|
+
const id = isRecord(value) ? value.id : value;
|
|
273
|
+
if (typeof id !== "string" && typeof id !== "number") return value;
|
|
274
|
+
const named = isRecord(value) && typeof value.resource === "string" ? value.resource : void 0;
|
|
275
|
+
if (named === void 0 && allowed.length > 1) {
|
|
276
|
+
missing[path] = "This field allows several libraries: name the library.";
|
|
277
|
+
return value;
|
|
278
|
+
}
|
|
279
|
+
const libraryId = named ?? allowed[0];
|
|
280
|
+
const library = definition.resources.find((candidate) => candidate.id === libraryId && allowed.includes(candidate.id));
|
|
281
|
+
const media = (store.get(library?.id ?? "") ?? []).find((candidate) => candidate.id === String(id));
|
|
282
|
+
if (media === void 0 || library === void 0) {
|
|
283
|
+
missing[path] = "This media does not exist.";
|
|
284
|
+
return value;
|
|
285
|
+
}
|
|
286
|
+
const title = media[library.itemTitle ?? "name"];
|
|
287
|
+
return {
|
|
288
|
+
id: media.id,
|
|
289
|
+
...allowed.length > 1 ? { resource: library.id } : {},
|
|
290
|
+
...typeof title === "string" ? { label: title } : {},
|
|
291
|
+
...isRecord(media.media) ? { media: media.media } : {},
|
|
292
|
+
...isRecord(value) && isRecord(value.transform) ? { transform: value.transform } : {}
|
|
293
|
+
};
|
|
294
|
+
};
|
|
295
|
+
const value = values[field.name];
|
|
296
|
+
resolved[field.name] = Array.isArray(value) ? value.map((entry, index) => toReference(entry, `${field.name}.${String(index)}`)) : toReference(value, field.name);
|
|
297
|
+
});
|
|
298
|
+
return {
|
|
299
|
+
values: resolved,
|
|
300
|
+
missing
|
|
301
|
+
};
|
|
302
|
+
};
|
|
303
|
+
const resolveValues = (resource, body) => {
|
|
304
|
+
const sent = { ...body };
|
|
305
|
+
if (resource.kind === "media") delete sent.media;
|
|
306
|
+
const files = resolveFiles(resource, sent);
|
|
307
|
+
if (files.missing.length > 0) return {
|
|
308
|
+
values: body,
|
|
309
|
+
problem: missingUploads(files.missing)
|
|
310
|
+
};
|
|
311
|
+
const media = resolveMedia(resource, files.values);
|
|
312
|
+
if (Object.keys(media.missing).length > 0) return {
|
|
313
|
+
values: body,
|
|
314
|
+
problem: missingMedia(media.missing)
|
|
315
|
+
};
|
|
316
|
+
const values = media.values;
|
|
317
|
+
if (resource.kind === "media" && isRecord(values.file)) values.media = mediaOf(values.file);
|
|
318
|
+
return {
|
|
319
|
+
values,
|
|
320
|
+
problem: null
|
|
321
|
+
};
|
|
322
|
+
};
|
|
323
|
+
const search = (params) => {
|
|
324
|
+
const read = readSearchQuery(params, definition);
|
|
325
|
+
if (read.errors !== void 0) return problem(422, "The given data was invalid.", {
|
|
326
|
+
type: "urn:panneau:problem:validation",
|
|
327
|
+
errors: read.errors
|
|
328
|
+
});
|
|
329
|
+
const { q, limit, resources } = read.query;
|
|
330
|
+
let left = 20;
|
|
331
|
+
return json(200, { data: resources.flatMap((id) => {
|
|
332
|
+
if (left === 0) return [];
|
|
333
|
+
const found = applyQuery(store.get(id) ?? [], { q });
|
|
334
|
+
const items = found.slice(0, Math.min(limit, left));
|
|
335
|
+
left -= items.length;
|
|
336
|
+
return items.length > 0 ? [{
|
|
337
|
+
resource: id,
|
|
338
|
+
items,
|
|
339
|
+
total: found.length
|
|
340
|
+
}] : [];
|
|
341
|
+
}) });
|
|
342
|
+
};
|
|
150
343
|
const handle = async (request) => {
|
|
151
344
|
const url = new URL(request.url, "http://panneau.test");
|
|
152
345
|
if (!url.pathname.startsWith(basePath)) return problem(404, "Not found.");
|
|
@@ -157,6 +350,8 @@ function createMockBackend({ definition, items = {} }) {
|
|
|
157
350
|
return isRecord(body) ? body : {};
|
|
158
351
|
};
|
|
159
352
|
if (segments.length === 1 && segments[0] === "definition" && method === "GET") return json(200, definition);
|
|
353
|
+
if (segments.length === 1 && segments[0] === "search" && method === "GET") return search(url.searchParams);
|
|
354
|
+
if (segments[0] === "uploads") return handleUploads(segments, method, readBody);
|
|
160
355
|
if (segments[0] !== "resources" || segments[1] === void 0) return problem(404, "Not found.");
|
|
161
356
|
const resource = definition.resources.find((candidate) => candidate.id === segments[1]);
|
|
162
357
|
const list = resource !== void 0 ? store.get(resource.id) : void 0;
|
|
@@ -182,18 +377,50 @@ function createMockBackend({ definition, items = {} }) {
|
|
|
182
377
|
});
|
|
183
378
|
}
|
|
184
379
|
if (method === "POST") {
|
|
185
|
-
const
|
|
186
|
-
const invalid = validationProblem(resource,
|
|
380
|
+
const body = await readBody();
|
|
381
|
+
const invalid = validationProblem(resource, body, "create");
|
|
187
382
|
if (invalid !== null) return invalid;
|
|
188
|
-
const
|
|
383
|
+
const { values, problem: unresolved } = resolveValues(resource, body);
|
|
384
|
+
if (unresolved !== null) return unresolved;
|
|
385
|
+
const item = withOwnTransform(resource, makeItem(resource, {
|
|
189
386
|
...values,
|
|
190
387
|
id: void 0
|
|
191
|
-
});
|
|
388
|
+
}));
|
|
192
389
|
list.push(item);
|
|
193
390
|
return json(201, { data: item }, etag(item));
|
|
194
391
|
}
|
|
195
392
|
return problem(405, "Method not allowed.");
|
|
196
393
|
}
|
|
394
|
+
if (segments[2] === "fields" && segments[4] === "check" && segments.length === 5) {
|
|
395
|
+
if (method !== "POST") return problem(405, "Method not allowed.");
|
|
396
|
+
const field = resource.fields.find((candidate) => candidate.name === segments[3] && candidate.check === true);
|
|
397
|
+
if (field === void 0) return problem(404, "Field not found.");
|
|
398
|
+
const body = await readBody();
|
|
399
|
+
if (typeof body.value !== "string") return problem(422, "The given data was invalid.", {
|
|
400
|
+
type: "urn:panneau:problem:validation",
|
|
401
|
+
errors: { value: ["The value is required."] }
|
|
402
|
+
});
|
|
403
|
+
const locale = typeof body.locale === "string" ? body.locale : null;
|
|
404
|
+
const id = typeof body.id === "string" || typeof body.id === "number" ? String(body.id) : null;
|
|
405
|
+
const valueOf = (item) => {
|
|
406
|
+
const value = item[field.name];
|
|
407
|
+
return locale !== null && isRecord(value) ? value[locale] : value;
|
|
408
|
+
};
|
|
409
|
+
const taken = (candidate) => list.some((item) => item.id !== id && valueOf(item) === candidate);
|
|
410
|
+
const available = !taken(body.value);
|
|
411
|
+
let suggestion = null;
|
|
412
|
+
for (let index = 2; !available && suggestion === null; index += 1) {
|
|
413
|
+
const candidate = `${body.value}-${String(index)}`;
|
|
414
|
+
suggestion = taken(candidate) ? null : candidate;
|
|
415
|
+
}
|
|
416
|
+
const prefix = typeof field.settings?.prefix === "string" ? field.settings.prefix : null;
|
|
417
|
+
return json(200, {
|
|
418
|
+
available,
|
|
419
|
+
suggestion,
|
|
420
|
+
prefix,
|
|
421
|
+
url: prefix !== null ? `${prefix}${body.value}` : null
|
|
422
|
+
});
|
|
423
|
+
}
|
|
197
424
|
if (segments[2] === "actions" && segments[3] !== void 0 && segments.length === 4) {
|
|
198
425
|
if (method !== "POST") return problem(405, "Method not allowed.");
|
|
199
426
|
const actionId = segments[3];
|
|
@@ -229,6 +456,23 @@ function createMockBackend({ definition, items = {} }) {
|
|
|
229
456
|
refresh: "list"
|
|
230
457
|
});
|
|
231
458
|
}
|
|
459
|
+
if (segments.length === 4 && segments[3] === "preview" && segments[2] !== void 0) {
|
|
460
|
+
const current = list.find((item) => item.id === segments[2]);
|
|
461
|
+
if (current === void 0) return problem(404, "Item not found.");
|
|
462
|
+
if (method !== "POST") return problem(405, "Method not allowed.");
|
|
463
|
+
const body = await readBody();
|
|
464
|
+
const item = {
|
|
465
|
+
...current,
|
|
466
|
+
...isRecord(body) ? body : {}
|
|
467
|
+
};
|
|
468
|
+
return json(200, {
|
|
469
|
+
url: options.previewUrl?.({
|
|
470
|
+
resource: resource.id,
|
|
471
|
+
item
|
|
472
|
+
}) ?? defaultPreviewUrl(item),
|
|
473
|
+
expiresAt: new Date(Date.now() + 3e5).toISOString()
|
|
474
|
+
});
|
|
475
|
+
}
|
|
232
476
|
if (segments.length !== 3 || segments[2] === void 0) return problem(404, "Not found.");
|
|
233
477
|
const id = segments[2];
|
|
234
478
|
const index = list.findIndex((item) => item.id === id);
|
|
@@ -241,15 +485,17 @@ function createMockBackend({ definition, items = {} }) {
|
|
|
241
485
|
type: "urn:panneau:problem:version-conflict",
|
|
242
486
|
current
|
|
243
487
|
});
|
|
244
|
-
const
|
|
245
|
-
const invalid = validationProblem(resource,
|
|
488
|
+
const body = await readBody();
|
|
489
|
+
const invalid = validationProblem(resource, body, "update");
|
|
246
490
|
if (invalid !== null) return invalid;
|
|
491
|
+
const { values, problem: unresolved } = resolveValues(resource, body);
|
|
492
|
+
if (unresolved !== null) return unresolved;
|
|
247
493
|
const updates = Object.fromEntries(Object.entries(values).filter(([key]) => !RESERVED_KEYS.has(key)));
|
|
248
|
-
const updated = {
|
|
494
|
+
const updated = withOwnTransform(resource, {
|
|
249
495
|
...current,
|
|
250
496
|
...updates,
|
|
251
497
|
version: current.version === null ? null : String(Number(current.version) + 1)
|
|
252
|
-
};
|
|
498
|
+
});
|
|
253
499
|
list[index] = updated;
|
|
254
500
|
return json(200, { data: updated }, etag(updated));
|
|
255
501
|
}
|
package/dist/mock/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/mock/index.ts"],"sourcesContent":["import { type ListQuery, fromSearchParams, isFilterRange, matchesFilterRange } from '../query';\nimport { type RuleFailure, checkValues } from '../rules';\nimport type { DefinitionInput, ResourceInput } from '../schemas';\nimport type { ActionResult, Item, Problem, Selection } from '../schemas/api';\nimport { isRecord } from '../values';\nimport { PROBLEM_CONTENT_TYPE } from '../version';\n\ntype Values = Record<string, unknown>;\n\nexport interface MockBackendOptions {\n /** The definition served by `GET /definition`. Its `api.baseUrl` is the prefix of every route. */\n definition: DefinitionInput;\n /** Initial values of the items, per resource id. Items without an `id` get one. */\n items?: Record<string, Values[]>;\n}\n\nexport interface MockBackend {\n /** A `fetch`-compatible function that answers protocol requests. */\n fetch: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;\n /** The current items of a resource. */\n items: (resource: string) => Item[];\n}\n\nconst RESERVED_KEYS = new Set(['id', 'version', 'abilities']);\n\nconst DEFAULT_MESSAGES: Record<string, (failure: RuleFailure) => string> = {\n required: () => 'This field is required.',\n min: (failure) => `This value must be at least ${String(failure.value)}.`,\n max: (failure) => `This value must be at most ${String(failure.value)}.`,\n pattern: () => 'This value has an invalid format.',\n email: () => 'This value must be an email address.',\n url: () => 'This value must be a URL.',\n in: () => 'This value is not one of the choices.',\n accept: () => 'This file type is not accepted.',\n maxSize: () => 'This file is too large.',\n before: () => 'This date is too late.',\n after: () => 'This date is too early.',\n};\n\nfunction message(failure: RuleFailure): string {\n return failure.message ?? DEFAULT_MESSAGES[failure.rule]?.(failure) ?? 'This value is invalid.';\n}\n\nfunction json(status: number, body: unknown, headers: Record<string, string> = {}): Response {\n return new Response(status === 204 ? null : JSON.stringify(body), {\n status,\n headers: status === 204 ? headers : { 'Content-Type': 'application/json', ...headers },\n });\n}\n\nfunction problem(status: number, title: string, extra: Partial<Problem> = {}): Response {\n const body: Problem = { type: 'about:blank', title, status, ...extra };\n return new Response(JSON.stringify(body), {\n status,\n headers: { 'Content-Type': PROBLEM_CONTENT_TYPE },\n });\n}\n\nfunction etag(item: Item): Record<string, string> {\n return item.version === null ? {} : { ETag: `\"${item.version}\"` };\n}\n\nfunction parseEtag(value: string): string {\n return value.replace(/^W\\//, '').replace(/^\"(.*)\"$/, '$1');\n}\n\nfunction searchableText(value: unknown): string {\n if (typeof value === 'string') {\n return value;\n }\n if (isRecord(value)) {\n return Object.values(value).map(searchableText).join(' ');\n }\n return '';\n}\n\nfunction compare(a: unknown, b: unknown): number {\n const left = isRecord(a) ? searchableText(a) : a;\n const right = isRecord(b) ? searchableText(b) : b;\n if (left === right) {\n return 0;\n }\n if (left === undefined || left === null) {\n return 1;\n }\n if (right === undefined || right === null) {\n return -1;\n }\n return String(left).localeCompare(String(right), undefined, { numeric: true });\n}\n\nfunction applyQuery(items: Item[], query: Pick<ListQuery, 'filter' | 'q' | 'sort'>): Item[] {\n let result = items;\n Object.entries(query.filter ?? {}).forEach(([key, expected]) => {\n if (isFilterRange(expected)) {\n result = result.filter((item) => matchesFilterRange(item[key], expected));\n return;\n }\n const accepted = (Array.isArray(expected) ? expected : [expected]).map(String);\n result = result.filter((item) => {\n const value = item[key];\n const values = Array.isArray(value) ? value : [value];\n return values.some((candidate) => accepted.includes(String(candidate)));\n });\n });\n if (query.q !== undefined && query.q !== '') {\n const terms = query.q.toLowerCase();\n result = result.filter((item) =>\n Object.entries(item)\n .filter(([key]) => !RESERVED_KEYS.has(key))\n .some(([, value]) => searchableText(value).toLowerCase().includes(terms)),\n );\n }\n const sort = query.sort ?? [];\n if (sort.length > 0) {\n result = [...result].sort((a, b) => {\n for (const part of sort) {\n const descending = part.startsWith('-');\n const key = descending ? part.slice(1) : part;\n const order = compare(a[key], b[key]);\n if (order !== 0) {\n return descending ? -order : order;\n }\n }\n return 0;\n });\n }\n return result;\n}\n\n/**\n * An in-memory backend that implements the protocol's resource routes: definition, list, show,\n * create, update (with `If-Match`), delete and the built-in `delete` action. Values are checked\n * with the portable rules. Used to test the conformance kit, and to develop frontends without a\n * real backend.\n */\nexport function createMockBackend({ definition, items = {} }: MockBackendOptions): MockBackend {\n const store = new Map<string, Item[]>();\n let nextId = 1;\n\n const abilitiesFor = (resource: ResourceInput) =>\n Object.fromEntries([\n ['update', true],\n ['delete', true],\n ...(resource.actions ?? []).map((action) => [action.id, true]),\n ]);\n\n const makeItem = (resource: ResourceInput, values: Values): Item => {\n const id =\n typeof values.id === 'string' || typeof values.id === 'number'\n ? String(values.id)\n : String(nextId++);\n const rest = Object.fromEntries(\n Object.entries(values).filter(([key]) => !RESERVED_KEYS.has(key)),\n );\n return { ...rest, id, version: '1', abilities: abilitiesFor(resource) };\n };\n\n definition.resources.forEach((resource) => {\n store.set(\n resource.id,\n (items[resource.id] ?? []).map((values) => makeItem(resource, values)),\n );\n });\n\n const basePath = new URL(definition.api.baseUrl, 'http://panneau.test').pathname.replace(\n /\\/$/,\n '',\n );\n\n const validationProblem = (\n resource: ResourceInput,\n values: Values,\n mode: 'create' | 'update',\n ) => {\n const failures = checkValues(resource.fields, values, {\n mode,\n partial: mode === 'update',\n locales: definition.locales,\n });\n const paths = Object.keys(failures);\n if (paths.length === 0) {\n return null;\n }\n return problem(422, 'The given data was invalid.', {\n type: 'urn:panneau:problem:validation',\n errors: Object.fromEntries(\n paths.map((path) => [path, (failures[path] ?? []).map(message)]),\n ),\n });\n };\n\n const selectItems = (list: Item[], selection: Selection | undefined): Item[] => {\n if (selection === undefined) {\n return [];\n }\n if ('ids' in selection) {\n const ids = selection.ids as string[];\n return list.filter((item) => ids.includes(item.id));\n }\n const except = selection.except ?? [];\n const { filter, q } = selection.query;\n const query: Pick<ListQuery, 'filter' | 'q'> = {};\n if (filter !== undefined) {\n query.filter = filter as NonNullable<ListQuery['filter']>;\n }\n if (q !== undefined) {\n query.q = q;\n }\n return applyQuery(list, query).filter((item) => !except.includes(item.id));\n };\n\n const handle = async (request: Request): Promise<Response> => {\n const url = new URL(request.url, 'http://panneau.test');\n if (!url.pathname.startsWith(basePath)) {\n return problem(404, 'Not found.');\n }\n const segments = url.pathname\n .slice(basePath.length)\n .split('/')\n .filter((segment) => segment !== '')\n .map(decodeURIComponent);\n const method = request.method.toUpperCase();\n const readBody = async (): Promise<Values> => {\n const body: unknown = await request.json().catch(() => null);\n return isRecord(body) ? body : {};\n };\n\n if (segments.length === 1 && segments[0] === 'definition' && method === 'GET') {\n return json(200, definition);\n }\n if (segments[0] !== 'resources' || segments[1] === undefined) {\n return problem(404, 'Not found.');\n }\n\n const resource = definition.resources.find((candidate) => candidate.id === segments[1]);\n const list = resource !== undefined ? store.get(resource.id) : undefined;\n if (resource === undefined || list === undefined) {\n return problem(404, 'Resource not found.');\n }\n\n if (segments.length === 2) {\n if (method === 'GET') {\n const query = fromSearchParams(url.searchParams);\n const perPage = query.perPage ?? resource.list?.perPage ?? 20;\n const page = query.page ?? 1;\n const sort = query.sort ?? resource.list?.defaultSort?.split(',');\n const matching = applyQuery(list, {\n ...query,\n ...(sort !== undefined ? { sort } : {}),\n });\n return json(200, {\n data: matching.slice((page - 1) * perPage, page * perPage),\n meta: {\n page,\n perPage,\n total: matching.length,\n lastPage: Math.max(1, Math.ceil(matching.length / perPage)),\n },\n });\n }\n if (method === 'POST') {\n const values = await readBody();\n const invalid = validationProblem(resource, values, 'create');\n if (invalid !== null) {\n return invalid;\n }\n const item = makeItem(resource, { ...values, id: undefined });\n list.push(item);\n return json(201, { data: item }, etag(item));\n }\n return problem(405, 'Method not allowed.');\n }\n\n if (segments[2] === 'actions' && segments[3] !== undefined && segments.length === 4) {\n if (method !== 'POST') {\n return problem(405, 'Method not allowed.');\n }\n const actionId = segments[3];\n const action = (resource.actions ?? []).find((candidate) => candidate.id === actionId);\n const body = await readBody();\n if (body.dryRun === true) {\n // A dry run counts what the action would do, and changes nothing.\n if (action?.dryRun !== true) {\n return problem(400, 'This action does not support dry runs.');\n }\n const selected = selectItems(list, body.selection as Selection | undefined);\n const allowed = selected.filter((item) => item.abilities?.[actionId] !== false);\n const result: ActionResult = {\n summary: {\n succeeded: allowed.length,\n failed: 0,\n skipped: selected.length - allowed.length,\n },\n results: selected\n .filter((item) => !allowed.includes(item))\n .slice(0, 20)\n .map((item) => ({ id: item.id, status: 'skipped', reason: 'forbidden' })),\n };\n return json(200, result);\n }\n if (actionId !== 'delete') {\n return problem(404, 'Action not found.');\n }\n const selected = selectItems(list, body.selection as Selection | undefined);\n store.set(\n resource.id,\n list.filter((item) => !selected.includes(item)),\n );\n const result: ActionResult = {\n message: `${selected.length} deleted.`,\n summary: { succeeded: selected.length, failed: 0, skipped: 0 },\n refresh: 'list',\n };\n return json(200, result);\n }\n\n if (segments.length !== 3 || segments[2] === undefined) {\n return problem(404, 'Not found.');\n }\n const id = segments[2];\n const index = list.findIndex((item) => item.id === id);\n const current = list[index];\n if (current === undefined) {\n return problem(404, 'Item not found.');\n }\n\n if (method === 'GET') {\n return json(200, { data: current }, etag(current));\n }\n if (method === 'PATCH') {\n const ifMatch = request.headers.get('If-Match');\n if (\n ifMatch !== null &&\n current.version !== null &&\n parseEtag(ifMatch) !== current.version\n ) {\n return problem(412, 'The item was changed by someone else.', {\n type: 'urn:panneau:problem:version-conflict',\n current,\n });\n }\n const values = await readBody();\n const invalid = validationProblem(resource, values, 'update');\n if (invalid !== null) {\n return invalid;\n }\n const updates = Object.fromEntries(\n Object.entries(values).filter(([key]) => !RESERVED_KEYS.has(key)),\n );\n const updated: Item = {\n ...current,\n ...updates,\n version: current.version === null ? null : String(Number(current.version) + 1),\n };\n list[index] = updated;\n return json(200, { data: updated }, etag(updated));\n }\n if (method === 'DELETE') {\n list.splice(index, 1);\n return json(204, null);\n }\n return problem(405, 'Method not allowed.');\n };\n\n return {\n fetch: (input, init) => {\n if (input instanceof Request) {\n return handle(input);\n }\n // Relative URLs are accepted, as a browser would resolve them.\n return handle(new Request(new URL(input, 'http://panneau.test'), init));\n },\n items: (resource) => [...(store.get(resource) ?? [])],\n };\n}\n\nexport { createExampleDefinition } from './example';\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/mock/index.ts"],"sourcesContent":["import { getMediaLibraries } from '../libraries';\nimport { type ListQuery, fromSearchParams, isFilterRange, matchesFilterRange } from '../query';\nimport { type RuleFailure, checkValues } from '../rules';\nimport type { DefinitionInput, ResourceInput } from '../schemas';\nimport type {\n ActionResult,\n FieldCheckResponse,\n Item,\n MediaInfo,\n MediaReference,\n PreviewResponse,\n Problem,\n SearchResponse,\n Selection,\n StoredFile,\n Upload,\n UploadSignResponse,\n} from '../schemas/api';\nimport { SEARCH_MAX_LIMIT, readSearchQuery } from '../search';\nimport { isRecord } from '../values';\nimport { PROBLEM_CONTENT_TYPE } from '../version';\n\ntype Values = Record<string, unknown>;\n\nexport interface MockBackendOptions {\n /** The definition served by `GET /definition`. Its `api.baseUrl` is the prefix of every route. */\n definition: DefinitionInput;\n /** Initial values of the items, per resource id. Items without an `id` get one. */\n items?: Record<string, Values[]>;\n /**\n * The URL that `POST /resources/{resource}/{id}/preview` returns, for the item with the posted\n * values. Defaults to a `data:` URL of a plain page with the item's values.\n */\n previewUrl?: (context: { resource: string; item: Item }) => string;\n}\n\nexport interface MockBackend {\n /** A `fetch`-compatible function that answers protocol requests. */\n fetch: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;\n /** The current items of a resource. */\n items: (resource: string) => Item[];\n}\n\nconst RESERVED_KEYS = new Set(['id', 'version', 'abilities']);\n\nfunction escapeHtml(text: string): string {\n return text.replace(/[&<>\"']/g, (character) => `&#${character.charCodeAt(0)};`);\n}\n\n/** A plain page with the values of an item, as a `data:` URL, for iframe previews. */\nfunction defaultPreviewUrl(item: Item): string {\n const rows = Object.entries(item)\n .filter(([key]) => !RESERVED_KEYS.has(key))\n .map(\n ([key, value]) =>\n `<dt>${escapeHtml(key)}</dt><dd>${escapeHtml(typeof value === 'string' ? value : JSON.stringify(value))}</dd>`,\n )\n .join('');\n const html = `<!doctype html><meta charset=\"utf-8\"><title>Preview</title><body style=\"font-family:system-ui;margin:2rem\"><h1>Item ${escapeHtml(item.id)}</h1><dl>${rows}</dl></body>`;\n return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;\n}\n\nconst DEFAULT_MESSAGES: Record<string, (failure: RuleFailure) => string> = {\n required: () => 'This field is required.',\n min: (failure) => `This value must be at least ${String(failure.value)}.`,\n max: (failure) => `This value must be at most ${String(failure.value)}.`,\n pattern: () => 'This value has an invalid format.',\n email: () => 'This value must be an email address.',\n url: () => 'This value must be a URL.',\n in: () => 'This value is not one of the choices.',\n accept: () => 'This file type is not accepted.',\n maxSize: () => 'This file is too large.',\n before: () => 'This date is too late.',\n after: () => 'This date is too early.',\n crop: (failure) => {\n const ratios = isRecord(failure.value) ? failure.value.ratios : undefined;\n return Array.isArray(ratios) && ratios.length > 0\n ? `Crop this image to ${ratios.join(' or ')}.`\n : 'Crop this image.';\n },\n};\n\nfunction message(failure: RuleFailure): string {\n return failure.message ?? DEFAULT_MESSAGES[failure.rule]?.(failure) ?? 'This value is invalid.';\n}\n\nfunction json(status: number, body: unknown, headers: Record<string, string> = {}): Response {\n return new Response(status === 204 ? null : JSON.stringify(body), {\n status,\n headers: status === 204 ? headers : { 'Content-Type': 'application/json', ...headers },\n });\n}\n\nfunction problem(status: number, title: string, extra: Partial<Problem> = {}): Response {\n const body: Problem = { type: 'about:blank', title, status, ...extra };\n return new Response(JSON.stringify(body), {\n status,\n headers: { 'Content-Type': PROBLEM_CONTENT_TYPE },\n });\n}\n\nfunction etag(item: Item): Record<string, string> {\n return item.version === null ? {} : { ETag: `\"${item.version}\"` };\n}\n\nfunction parseEtag(value: string): string {\n return value.replace(/^W\\//, '').replace(/^\"(.*)\"$/, '$1');\n}\n\nfunction searchableText(value: unknown): string {\n if (typeof value === 'string') {\n return value;\n }\n if (isRecord(value)) {\n return Object.values(value).map(searchableText).join(' ');\n }\n return '';\n}\n\nfunction compare(a: unknown, b: unknown): number {\n const left = isRecord(a) ? searchableText(a) : a;\n const right = isRecord(b) ? searchableText(b) : b;\n if (left === right) {\n return 0;\n }\n if (left === undefined || left === null) {\n return 1;\n }\n if (right === undefined || right === null) {\n return -1;\n }\n return String(left).localeCompare(String(right), undefined, { numeric: true });\n}\n\nfunction applyQuery(items: Item[], query: Pick<ListQuery, 'filter' | 'q' | 'sort'>): Item[] {\n let result = items;\n Object.entries(query.filter ?? {}).forEach(([key, expected]) => {\n if (isFilterRange(expected)) {\n result = result.filter((item) => matchesFilterRange(item[key], expected));\n return;\n }\n const accepted = (Array.isArray(expected) ? expected : [expected]).map(String);\n result = result.filter((item) => {\n // A filter that isn't a value of the item reads its `media`: `filter[type]=image`.\n const value =\n key in item ? item[key] : isRecord(item.media) ? item.media[key] : undefined;\n const values = Array.isArray(value) ? value : [value];\n return values.some((candidate) => accepted.includes(String(candidate)));\n });\n });\n if (query.q !== undefined && query.q !== '') {\n const terms = query.q.toLowerCase();\n result = result.filter((item) =>\n Object.entries(item)\n .filter(([key]) => !RESERVED_KEYS.has(key))\n .some(([, value]) => searchableText(value).toLowerCase().includes(terms)),\n );\n }\n const sort = query.sort ?? [];\n if (sort.length > 0) {\n result = [...result].sort((a, b) => {\n for (const part of sort) {\n const descending = part.startsWith('-');\n const key = descending ? part.slice(1) : part;\n const order = compare(a[key], b[key]);\n if (order !== 0) {\n return descending ? -order : order;\n }\n }\n return 0;\n });\n }\n return result;\n}\n\n/** The type of a media, from its MIME type. */\nfunction mediaType(mime: string | null | undefined): string {\n const family = (mime ?? '').split('/')[0] ?? '';\n if (family === 'image' || family === 'video' || family === 'audio') {\n return family;\n }\n return mime === 'application/pdf' || family === 'text' ? 'document' : 'other';\n}\n\n/** What the mock computes about a stored file: no renditions, ready at once. */\nfunction mediaOf(file: StoredFile): MediaInfo {\n const type = mediaType(file.mime);\n return {\n type,\n mime: file.mime ?? null,\n size: file.size ?? null,\n url: file.url ?? null,\n thumbnailUrl: type === 'image' ? (file.url ?? null) : null,\n status: 'ready',\n };\n}\n\n/**\n * A media item with its own transform (its `transform` field) in its `media`, where references\n * give it to clients (decision 0031).\n */\nfunction withOwnTransform(resource: ResourceInput, item: Item): Item {\n if (resource.kind !== 'media' || !isRecord(item.media)) {\n return item;\n }\n const { transform: _previous, ...media } = item.media;\n return {\n ...item,\n media: isRecord(item.transform) ? { ...media, transform: item.transform } : media,\n };\n}\n\n/**\n * An in-memory backend that implements the protocol's resource routes: definition, list, show,\n * create, update (with `If-Match`), delete and the built-in `delete` action. Values are checked\n * with the portable rules. Used to test the conformance kit, and to develop frontends without a\n * real backend.\n */\nexport function createMockBackend(options: MockBackendOptions): MockBackend {\n const { definition, items = {} } = options;\n const store = new Map<string, Item[]>();\n let nextId = 1;\n\n const abilitiesFor = (resource: ResourceInput) =>\n Object.fromEntries([\n ['update', true],\n ['delete', true],\n ...(resource.actions ?? []).map((action) => [action.id, true]),\n ]);\n\n const makeItem = (resource: ResourceInput, values: Values): Item => {\n const id =\n typeof values.id === 'string' || typeof values.id === 'number'\n ? String(values.id)\n : String(nextId++);\n const rest = Object.fromEntries(\n Object.entries(values).filter(([key]) => !RESERVED_KEYS.has(key)),\n );\n return { ...rest, id, version: '1', abilities: abilitiesFor(resource) };\n };\n\n definition.resources.forEach((resource) => {\n store.set(\n resource.id,\n (items[resource.id] ?? []).map((values) => makeItem(resource, values)),\n );\n });\n\n const basePath = new URL(definition.api.baseUrl, 'http://panneau.test').pathname.replace(\n /\\/$/,\n '',\n );\n\n const validationProblem = (\n resource: ResourceInput,\n values: Values,\n mode: 'create' | 'update',\n ) => {\n const failures = checkValues(resource.fields, values, {\n mode,\n partial: mode === 'update',\n locales: definition.locales,\n });\n const paths = Object.keys(failures);\n if (paths.length === 0) {\n return null;\n }\n return problem(422, 'The given data was invalid.', {\n type: 'urn:panneau:problem:validation',\n errors: Object.fromEntries(\n paths.map((path) => [path, (failures[path] ?? []).map(message)]),\n ),\n });\n };\n\n const selectItems = (list: Item[], selection: Selection | undefined): Item[] => {\n if (selection === undefined) {\n return [];\n }\n if ('ids' in selection) {\n const ids = selection.ids as string[];\n return list.filter((item) => ids.includes(item.id));\n }\n const except = selection.except ?? [];\n const { filter, q } = selection.query;\n const query: Pick<ListQuery, 'filter' | 'q'> = {};\n if (filter !== undefined) {\n query.filter = filter as NonNullable<ListQuery['filter']>;\n }\n if (q !== undefined) {\n query.q = q;\n }\n return applyQuery(list, query).filter((item) => !except.includes(item.id));\n };\n\n // Direct uploads: the open uploads by id, with their key. The mock signs fake URLs, and\n // trusts the client when it completes an upload.\n const uploads = new Map<string, Upload>();\n let nextUpload = 1;\n\n const handleUploads = async (\n segments: string[],\n method: string,\n readBody: () => Promise<Values>,\n ): Promise<Response> => {\n if (definition.uploads?.mode !== 'presigned') {\n return problem(404, 'Not found.');\n }\n if (method !== 'POST') {\n return problem(405, 'Method not allowed.');\n }\n if (segments.length === 2 && segments[1] === 'sign') {\n const body = await readBody();\n const opens =\n body.uploadId === undefined && (body.method === 'POST' || body.method === 'PUT');\n if (opens) {\n const id = `upl_${nextUpload++}`;\n const upload: Upload = { id, key: `uploads/${id}` };\n uploads.set(id, upload);\n const response: UploadSignResponse = {\n url: `https://storage.test/${upload.key}?signature=mock`,\n key: upload.key,\n upload: id,\n };\n return json(200, response);\n }\n const upload = [...uploads.values()].find((candidate) => candidate.key === body.key);\n if (upload === undefined) {\n return problem(404, 'Upload not found.');\n }\n return json(200, { url: `https://storage.test/${upload.key}?signature=mock` });\n }\n if (segments.length === 3 && segments[2] === 'complete' && segments[1] !== undefined) {\n const upload = uploads.get(segments[1]);\n if (upload === undefined) {\n return problem(404, 'Upload not found.');\n }\n const { name } = await readBody();\n if (typeof name === 'string') {\n upload.name = name;\n }\n return json(200, { data: upload });\n }\n return problem(404, 'Not found.');\n };\n\n // The values of `file` fields at the top of a resource: an upload becomes a stored file, and\n // a stored file or null is kept. Returns the paths of uploads that don't exist.\n const resolveFiles = (\n resource: ResourceInput,\n values: Values,\n ): { values: Values; missing: string[] } => {\n const missing: string[] = [];\n const resolved = { ...values };\n const toFile = (value: unknown, path: string): unknown => {\n if (!isRecord(value) || typeof value.upload !== 'string') {\n return value;\n }\n const upload = uploads.get(value.upload);\n if (upload === undefined) {\n missing.push(path);\n return value;\n }\n const file: StoredFile = {\n name: upload.name ?? (typeof value.name === 'string' ? value.name : upload.id),\n url: `https://storage.test/${upload.key ?? upload.id}`,\n mime: upload.mime ?? (typeof value.mime === 'string' ? value.mime : null),\n size: upload.size ?? (typeof value.size === 'number' ? value.size : null),\n };\n return file;\n };\n resource.fields\n .filter((field) => field.type === 'file' && field.name in values)\n .forEach((field) => {\n const value = values[field.name];\n resolved[field.name] = Array.isArray(value)\n ? value.map((entry, index) => toFile(entry, `${field.name}.${String(index)}`))\n : toFile(value, field.name);\n });\n return { values: resolved, missing };\n };\n\n const missingMedia = (paths: Record<string, string>) =>\n problem(422, 'The given data was invalid.', {\n type: 'urn:panneau:problem:validation',\n errors: Object.fromEntries(\n Object.entries(paths).map(([path, message]) => [path, [message]]),\n ),\n });\n\n const missingUploads = (paths: string[]) =>\n problem(422, 'The given data was invalid.', {\n type: 'urn:panneau:problem:validation',\n errors: Object.fromEntries(\n paths.map((path) => [path, ['This upload does not exist.']]),\n ),\n });\n\n // The values of `media` and `image` fields at the top of a resource: an id, or\n // `{ id, resource, transform }`, becomes a reference to a media of one of its libraries. A\n // field that allows several libraries needs the library of each media (decision 0027).\n // Returns the problems by path.\n const resolveMedia = (\n resource: ResourceInput,\n values: Values,\n ): { values: Values; missing: Record<string, string> } => {\n const missing: Record<string, string> = {};\n const resolved = { ...values };\n resource.fields\n .filter(\n (field) =>\n (field.type === 'media' || field.type === 'image') && field.name in values,\n )\n .forEach((field) => {\n const allowed = getMediaLibraries(definition, field);\n const toReference = (value: unknown, path: string): unknown => {\n const id = isRecord(value) ? value.id : value;\n if (typeof id !== 'string' && typeof id !== 'number') {\n return value;\n }\n const named =\n isRecord(value) && typeof value.resource === 'string'\n ? value.resource\n : undefined;\n if (named === undefined && allowed.length > 1) {\n missing[path] = 'This field allows several libraries: name the library.';\n return value;\n }\n const libraryId = named ?? allowed[0];\n const library = definition.resources.find(\n (candidate) => candidate.id === libraryId && allowed.includes(candidate.id),\n );\n const media = (store.get(library?.id ?? '') ?? []).find(\n (candidate) => candidate.id === String(id),\n );\n if (media === undefined || library === undefined) {\n missing[path] = 'This media does not exist.';\n return value;\n }\n const title = media[library.itemTitle ?? 'name'];\n const reference: MediaReference = {\n id: media.id,\n ...(allowed.length > 1 ? { resource: library.id } : {}),\n ...(typeof title === 'string' ? { label: title } : {}),\n ...(isRecord(media.media) ? { media: media.media as MediaInfo } : {}),\n ...(isRecord(value) && isRecord(value.transform)\n ? { transform: value.transform }\n : {}),\n };\n return reference;\n };\n const value = values[field.name];\n resolved[field.name] = Array.isArray(value)\n ? value.map((entry, index) =>\n toReference(entry, `${field.name}.${String(index)}`),\n )\n : toReference(value, field.name);\n });\n return { values: resolved, missing };\n };\n\n // Resolves the files and media of the values, and computes the `media` of a media item from\n // its file. The client never sets `media`.\n const resolveValues = (\n resource: ResourceInput,\n body: Values,\n ): { values: Values; problem: Response | null } => {\n const sent = { ...body };\n if (resource.kind === 'media') {\n delete sent.media;\n }\n const files = resolveFiles(resource, sent);\n if (files.missing.length > 0) {\n return { values: body, problem: missingUploads(files.missing) };\n }\n const media = resolveMedia(resource, files.values);\n if (Object.keys(media.missing).length > 0) {\n return { values: body, problem: missingMedia(media.missing) };\n }\n const values = media.values;\n if (resource.kind === 'media' && isRecord(values.file)) {\n values.media = mediaOf(values.file as StoredFile);\n }\n return { values, problem: null };\n };\n\n // The global search (decision 0029): the searchable resources asked for, `limit` items each\n // (5 by default), 20 in all, from 2 characters.\n const search = (params: URLSearchParams): Response => {\n const read = readSearchQuery(params, definition);\n if (read.errors !== undefined) {\n return problem(422, 'The given data was invalid.', {\n type: 'urn:panneau:problem:validation',\n errors: read.errors,\n });\n }\n const { q, limit, resources } = read.query;\n let left = SEARCH_MAX_LIMIT;\n const data: SearchResponse['data'] = resources.flatMap((id) => {\n if (left === 0) {\n return [];\n }\n const found = applyQuery(store.get(id) ?? [], { q });\n const items = found.slice(0, Math.min(limit, left));\n left -= items.length;\n return items.length > 0 ? [{ resource: id, items, total: found.length }] : [];\n });\n return json(200, { data } satisfies SearchResponse);\n };\n\n const handle = async (request: Request): Promise<Response> => {\n const url = new URL(request.url, 'http://panneau.test');\n if (!url.pathname.startsWith(basePath)) {\n return problem(404, 'Not found.');\n }\n const segments = url.pathname\n .slice(basePath.length)\n .split('/')\n .filter((segment) => segment !== '')\n .map(decodeURIComponent);\n const method = request.method.toUpperCase();\n const readBody = async (): Promise<Values> => {\n const body: unknown = await request.json().catch(() => null);\n return isRecord(body) ? body : {};\n };\n\n if (segments.length === 1 && segments[0] === 'definition' && method === 'GET') {\n return json(200, definition);\n }\n if (segments.length === 1 && segments[0] === 'search' && method === 'GET') {\n return search(url.searchParams);\n }\n if (segments[0] === 'uploads') {\n return handleUploads(segments, method, readBody);\n }\n if (segments[0] !== 'resources' || segments[1] === undefined) {\n return problem(404, 'Not found.');\n }\n\n const resource = definition.resources.find((candidate) => candidate.id === segments[1]);\n const list = resource !== undefined ? store.get(resource.id) : undefined;\n if (resource === undefined || list === undefined) {\n return problem(404, 'Resource not found.');\n }\n\n if (segments.length === 2) {\n if (method === 'GET') {\n const query = fromSearchParams(url.searchParams);\n const perPage = query.perPage ?? resource.list?.perPage ?? 20;\n const page = query.page ?? 1;\n const sort = query.sort ?? resource.list?.defaultSort?.split(',');\n const matching = applyQuery(list, {\n ...query,\n ...(sort !== undefined ? { sort } : {}),\n });\n return json(200, {\n data: matching.slice((page - 1) * perPage, page * perPage),\n meta: {\n page,\n perPage,\n total: matching.length,\n lastPage: Math.max(1, Math.ceil(matching.length / perPage)),\n },\n });\n }\n if (method === 'POST') {\n const body = await readBody();\n const invalid = validationProblem(resource, body, 'create');\n if (invalid !== null) {\n return invalid;\n }\n const { values, problem: unresolved } = resolveValues(resource, body);\n if (unresolved !== null) {\n return unresolved;\n }\n const item = withOwnTransform(\n resource,\n makeItem(resource, { ...values, id: undefined }),\n );\n list.push(item);\n return json(201, { data: item }, etag(item));\n }\n return problem(405, 'Method not allowed.');\n }\n\n if (segments[2] === 'fields' && segments[4] === 'check' && segments.length === 5) {\n if (method !== 'POST') {\n return problem(405, 'Method not allowed.');\n }\n const field = resource.fields.find(\n (candidate) => candidate.name === segments[3] && candidate.check === true,\n );\n if (field === undefined) {\n return problem(404, 'Field not found.');\n }\n const body = await readBody();\n if (typeof body.value !== 'string') {\n return problem(422, 'The given data was invalid.', {\n type: 'urn:panneau:problem:validation',\n errors: { value: ['The value is required.'] },\n });\n }\n const locale = typeof body.locale === 'string' ? body.locale : null;\n const id =\n typeof body.id === 'string' || typeof body.id === 'number' ? String(body.id) : null;\n const valueOf = (item: Item): unknown => {\n const value = item[field.name];\n return locale !== null && isRecord(value) ? value[locale] : value;\n };\n const taken = (candidate: string) =>\n list.some((item) => item.id !== id && valueOf(item) === candidate);\n const available = !taken(body.value);\n let suggestion: string | null = null;\n for (let index = 2; !available && suggestion === null; index += 1) {\n const candidate = `${body.value}-${String(index)}`;\n suggestion = taken(candidate) ? null : candidate;\n }\n // The mock gives a URL when the field has a fixed prefix\n const prefix =\n typeof field.settings?.prefix === 'string' ? field.settings.prefix : null;\n const response: FieldCheckResponse = {\n available,\n suggestion,\n prefix,\n url: prefix !== null ? `${prefix}${body.value}` : null,\n };\n return json(200, response);\n }\n\n if (segments[2] === 'actions' && segments[3] !== undefined && segments.length === 4) {\n if (method !== 'POST') {\n return problem(405, 'Method not allowed.');\n }\n const actionId = segments[3];\n const action = (resource.actions ?? []).find((candidate) => candidate.id === actionId);\n const body = await readBody();\n if (body.dryRun === true) {\n // A dry run counts what the action would do, and changes nothing.\n if (action?.dryRun !== true) {\n return problem(400, 'This action does not support dry runs.');\n }\n const selected = selectItems(list, body.selection as Selection | undefined);\n const allowed = selected.filter((item) => item.abilities?.[actionId] !== false);\n const result: ActionResult = {\n summary: {\n succeeded: allowed.length,\n failed: 0,\n skipped: selected.length - allowed.length,\n },\n results: selected\n .filter((item) => !allowed.includes(item))\n .slice(0, 20)\n .map((item) => ({ id: item.id, status: 'skipped', reason: 'forbidden' })),\n };\n return json(200, result);\n }\n if (actionId !== 'delete') {\n return problem(404, 'Action not found.');\n }\n const selected = selectItems(list, body.selection as Selection | undefined);\n store.set(\n resource.id,\n list.filter((item) => !selected.includes(item)),\n );\n const result: ActionResult = {\n message: `${selected.length} deleted.`,\n summary: { succeeded: selected.length, failed: 0, skipped: 0 },\n refresh: 'list',\n };\n return json(200, result);\n }\n\n if (segments.length === 4 && segments[3] === 'preview' && segments[2] !== undefined) {\n const current = list.find((item) => item.id === segments[2]);\n if (current === undefined) {\n return problem(404, 'Item not found.');\n }\n if (method !== 'POST') {\n return problem(405, 'Method not allowed.');\n }\n const body = await readBody();\n const item: Item = { ...current, ...(isRecord(body) ? body : {}) };\n const url =\n options.previewUrl?.({ resource: resource.id, item }) ?? defaultPreviewUrl(item);\n return json(200, {\n url,\n expiresAt: new Date(Date.now() + 5 * 60_000).toISOString(),\n } satisfies PreviewResponse);\n }\n\n if (segments.length !== 3 || segments[2] === undefined) {\n return problem(404, 'Not found.');\n }\n const id = segments[2];\n const index = list.findIndex((item) => item.id === id);\n const current = list[index];\n if (current === undefined) {\n return problem(404, 'Item not found.');\n }\n\n if (method === 'GET') {\n return json(200, { data: current }, etag(current));\n }\n if (method === 'PATCH') {\n const ifMatch = request.headers.get('If-Match');\n if (\n ifMatch !== null &&\n current.version !== null &&\n parseEtag(ifMatch) !== current.version\n ) {\n return problem(412, 'The item was changed by someone else.', {\n type: 'urn:panneau:problem:version-conflict',\n current,\n });\n }\n const body = await readBody();\n const invalid = validationProblem(resource, body, 'update');\n if (invalid !== null) {\n return invalid;\n }\n const { values, problem: unresolved } = resolveValues(resource, body);\n if (unresolved !== null) {\n return unresolved;\n }\n const updates = Object.fromEntries(\n Object.entries(values).filter(([key]) => !RESERVED_KEYS.has(key)),\n );\n const updated = withOwnTransform(resource, {\n ...current,\n ...updates,\n version: current.version === null ? null : String(Number(current.version) + 1),\n });\n list[index] = updated;\n return json(200, { data: updated }, etag(updated));\n }\n if (method === 'DELETE') {\n list.splice(index, 1);\n return json(204, null);\n }\n return problem(405, 'Method not allowed.');\n };\n\n return {\n fetch: (input, init) => {\n if (input instanceof Request) {\n return handle(input);\n }\n // Relative URLs are accepted, as a browser would resolve them.\n return handle(new Request(new URL(input, 'http://panneau.test'), init));\n },\n items: (resource) => [...(store.get(resource) ?? [])],\n };\n}\n\nexport { createExampleDefinition } from './example';\n"],"mappings":"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"}
|
package/dist/openapi.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"AAcA,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AA6E1C,oEAAoE;AACpE,wBAAgB,sBAAsB,IAAI,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAWnE;AAED,6DAA6D;AAC7D,wBAAgB,0BAA0B,IAAI,UAAU,CAQvD;AAED,oEAAoE;AACpE,wBAAgB,qBAAqB,IAAI,UAAU,CAQlD;AAED,wFAAwF;AACxF,wBAAgB,0BAA0B,IAAI,UAAU,CAQvD;AAED,wDAAwD;AACxD,wBAAgB,qBAAqB,IAAI,UAAU,CA2XlD"}
|