@panneau/protocol 5.0.0-alpha.4 → 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.
Files changed (66) hide show
  1. package/dist/conformance/index.d.ts.map +1 -1
  2. package/dist/conformance/index.js +121 -1
  3. package/dist/conformance/index.js.map +1 -1
  4. package/dist/constants.d.ts +2 -2
  5. package/dist/constants.d.ts.map +1 -1
  6. package/dist/constants.js +3 -1
  7. package/dist/constants.js.map +1 -1
  8. package/dist/index.js +5 -2
  9. package/dist/libraries.d.ts +16 -0
  10. package/dist/libraries.d.ts.map +1 -0
  11. package/dist/libraries.js +15 -0
  12. package/dist/libraries.js.map +1 -0
  13. package/dist/lint.d.ts +2 -2
  14. package/dist/lint.d.ts.map +1 -1
  15. package/dist/lint.js +15 -9
  16. package/dist/lint.js.map +1 -1
  17. package/dist/mock/example.d.ts.map +1 -1
  18. package/dist/mock/example.js +8 -0
  19. package/dist/mock/example.js.map +1 -1
  20. package/dist/mock/index.d.ts +9 -1
  21. package/dist/mock/index.d.ts.map +1 -1
  22. package/dist/mock/index.js +119 -13
  23. package/dist/mock/index.js.map +1 -1
  24. package/dist/openapi.d.ts.map +1 -1
  25. package/dist/openapi.js +37 -7
  26. package/dist/openapi.js.map +1 -1
  27. package/dist/rules.d.ts.map +1 -1
  28. package/dist/rules.js +17 -0
  29. package/dist/rules.js.map +1 -1
  30. package/dist/runtime/index.d.ts +3 -0
  31. package/dist/runtime/index.d.ts.map +1 -1
  32. package/dist/runtime/index.js +4 -1
  33. package/dist/schemas/action.d.ts +17 -1
  34. package/dist/schemas/action.d.ts.map +1 -1
  35. package/dist/schemas/api.d.ts +87 -0
  36. package/dist/schemas/api.d.ts.map +1 -1
  37. package/dist/schemas/api.js +28 -9
  38. package/dist/schemas/api.js.map +1 -1
  39. package/dist/schemas/definition.d.ts +38 -2
  40. package/dist/schemas/definition.d.ts.map +1 -1
  41. package/dist/schemas/definition.js +2 -1
  42. package/dist/schemas/definition.js.map +1 -1
  43. package/dist/schemas/field.d.ts +24 -1
  44. package/dist/schemas/field.d.ts.map +1 -1
  45. package/dist/schemas/field.js +22 -4
  46. package/dist/schemas/field.js.map +1 -1
  47. package/dist/schemas/index.d.ts +201 -6
  48. package/dist/schemas/index.d.ts.map +1 -1
  49. package/dist/schemas/index.js +4 -2
  50. package/dist/schemas/index.js.map +1 -1
  51. package/dist/schemas/resource.d.ts +36 -2
  52. package/dist/schemas/resource.d.ts.map +1 -1
  53. package/dist/schemas/resource.js +3 -1
  54. package/dist/schemas/resource.js.map +1 -1
  55. package/dist/search.d.ts +51 -0
  56. package/dist/search.d.ts.map +1 -0
  57. package/dist/search.js +53 -0
  58. package/dist/search.js.map +1 -0
  59. package/dist/transforms.d.ts +17 -0
  60. package/dist/transforms.d.ts.map +1 -0
  61. package/dist/transforms.js +34 -0
  62. package/dist/transforms.js.map +1 -0
  63. package/package.json +1 -1
  64. package/schema/boot.schema.json +80 -1
  65. package/schema/definition.schema.json +80 -1
  66. package/schema/openapi.json +342 -13
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/conformance/index.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,kBAAkB;IAC/B,sFAAsF;IACtF,OAAO,EAAE,MAAM,CAAC;IAChB,sDAAsD;IACtD,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IACjE,wEAAwE;IACxE,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,MAAM,CAAC,EAAE;QACL,mCAAmC;QACnC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAChC,kCAAkC;QAClC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAChC,2FAA2F;QAC3F,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACrC,CAAC;CACL;AAED,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEhE,MAAM,WAAW,gBAAgB;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,iBAAiB,CAAC;IAC1B,qDAAqD;IACrD,MAAM,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAC9B,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,gBAAgB,EAAE,CAAC;CAC9B;AAoED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAwa5F;AAED,oDAAoD;AACpD,wBAAgB,YAAY,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAU9D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/conformance/index.ts"],"names":[],"mappings":"AAuBA,MAAM,WAAW,kBAAkB;IAC/B,sFAAsF;IACtF,OAAO,EAAE,MAAM,CAAC;IAChB,sDAAsD;IACtD,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IACjE,wEAAwE;IACxE,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,MAAM,CAAC,EAAE;QACL,mCAAmC;QACnC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAChC,kCAAkC;QAClC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAChC,2FAA2F;QAC3F,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACrC,CAAC;CACL;AAED,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEhE,MAAM,WAAW,gBAAgB;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,iBAAiB,CAAC;IAC1B,qDAAqD;IACrD,MAAM,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAC9B,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,gBAAgB,EAAE,CAAC;CAC9B;AAmFD;;;GAGG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAykB5F;AAED,oDAAoD;AACpD,wBAAgB,YAAY,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAU9D"}
@@ -1,8 +1,9 @@
1
1
  import { PROBLEM_CONTENT_TYPE } from "../version.js";
2
2
  import { isFilled } from "../values.js";
3
3
  import { toSearchParams } from "../query.js";
4
+ import { isSearchableResource, toSearchQueryParams } from "../search.js";
4
5
  import { Definition } from "../schemas/definition.js";
5
- import { ActionResult, ItemResponse, ListResponse, Problem, UploadSignResponse } from "../schemas/api.js";
6
+ import { ActionResult, FieldCheckResponse, ItemResponse, ListResponse, Problem, SearchResponse, UploadSignResponse } from "../schemas/api.js";
6
7
  //#region src/conformance/index.ts
7
8
  var UNKNOWN = "__panneau_conformance_unknown__";
8
9
  function describeIssues(error) {
@@ -11,6 +12,14 @@ function describeIssues(error) {
11
12
  return path === "" ? issue.message : `${path}: ${issue.message}`;
12
13
  });
13
14
  }
15
+ /** The first text of a value: a string, or the first string of a localized value. */
16
+ function textOf(value) {
17
+ if (typeof value === "string") return value;
18
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
19
+ for (const entry of Object.values(value)) if (typeof entry === "string" && entry.trim() !== "") return entry;
20
+ }
21
+ return null;
22
+ }
14
23
  function normalizeEtag(value) {
15
24
  return value.replace(/^W\//, "");
16
25
  }
@@ -217,6 +226,29 @@ async function runConformance(options) {
217
226
  }), 404);
218
227
  check.expectProblem(await request(`/uploads/${UNKNOWN}/complete`, { method: "POST" }), 404);
219
228
  });
229
+ await run("fields.check", "POST /resources/{resource}/fields/{field}/check checks a value of a field with `check`", async (check) => {
230
+ const field = resource?.fields.find((candidate) => candidate.check === true);
231
+ if (resource === null || field === void 0) {
232
+ check.skip("The resource has no field with `check`.");
233
+ return;
234
+ }
235
+ const checkPath = (name) => `${resourcePath()}/fields/${encodeURIComponent(name)}/check`;
236
+ const locale = field.localized === true ? definition?.locale : void 0;
237
+ const reply = await request(checkPath(field.name), {
238
+ method: "POST",
239
+ body: JSON.stringify({
240
+ value: "panneau-conformance",
241
+ ...locale !== void 0 ? { locale } : {},
242
+ id: null,
243
+ values: {}
244
+ })
245
+ });
246
+ if (check.expectStatus(reply, 200)) check.expectSchema(FieldCheckResponse, reply.body, "The check");
247
+ check.expectProblem(await request(checkPath(UNKNOWN), {
248
+ method: "POST",
249
+ body: "{\"value\":\"x\"}"
250
+ }), 404);
251
+ });
220
252
  await run("list.unknown-resource", "An unknown resource is a 404 problem", async (check) => {
221
253
  check.expectProblem(await request(`/resources/${UNKNOWN}`), 404);
222
254
  });
@@ -239,6 +271,94 @@ async function runConformance(options) {
239
271
  }
240
272
  check.expectProblem(await request(`${resourcePath()}/${UNKNOWN}`), 404);
241
273
  });
274
+ let searched = null;
275
+ const searchSkip = () => {
276
+ const current = definition;
277
+ if (current === null) return "No valid definition.";
278
+ if (current.search?.enabled === false) return "The definition disables the global search.";
279
+ return searched === null ? "No searchable resource has an item with a title to search for." : null;
280
+ };
281
+ const search = (query) => request(`/search?${toSearchQueryParams(query)}`);
282
+ await run("search", "GET /search finds items of the searchable resources", async (check) => {
283
+ const current = definition;
284
+ const tested = resource;
285
+ if (current === null || current.search?.enabled === false) {
286
+ check.skip(searchSkip() ?? "");
287
+ return;
288
+ }
289
+ const candidates = current.resources.filter((candidate) => isSearchableResource(candidate) && candidate.abilities?.viewAny !== false).sort((a, b) => Number(b.id === tested?.id) - Number(a.id === tested?.id));
290
+ for (const candidate of candidates) {
291
+ if (candidate.itemTitle === void 0 || searched !== null) continue;
292
+ const reply = await request(`/resources/${encodeURIComponent(candidate.id)}?page=1&perPage=1`);
293
+ const list = ListResponse.safeParse(reply.body);
294
+ const term = (list.success ? textOf(list.data.data[0]?.[candidate.itemTitle]) : null)?.split(/\s+/).find((word) => word.length >= 2);
295
+ if (term !== void 0) searched = {
296
+ resource: candidate,
297
+ term
298
+ };
299
+ }
300
+ const reason = searchSkip();
301
+ if (reason !== null || searched === null) {
302
+ check.skip(reason ?? "");
303
+ return;
304
+ }
305
+ const { resource: target, term } = searched;
306
+ const reply = await search({ q: term });
307
+ if (!check.expectStatus(reply, 200)) return;
308
+ const results = check.expectSchema(SearchResponse, reply.body, "The results");
309
+ if (results === null) return;
310
+ const ids = candidates.map((candidate) => candidate.id);
311
+ for (const group of results.data) {
312
+ check.expect(ids.includes(group.resource), `Results of "${group.resource}", which is not a searchable resource.`);
313
+ check.expect(group.items.length <= 5, `"${group.resource}" has ${group.items.length} results; without a limit, at most 5.`);
314
+ }
315
+ const count = results.data.reduce((total, group) => total + group.items.length, 0);
316
+ check.expect(count <= 20, `The results have ${count} items, at most 20.`);
317
+ check.expect(results.data.some((group) => group.resource === target.id && group.items.length > 0), `Searching "${term}", a word of the title of an item of "${target.id}", finds none of its items.`);
318
+ });
319
+ await run("search.scope", "GET /search reads limit and resources", async (check) => {
320
+ const reason = searchSkip();
321
+ if (reason !== null || searched === null) {
322
+ check.skip(reason ?? "");
323
+ return;
324
+ }
325
+ const { resource: target, term } = searched;
326
+ const reply = await search({
327
+ q: term,
328
+ limit: 1,
329
+ resources: [target.id]
330
+ });
331
+ if (!check.expectStatus(reply, 200)) return;
332
+ const results = check.expectSchema(SearchResponse, reply.body, "The results");
333
+ if (results !== null) check.expect(results.data.length === 1 && results.data[0]?.resource === target.id && results.data[0].items.length === 1, `With limit=1&resources=${target.id}, expected one item of "${target.id}", got ${JSON.stringify(results.data.map((group) => [group.resource, group.items.length]))}.`);
334
+ });
335
+ await run("search.invalid", "An invalid search is a 422 problem, with errors keyed by parameter", async (check) => {
336
+ const reason = searchSkip();
337
+ if (reason !== null || searched === null) {
338
+ check.skip(reason ?? "");
339
+ return;
340
+ }
341
+ const { term } = searched;
342
+ const invalid = [
343
+ ["q", { q: term.slice(0, 1) }],
344
+ ["limit", {
345
+ q: term,
346
+ limit: 0
347
+ }],
348
+ ["limit", {
349
+ q: term,
350
+ limit: 21
351
+ }],
352
+ ["resources", {
353
+ q: term,
354
+ resources: [UNKNOWN]
355
+ }]
356
+ ];
357
+ for (const [parameter, query] of invalid) {
358
+ const failure = check.expectProblem(await search(query), 422);
359
+ if (failure !== null) check.expect(failure.errors?.[parameter] !== void 0, `${toSearchQueryParams(query)}: the 422 problem must have errors.${parameter}.`);
360
+ }
361
+ });
242
362
  const writes = options.writes;
243
363
  let createdId = null;
244
364
  let createdVersion = null;
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/conformance/index.ts"],"sourcesContent":["import type { z } from 'zod';\n\nimport { type FilterRange, toSearchParams } from '../query';\nimport { Definition, type Resource } from '../schemas';\nimport {\n ActionResult,\n ItemResponse,\n ListResponse,\n Problem,\n UploadSignResponse,\n} from '../schemas/api';\nimport { isFilled } from '../values';\nimport { PROBLEM_CONTENT_TYPE } from '../version';\n\nexport interface ConformanceOptions {\n /** Base URL of the API, the same as `api.baseUrl` of the definition, but absolute. */\n baseUrl: string;\n /** The `fetch` to use. Defaults to the global one. */\n fetch?: (input: string, init?: RequestInit) => Promise<Response>;\n /** Merged into every request, for authentication headers or cookies. */\n init?: RequestInit;\n /** Resource to test. Defaults to the first resource of the definition. */\n resource?: string;\n /**\n * Enables the checks that write data, on the tested resource. Use a test database: these\n * checks create, update and delete an item.\n */\n writes?: {\n /** Valid values for a new item. */\n create: Record<string, unknown>;\n /** Valid values for an update. */\n update: Record<string, unknown>;\n /** Values that must be refused with a `422`. Defaults to `{}` when a field is required. */\n invalid?: Record<string, unknown>;\n };\n}\n\nexport type ConformanceStatus = 'passed' | 'failed' | 'skipped';\n\nexport interface ConformanceCheck {\n id: string;\n title: string;\n status: ConformanceStatus;\n /** What went wrong, or why the check was skipped. */\n issues: string[];\n}\n\nexport interface ConformanceReport {\n passed: boolean;\n checks: ConformanceCheck[];\n}\n\ninterface Reply {\n status: number;\n contentType: string;\n etag: string | null;\n body: unknown;\n}\n\nconst UNKNOWN = '__panneau_conformance_unknown__';\n\nfunction describeIssues(error: z.ZodError): string[] {\n return error.issues.slice(0, 10).map((issue) => {\n const path = issue.path.map(String).join('.');\n return path === '' ? issue.message : `${path}: ${issue.message}`;\n });\n}\n\nfunction normalizeEtag(value: string): string {\n return value.replace(/^W\\//, '');\n}\n\nclass Check {\n issues: string[] = [];\n skippedBecause: string | null = null;\n\n expect(condition: boolean, issue: string): boolean {\n if (!condition) {\n this.issues.push(issue);\n }\n return condition;\n }\n\n expectStatus(reply: Reply, status: number): boolean {\n return this.expect(\n reply.status === status,\n `Expected status ${status}, got ${reply.status}.`,\n );\n }\n\n expectSchema<T extends z.ZodType>(schema: T, value: unknown, label: string): z.infer<T> | null {\n const result = schema.safeParse(value);\n if (result.success) {\n return result.data;\n }\n this.issues.push(\n `${label} does not match the schema:`,\n ...describeIssues(result.error).map((issue) => ` ${issue}`),\n );\n return null;\n }\n\n expectProblem(reply: Reply, status: number): Problem | null {\n if (!this.expectStatus(reply, status)) {\n return null;\n }\n this.expect(\n reply.contentType.startsWith(PROBLEM_CONTENT_TYPE),\n `Expected content type ${PROBLEM_CONTENT_TYPE}, got \"${reply.contentType}\".`,\n );\n return this.expectSchema(Problem, reply.body, 'The error');\n }\n\n skip(reason: string) {\n this.skippedBecause = reason;\n }\n}\n\n/**\n * Runs the protocol's conformance checks against a live backend. Read-only checks always run;\n * checks that write data run only with `writes`.\n */\nexport async function runConformance(options: ConformanceOptions): Promise<ConformanceReport> {\n const fetcher = options.fetch ?? ((input: string, init?: RequestInit) => fetch(input, init));\n const baseUrl = options.baseUrl.replace(/\\/$/, '');\n const checks: ConformanceCheck[] = [];\n\n const request = async (path: string, init: RequestInit = {}): Promise<Reply> => {\n const headers = new Headers(options.init?.headers);\n headers.set('Accept', 'application/json');\n new Headers(init.headers).forEach((value, key) => headers.set(key, value));\n if (init.body !== undefined) {\n headers.set('Content-Type', 'application/json');\n }\n const response = await fetcher(`${baseUrl}${path}`, { ...options.init, ...init, headers });\n const text = await response.text();\n const parse = (): unknown => {\n if (text === '') {\n return null;\n }\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n };\n const body = parse();\n return {\n status: response.status,\n contentType: response.headers.get('Content-Type') ?? '',\n etag: response.headers.get('ETag'),\n body,\n };\n };\n\n const run = async (id: string, title: string, body: (check: Check) => Promise<void>) => {\n const check = new Check();\n try {\n await body(check);\n } catch (error) {\n check.issues.push(\n `Request failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n const status: ConformanceStatus =\n check.issues.length > 0\n ? 'failed'\n : check.skippedBecause !== null\n ? 'skipped'\n : 'passed';\n checks.push({\n id,\n title,\n status,\n issues:\n status === 'skipped' && check.skippedBecause !== null\n ? [check.skippedBecause]\n : check.issues,\n });\n };\n\n let definition: Definition | null = null;\n let resource: Resource | null = null;\n let firstItemId: string | null = null;\n\n await run('definition', 'GET /definition returns a valid definition', async (check) => {\n const reply = await request('/definition');\n if (check.expectStatus(reply, 200)) {\n definition = check.expectSchema(Definition, reply.body, 'The definition');\n }\n });\n\n await run('definition.resource', 'The definition has the resource to test', async (check) => {\n const current = definition as Definition | null;\n if (current === null) {\n check.skip('No valid definition.');\n return;\n }\n resource =\n (options.resource !== undefined\n ? current.resources.find((candidate) => candidate.id === options.resource)\n : current.resources[0]) ?? null;\n check.expect(\n resource !== null,\n options.resource !== undefined\n ? `The definition has no resource \"${options.resource}\".`\n : 'The definition has no resources.',\n );\n });\n\n const resourcePath = () =>\n `/resources/${encodeURIComponent((resource as Resource | null)?.id ?? '')}`;\n\n await run('list', 'GET /resources/{resource} returns a page of items', async (check) => {\n if (resource === null) {\n check.skip('No resource to test.');\n return;\n }\n const reply = await request(`${resourcePath()}?page=1&perPage=2`);\n if (!check.expectStatus(reply, 200)) {\n return;\n }\n const list = check.expectSchema(ListResponse, reply.body, 'The list');\n if (list === null) {\n return;\n }\n check.expect(list.data.length <= 2, `perPage=2 returned ${list.data.length} items.`);\n check.expect(list.meta.page === 1, `meta.page is ${list.meta.page}, expected 1.`);\n check.expect(list.meta.perPage === 2, `meta.perPage is ${list.meta.perPage}, expected 2.`);\n firstItemId = list.data[0]?.id ?? null;\n });\n\n await run('list.sort', 'GET /resources/{resource} accepts a sort', async (check) => {\n const sort = (resource as Resource | null)?.list?.sorts?.[0];\n if (sort === undefined) {\n check.skip('The resource declares no sorts.');\n return;\n }\n const reply = await request(`${resourcePath()}?sort=${encodeURIComponent(`-${sort}`)}`);\n if (check.expectStatus(reply, 200)) {\n check.expectSchema(ListResponse, reply.body, 'The sorted list');\n }\n });\n\n await run(\n 'list.filter-range',\n 'GET /resources/{resource} reads the bounds of a range filter',\n async (check) => {\n const filter = (resource as Resource | null)?.list?.filters?.find(\n (candidate) => candidate.type === 'date-range',\n );\n if (filter === undefined) {\n check.skip('The resource declares no date-range filter.');\n return;\n }\n const list = async (range: FilterRange | null, label: string) => {\n const query =\n range !== null ? `?${toSearchParams({ filter: { [filter.id]: range } })}` : '';\n const reply = await request(`${resourcePath()}${query}`);\n if (!check.expectStatus(reply, 200)) {\n return null;\n }\n return check.expectSchema(ListResponse, reply.body, label);\n };\n const all = await list(null, 'The list');\n if (all === null) {\n return;\n }\n // Items of the first page with a value: a range that covers every day includes them.\n const dated = all.data.filter((item) => isFilled(item[filter.id])).length;\n const ranges: [FilterRange, string][] = [\n [{ from: '0001-01-01', to: '9999-12-31' }, 'from 0001-01-01 to 9999-12-31'],\n [{ to: '9999-12-31' }, 'up to 9999-12-31 (an open start)'],\n ];\n for (const [range, label] of ranges) {\n const reply = await list(range, `The list filtered ${label}`);\n if (reply !== null) {\n check.expect(\n reply.meta.total >= dated,\n `Filtered ${label}, the list has ${reply.meta.total} items; the first page alone has ${dated} with a ${filter.id}.`,\n );\n }\n }\n const future = await list({ from: '9999-12-31' }, 'The list filtered from 9999-12-31');\n if (future !== null) {\n check.expect(\n future.meta.total === 0,\n `Filtered from 9999-12-31, the list has ${future.meta.total} items, expected 0.`,\n );\n }\n },\n );\n\n await run(\n 'action.dry-run',\n 'A dry run of an action counts what it would do, and changes nothing',\n async (check) => {\n const action = (resource as Resource | null)?.actions?.find(\n (candidate) =>\n candidate.dryRun === true &&\n candidate.kind === 'server' &&\n candidate.scope.includes('selection'),\n );\n if (action === undefined) {\n check.skip('The resource declares no selection action with dryRun.');\n return;\n }\n const page = async (label: string) => {\n const reply = await request(`${resourcePath()}?page=1&perPage=20`);\n return check.expectStatus(reply, 200)\n ? check.expectSchema(ListResponse, reply.body, label)\n : null;\n };\n const before = await page('The list before the dry run');\n const reply = await request(\n `${resourcePath()}/actions/${encodeURIComponent(action.id)}`,\n {\n method: 'POST',\n body: JSON.stringify({ selection: { query: {} }, dryRun: true }),\n },\n );\n if (!check.expectStatus(reply, 200)) {\n return;\n }\n const result = check.expectSchema(ActionResult, reply.body, 'The dry run result');\n if (result !== null && before !== null) {\n check.expect(\n result.summary !== undefined,\n 'A dry run result must have a summary of the items that would succeed and be skipped.',\n );\n if (result.summary !== undefined) {\n const counted =\n result.summary.succeeded + result.summary.failed + result.summary.skipped;\n check.expect(\n counted === before.meta.total,\n `The dry run counts ${counted} items; the list has ${before.meta.total}.`,\n );\n }\n }\n const after = await page('The list after the dry run');\n if (before !== null && after !== null) {\n const versions = (list: typeof before) =>\n JSON.stringify(list.data.map((item) => [item.id, item.version ?? null]));\n check.expect(\n after.meta.total === before.meta.total && versions(after) === versions(before),\n 'A dry run must not change the items.',\n );\n }\n },\n );\n\n await run(\n 'uploads.sign',\n 'POST /uploads/sign signs the first request of a file, and only the keys of open uploads',\n async (check) => {\n const uploads = (definition as Definition | null)?.uploads;\n if (uploads?.mode !== 'presigned') {\n check.skip('The definition has no presigned uploads.');\n return;\n }\n if (uploads.endpoint !== undefined) {\n check.skip('The upload routes are not at {api.baseUrl}/uploads.');\n return;\n }\n const sign = (body: unknown) =>\n request('/uploads/sign', { method: 'POST', body: JSON.stringify(body) });\n const reply = await sign({ method: 'POST', key: 'panneau-conformance.txt' });\n if (check.expectStatus(reply, 200)) {\n const signed = check.expectSchema(\n UploadSignResponse,\n reply.body,\n 'The signed request',\n );\n if (signed !== null) {\n check.expect(\n signed.key !== undefined && signed.upload !== undefined,\n 'The first request of a file must answer the key and the upload the backend chose.',\n );\n }\n }\n check.expectProblem(\n await sign({\n method: 'PUT',\n key: 'not-an-upload-of-this-user',\n uploadId: 'unknown',\n partNumber: 1,\n }),\n 404,\n );\n check.expectProblem(\n await request(`/uploads/${UNKNOWN}/complete`, { method: 'POST' }),\n 404,\n );\n },\n );\n\n await run('list.unknown-resource', 'An unknown resource is a 404 problem', async (check) => {\n check.expectProblem(await request(`/resources/${UNKNOWN}`), 404);\n });\n\n await run(\n 'item',\n 'GET /resources/{resource}/{id} returns the item with its ETag',\n async (check) => {\n if (firstItemId === null) {\n check.skip('The list has no items.');\n return;\n }\n const reply = await request(`${resourcePath()}/${encodeURIComponent(firstItemId)}`);\n if (!check.expectStatus(reply, 200)) {\n return;\n }\n const item = check.expectSchema(ItemResponse, reply.body, 'The item');\n if (item === null) {\n return;\n }\n check.expect(\n item.data.id === firstItemId,\n `Asked for item \"${firstItemId}\", got \"${item.data.id}\".`,\n );\n if (item.data.version !== null) {\n check.expect(\n reply.etag !== null && normalizeEtag(reply.etag) === `\"${item.data.version}\"`,\n `Expected ETag \"${item.data.version}\" (quoted), got ${reply.etag ?? 'none'}.`,\n );\n }\n },\n );\n\n await run('item.missing', 'A missing item is a 404 problem', async (check) => {\n if (resource === null) {\n check.skip('No resource to test.');\n return;\n }\n check.expectProblem(await request(`${resourcePath()}/${UNKNOWN}`), 404);\n });\n\n const writes = options.writes;\n let createdId: string | null = null;\n let createdVersion: string | null = null;\n\n await run('create.invalid', 'Invalid values are refused with a 422 problem', async (check) => {\n if (writes === undefined || resource === null) {\n check.skip('Write checks are disabled.');\n return;\n }\n const hasRequired = resource.fields.some((field) =>\n (field.rules ?? []).some((rule) => rule.rule === 'required'),\n );\n if (writes.invalid === undefined && !hasRequired) {\n check.skip('No invalid values given, and the resource has no required field.');\n return;\n }\n const reply = await request(resourcePath(), {\n method: 'POST',\n body: JSON.stringify(writes.invalid ?? {}),\n });\n const failure = check.expectProblem(reply, 422);\n if (failure !== null) {\n check.expect(\n failure.errors !== undefined && Object.keys(failure.errors).length > 0,\n 'A 422 problem must list the invalid values in `errors`.',\n );\n }\n });\n\n await run('create', 'POST /resources/{resource} creates an item', async (check) => {\n if (writes === undefined || resource === null) {\n check.skip('Write checks are disabled.');\n return;\n }\n const reply = await request(resourcePath(), {\n method: 'POST',\n body: JSON.stringify(writes.create),\n });\n if (!check.expectStatus(reply, 201)) {\n return;\n }\n const item = check.expectSchema(ItemResponse, reply.body, 'The created item');\n createdId = item?.data.id ?? null;\n createdVersion = item?.data.version ?? null;\n });\n\n const itemPath = () => `${resourcePath()}/${encodeURIComponent(createdId ?? '')}`;\n\n await run('update', 'PATCH /resources/{resource}/{id} updates the item', async (check) => {\n if (createdId === null || writes === undefined) {\n check.skip('No item was created.');\n return;\n }\n const reply = await request(itemPath(), {\n method: 'PATCH',\n body: JSON.stringify(writes.update),\n headers: createdVersion !== null ? { 'If-Match': `\"${createdVersion}\"` } : {},\n });\n if (!check.expectStatus(reply, 200)) {\n return;\n }\n const item = check.expectSchema(ItemResponse, reply.body, 'The updated item');\n if (item !== null && createdVersion !== null) {\n check.expect(\n item.data.version !== createdVersion,\n 'The version did not change after an update.',\n );\n }\n });\n\n await run(\n 'update.conflict',\n 'A stale If-Match is a 412 problem with the current item',\n async (check) => {\n if (createdId === null || writes === undefined) {\n check.skip('No item was created.');\n return;\n }\n if (createdVersion === null) {\n check.skip('The backend has no versions.');\n return;\n }\n const reply = await request(itemPath(), {\n method: 'PATCH',\n body: JSON.stringify(writes.update),\n headers: { 'If-Match': '\"__panneau_conformance_stale__\"' },\n });\n const failure = check.expectProblem(reply, 412);\n if (failure !== null) {\n check.expect(\n failure.current !== undefined,\n 'A 412 problem must include the current item.',\n );\n }\n },\n );\n\n await run('delete', 'DELETE /resources/{resource}/{id} deletes the item', async (check) => {\n if (createdId === null) {\n check.skip('No item was created.');\n return;\n }\n if (!check.expectStatus(await request(itemPath(), { method: 'DELETE' }), 204)) {\n return;\n }\n check.expectProblem(await request(itemPath()), 404);\n });\n\n return { passed: checks.every((check) => check.status !== 'failed'), checks };\n}\n\n/** Formats a report as text, one line per check. */\nexport function formatReport(report: ConformanceReport): string {\n const symbols: Record<ConformanceStatus, string> = { passed: '✓', failed: '✗', skipped: '-' };\n return report.checks\n .map((check) =>\n [\n `${symbols[check.status]} ${check.title}`,\n ...check.issues.map((issue) => ` ${issue}`),\n ].join('\\n'),\n )\n .join('\\n');\n}\n"],"mappings":";;;;;;AA2DA,IAAM,UAAU;AAEhB,SAAS,eAAe,OAA6B;CACjD,OAAO,MAAM,OAAO,MAAM,GAAG,EAAE,CAAC,CAAC,KAAK,UAAU;EAC5C,MAAM,OAAO,MAAM,KAAK,IAAI,MAAM,CAAC,CAAC,KAAK,GAAG;EAC5C,OAAO,SAAS,KAAK,MAAM,UAAU,GAAG,KAAK,IAAI,MAAM;CAC3D,CAAC;AACL;AAEA,SAAS,cAAc,OAAuB;CAC1C,OAAO,MAAM,QAAQ,QAAQ,EAAE;AACnC;AAEA,IAAM,QAAN,MAAY;CACR,SAAmB,CAAC;CACpB,iBAAgC;CAEhC,OAAO,WAAoB,OAAwB;EAC/C,IAAI,CAAC,WACD,KAAK,OAAO,KAAK,KAAK;EAE1B,OAAO;CACX;CAEA,aAAa,OAAc,QAAyB;EAChD,OAAO,KAAK,OACR,MAAM,WAAW,QACjB,mBAAmB,OAAO,QAAQ,MAAM,OAAO,EACnD;CACJ;CAEA,aAAkC,QAAW,OAAgB,OAAkC;EAC3F,MAAM,SAAS,OAAO,UAAU,KAAK;EACrC,IAAI,OAAO,SACP,OAAO,OAAO;EAElB,KAAK,OAAO,KACR,GAAG,MAAM,8BACT,GAAG,eAAe,OAAO,KAAK,CAAC,CAAC,KAAK,UAAU,KAAK,OAAO,CAC/D;EACA,OAAO;CACX;CAEA,cAAc,OAAc,QAAgC;EACxD,IAAI,CAAC,KAAK,aAAa,OAAO,MAAM,GAChC,OAAO;EAEX,KAAK,OACD,MAAM,YAAY,WAAW,oBAAoB,GACjD,yBAAyB,qBAAqB,SAAS,MAAM,YAAY,GAC7E;EACA,OAAO,KAAK,aAAa,SAAS,MAAM,MAAM,WAAW;CAC7D;CAEA,KAAK,QAAgB;EACjB,KAAK,iBAAiB;CAC1B;AACJ;;;;;AAMA,eAAsB,eAAe,SAAyD;CAC1F,MAAM,UAAU,QAAQ,WAAW,OAAe,SAAuB,MAAM,OAAO,IAAI;CAC1F,MAAM,UAAU,QAAQ,QAAQ,QAAQ,OAAO,EAAE;CACjD,MAAM,SAA6B,CAAC;CAEpC,MAAM,UAAU,OAAO,MAAc,OAAoB,CAAC,MAAsB;EAC5E,MAAM,UAAU,IAAI,QAAQ,QAAQ,MAAM,OAAO;EACjD,QAAQ,IAAI,UAAU,kBAAkB;EACxC,IAAI,QAAQ,KAAK,OAAO,CAAC,CAAC,SAAS,OAAO,QAAQ,QAAQ,IAAI,KAAK,KAAK,CAAC;EACzE,IAAI,KAAK,SAAS,KAAA,GACd,QAAQ,IAAI,gBAAgB,kBAAkB;EAElD,MAAM,WAAW,MAAM,QAAQ,GAAG,UAAU,QAAQ;GAAE,GAAG,QAAQ;GAAM,GAAG;GAAM;EAAQ,CAAC;EACzF,MAAM,OAAO,MAAM,SAAS,KAAK;EACjC,MAAM,cAAuB;GACzB,IAAI,SAAS,IACT,OAAO;GAEX,IAAI;IACA,OAAO,KAAK,MAAM,IAAI;GAC1B,QAAQ;IACJ,OAAO;GACX;EACJ;EACA,MAAM,OAAO,MAAM;EACnB,OAAO;GACH,QAAQ,SAAS;GACjB,aAAa,SAAS,QAAQ,IAAI,cAAc,KAAK;GACrD,MAAM,SAAS,QAAQ,IAAI,MAAM;GACjC;EACJ;CACJ;CAEA,MAAM,MAAM,OAAO,IAAY,OAAe,SAA0C;EACpF,MAAM,QAAQ,IAAI,MAAM;EACxB,IAAI;GACA,MAAM,KAAK,KAAK;EACpB,SAAS,OAAO;GACZ,MAAM,OAAO,KACT,mBAAmB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAC5E;EACJ;EACA,MAAM,SACF,MAAM,OAAO,SAAS,IAChB,WACA,MAAM,mBAAmB,OACvB,YACA;EACZ,OAAO,KAAK;GACR;GACA;GACA;GACA,QACI,WAAW,aAAa,MAAM,mBAAmB,OAC3C,CAAC,MAAM,cAAc,IACrB,MAAM;EACpB,CAAC;CACL;CAEA,IAAI,aAAgC;CACpC,IAAI,WAA4B;CAChC,IAAI,cAA6B;CAEjC,MAAM,IAAI,cAAc,8CAA8C,OAAO,UAAU;EACnF,MAAM,QAAQ,MAAM,QAAQ,aAAa;EACzC,IAAI,MAAM,aAAa,OAAO,GAAG,GAC7B,aAAa,MAAM,aAAa,YAAY,MAAM,MAAM,gBAAgB;CAEhF,CAAC;CAED,MAAM,IAAI,uBAAuB,2CAA2C,OAAO,UAAU;EACzF,MAAM,UAAU;EAChB,IAAI,YAAY,MAAM;GAClB,MAAM,KAAK,sBAAsB;GACjC;EACJ;EACA,YACK,QAAQ,aAAa,KAAA,IAChB,QAAQ,UAAU,MAAM,cAAc,UAAU,OAAO,QAAQ,QAAQ,IACvE,QAAQ,UAAU,OAAO;EACnC,MAAM,OACF,aAAa,MACb,QAAQ,aAAa,KAAA,IACf,mCAAmC,QAAQ,SAAS,MACpD,kCACV;CACJ,CAAC;CAED,MAAM,qBACF,cAAc,mBAAoB,UAA8B,MAAM,EAAE;CAE5E,MAAM,IAAI,QAAQ,qDAAqD,OAAO,UAAU;EACpF,IAAI,aAAa,MAAM;GACnB,MAAM,KAAK,sBAAsB;GACjC;EACJ;EACA,MAAM,QAAQ,MAAM,QAAQ,GAAG,aAAa,EAAE,kBAAkB;EAChE,IAAI,CAAC,MAAM,aAAa,OAAO,GAAG,GAC9B;EAEJ,MAAM,OAAO,MAAM,aAAa,cAAc,MAAM,MAAM,UAAU;EACpE,IAAI,SAAS,MACT;EAEJ,MAAM,OAAO,KAAK,KAAK,UAAU,GAAG,sBAAsB,KAAK,KAAK,OAAO,QAAQ;EACnF,MAAM,OAAO,KAAK,KAAK,SAAS,GAAG,gBAAgB,KAAK,KAAK,KAAK,cAAc;EAChF,MAAM,OAAO,KAAK,KAAK,YAAY,GAAG,mBAAmB,KAAK,KAAK,QAAQ,cAAc;EACzF,cAAc,KAAK,KAAK,EAAE,EAAE,MAAM;CACtC,CAAC;CAED,MAAM,IAAI,aAAa,4CAA4C,OAAO,UAAU;EAChF,MAAM,OAAQ,UAA8B,MAAM,QAAQ;EAC1D,IAAI,SAAS,KAAA,GAAW;GACpB,MAAM,KAAK,iCAAiC;GAC5C;EACJ;EACA,MAAM,QAAQ,MAAM,QAAQ,GAAG,aAAa,EAAE,QAAQ,mBAAmB,IAAI,MAAM,GAAG;EACtF,IAAI,MAAM,aAAa,OAAO,GAAG,GAC7B,MAAM,aAAa,cAAc,MAAM,MAAM,iBAAiB;CAEtE,CAAC;CAED,MAAM,IACF,qBACA,gEACA,OAAO,UAAU;EACb,MAAM,SAAU,UAA8B,MAAM,SAAS,MACxD,cAAc,UAAU,SAAS,YACtC;EACA,IAAI,WAAW,KAAA,GAAW;GACtB,MAAM,KAAK,6CAA6C;GACxD;EACJ;EACA,MAAM,OAAO,OAAO,OAA2B,UAAkB;GAC7D,MAAM,QACF,UAAU,OAAO,IAAI,eAAe,EAAE,QAAQ,GAAG,OAAO,KAAK,MAAM,EAAE,CAAC,MAAM;GAChF,MAAM,QAAQ,MAAM,QAAQ,GAAG,aAAa,IAAI,OAAO;GACvD,IAAI,CAAC,MAAM,aAAa,OAAO,GAAG,GAC9B,OAAO;GAEX,OAAO,MAAM,aAAa,cAAc,MAAM,MAAM,KAAK;EAC7D;EACA,MAAM,MAAM,MAAM,KAAK,MAAM,UAAU;EACvC,IAAI,QAAQ,MACR;EAGJ,MAAM,QAAQ,IAAI,KAAK,QAAQ,SAAS,SAAS,KAAK,OAAO,GAAG,CAAC,CAAC,CAAC;EAKnE,KAAK,MAAM,CAAC,OAAO,UAAU,CAHzB,CAAC;GAAE,MAAM;GAAc,IAAI;EAAa,GAAG,+BAA+B,GAC1E,CAAC,EAAE,IAAI,aAAa,GAAG,kCAAkC,CAEhC,GAAQ;GACjC,MAAM,QAAQ,MAAM,KAAK,OAAO,qBAAqB,OAAO;GAC5D,IAAI,UAAU,MACV,MAAM,OACF,MAAM,KAAK,SAAS,OACpB,YAAY,MAAM,iBAAiB,MAAM,KAAK,MAAM,mCAAmC,MAAM,UAAU,OAAO,GAAG,EACrH;EAER;EACA,MAAM,SAAS,MAAM,KAAK,EAAE,MAAM,aAAa,GAAG,mCAAmC;EACrF,IAAI,WAAW,MACX,MAAM,OACF,OAAO,KAAK,UAAU,GACtB,0CAA0C,OAAO,KAAK,MAAM,oBAChE;CAER,CACJ;CAEA,MAAM,IACF,kBACA,uEACA,OAAO,UAAU;EACb,MAAM,SAAU,UAA8B,SAAS,MAClD,cACG,UAAU,WAAW,QACrB,UAAU,SAAS,YACnB,UAAU,MAAM,SAAS,WAAW,CAC5C;EACA,IAAI,WAAW,KAAA,GAAW;GACtB,MAAM,KAAK,wDAAwD;GACnE;EACJ;EACA,MAAM,OAAO,OAAO,UAAkB;GAClC,MAAM,QAAQ,MAAM,QAAQ,GAAG,aAAa,EAAE,mBAAmB;GACjE,OAAO,MAAM,aAAa,OAAO,GAAG,IAC9B,MAAM,aAAa,cAAc,MAAM,MAAM,KAAK,IAClD;EACV;EACA,MAAM,SAAS,MAAM,KAAK,6BAA6B;EACvD,MAAM,QAAQ,MAAM,QAChB,GAAG,aAAa,EAAE,WAAW,mBAAmB,OAAO,EAAE,KACzD;GACI,QAAQ;GACR,MAAM,KAAK,UAAU;IAAE,WAAW,EAAE,OAAO,CAAC,EAAE;IAAG,QAAQ;GAAK,CAAC;EACnE,CACJ;EACA,IAAI,CAAC,MAAM,aAAa,OAAO,GAAG,GAC9B;EAEJ,MAAM,SAAS,MAAM,aAAa,cAAc,MAAM,MAAM,oBAAoB;EAChF,IAAI,WAAW,QAAQ,WAAW,MAAM;GACpC,MAAM,OACF,OAAO,YAAY,KAAA,GACnB,sFACJ;GACA,IAAI,OAAO,YAAY,KAAA,GAAW;IAC9B,MAAM,UACF,OAAO,QAAQ,YAAY,OAAO,QAAQ,SAAS,OAAO,QAAQ;IACtE,MAAM,OACF,YAAY,OAAO,KAAK,OACxB,sBAAsB,QAAQ,uBAAuB,OAAO,KAAK,MAAM,EAC3E;GACJ;EACJ;EACA,MAAM,QAAQ,MAAM,KAAK,4BAA4B;EACrD,IAAI,WAAW,QAAQ,UAAU,MAAM;GACnC,MAAM,YAAY,SACd,KAAK,UAAU,KAAK,KAAK,KAAK,SAAS,CAAC,KAAK,IAAI,KAAK,WAAW,IAAI,CAAC,CAAC;GAC3E,MAAM,OACF,MAAM,KAAK,UAAU,OAAO,KAAK,SAAS,SAAS,KAAK,MAAM,SAAS,MAAM,GAC7E,sCACJ;EACJ;CACJ,CACJ;CAEA,MAAM,IACF,gBACA,2FACA,OAAO,UAAU;EACb,MAAM,UAAW,YAAkC;EACnD,IAAI,SAAS,SAAS,aAAa;GAC/B,MAAM,KAAK,0CAA0C;GACrD;EACJ;EACA,IAAI,QAAQ,aAAa,KAAA,GAAW;GAChC,MAAM,KAAK,qDAAqD;GAChE;EACJ;EACA,MAAM,QAAQ,SACV,QAAQ,iBAAiB;GAAE,QAAQ;GAAQ,MAAM,KAAK,UAAU,IAAI;EAAE,CAAC;EAC3E,MAAM,QAAQ,MAAM,KAAK;GAAE,QAAQ;GAAQ,KAAK;EAA0B,CAAC;EAC3E,IAAI,MAAM,aAAa,OAAO,GAAG,GAAG;GAChC,MAAM,SAAS,MAAM,aACjB,oBACA,MAAM,MACN,oBACJ;GACA,IAAI,WAAW,MACX,MAAM,OACF,OAAO,QAAQ,KAAA,KAAa,OAAO,WAAW,KAAA,GAC9C,mFACJ;EAER;EACA,MAAM,cACF,MAAM,KAAK;GACP,QAAQ;GACR,KAAK;GACL,UAAU;GACV,YAAY;EAChB,CAAC,GACD,GACJ;EACA,MAAM,cACF,MAAM,QAAQ,YAAY,QAAQ,YAAY,EAAE,QAAQ,OAAO,CAAC,GAChE,GACJ;CACJ,CACJ;CAEA,MAAM,IAAI,yBAAyB,wCAAwC,OAAO,UAAU;EACxF,MAAM,cAAc,MAAM,QAAQ,cAAc,SAAS,GAAG,GAAG;CACnE,CAAC;CAED,MAAM,IACF,QACA,iEACA,OAAO,UAAU;EACb,IAAI,gBAAgB,MAAM;GACtB,MAAM,KAAK,wBAAwB;GACnC;EACJ;EACA,MAAM,QAAQ,MAAM,QAAQ,GAAG,aAAa,EAAE,GAAG,mBAAmB,WAAW,GAAG;EAClF,IAAI,CAAC,MAAM,aAAa,OAAO,GAAG,GAC9B;EAEJ,MAAM,OAAO,MAAM,aAAa,cAAc,MAAM,MAAM,UAAU;EACpE,IAAI,SAAS,MACT;EAEJ,MAAM,OACF,KAAK,KAAK,OAAO,aACjB,mBAAmB,YAAY,UAAU,KAAK,KAAK,GAAG,GAC1D;EACA,IAAI,KAAK,KAAK,YAAY,MACtB,MAAM,OACF,MAAM,SAAS,QAAQ,cAAc,MAAM,IAAI,MAAM,IAAI,KAAK,KAAK,QAAQ,IAC3E,kBAAkB,KAAK,KAAK,QAAQ,kBAAkB,MAAM,QAAQ,OAAO,EAC/E;CAER,CACJ;CAEA,MAAM,IAAI,gBAAgB,mCAAmC,OAAO,UAAU;EAC1E,IAAI,aAAa,MAAM;GACnB,MAAM,KAAK,sBAAsB;GACjC;EACJ;EACA,MAAM,cAAc,MAAM,QAAQ,GAAG,aAAa,EAAE,GAAG,SAAS,GAAG,GAAG;CAC1E,CAAC;CAED,MAAM,SAAS,QAAQ;CACvB,IAAI,YAA2B;CAC/B,IAAI,iBAAgC;CAEpC,MAAM,IAAI,kBAAkB,iDAAiD,OAAO,UAAU;EAC1F,IAAI,WAAW,KAAA,KAAa,aAAa,MAAM;GAC3C,MAAM,KAAK,4BAA4B;GACvC;EACJ;EACA,MAAM,cAAc,SAAS,OAAO,MAAM,WACrC,MAAM,SAAS,CAAC,EAAA,CAAG,MAAM,SAAS,KAAK,SAAS,UAAU,CAC/D;EACA,IAAI,OAAO,YAAY,KAAA,KAAa,CAAC,aAAa;GAC9C,MAAM,KAAK,kEAAkE;GAC7E;EACJ;EACA,MAAM,QAAQ,MAAM,QAAQ,aAAa,GAAG;GACxC,QAAQ;GACR,MAAM,KAAK,UAAU,OAAO,WAAW,CAAC,CAAC;EAC7C,CAAC;EACD,MAAM,UAAU,MAAM,cAAc,OAAO,GAAG;EAC9C,IAAI,YAAY,MACZ,MAAM,OACF,QAAQ,WAAW,KAAA,KAAa,OAAO,KAAK,QAAQ,MAAM,CAAC,CAAC,SAAS,GACrE,yDACJ;CAER,CAAC;CAED,MAAM,IAAI,UAAU,8CAA8C,OAAO,UAAU;EAC/E,IAAI,WAAW,KAAA,KAAa,aAAa,MAAM;GAC3C,MAAM,KAAK,4BAA4B;GACvC;EACJ;EACA,MAAM,QAAQ,MAAM,QAAQ,aAAa,GAAG;GACxC,QAAQ;GACR,MAAM,KAAK,UAAU,OAAO,MAAM;EACtC,CAAC;EACD,IAAI,CAAC,MAAM,aAAa,OAAO,GAAG,GAC9B;EAEJ,MAAM,OAAO,MAAM,aAAa,cAAc,MAAM,MAAM,kBAAkB;EAC5E,YAAY,MAAM,KAAK,MAAM;EAC7B,iBAAiB,MAAM,KAAK,WAAW;CAC3C,CAAC;CAED,MAAM,iBAAiB,GAAG,aAAa,EAAE,GAAG,mBAAmB,aAAa,EAAE;CAE9E,MAAM,IAAI,UAAU,qDAAqD,OAAO,UAAU;EACtF,IAAI,cAAc,QAAQ,WAAW,KAAA,GAAW;GAC5C,MAAM,KAAK,sBAAsB;GACjC;EACJ;EACA,MAAM,QAAQ,MAAM,QAAQ,SAAS,GAAG;GACpC,QAAQ;GACR,MAAM,KAAK,UAAU,OAAO,MAAM;GAClC,SAAS,mBAAmB,OAAO,EAAE,YAAY,IAAI,eAAe,GAAG,IAAI,CAAC;EAChF,CAAC;EACD,IAAI,CAAC,MAAM,aAAa,OAAO,GAAG,GAC9B;EAEJ,MAAM,OAAO,MAAM,aAAa,cAAc,MAAM,MAAM,kBAAkB;EAC5E,IAAI,SAAS,QAAQ,mBAAmB,MACpC,MAAM,OACF,KAAK,KAAK,YAAY,gBACtB,6CACJ;CAER,CAAC;CAED,MAAM,IACF,mBACA,2DACA,OAAO,UAAU;EACb,IAAI,cAAc,QAAQ,WAAW,KAAA,GAAW;GAC5C,MAAM,KAAK,sBAAsB;GACjC;EACJ;EACA,IAAI,mBAAmB,MAAM;GACzB,MAAM,KAAK,8BAA8B;GACzC;EACJ;EACA,MAAM,QAAQ,MAAM,QAAQ,SAAS,GAAG;GACpC,QAAQ;GACR,MAAM,KAAK,UAAU,OAAO,MAAM;GAClC,SAAS,EAAE,YAAY,oCAAkC;EAC7D,CAAC;EACD,MAAM,UAAU,MAAM,cAAc,OAAO,GAAG;EAC9C,IAAI,YAAY,MACZ,MAAM,OACF,QAAQ,YAAY,KAAA,GACpB,8CACJ;CAER,CACJ;CAEA,MAAM,IAAI,UAAU,sDAAsD,OAAO,UAAU;EACvF,IAAI,cAAc,MAAM;GACpB,MAAM,KAAK,sBAAsB;GACjC;EACJ;EACA,IAAI,CAAC,MAAM,aAAa,MAAM,QAAQ,SAAS,GAAG,EAAE,QAAQ,SAAS,CAAC,GAAG,GAAG,GACxE;EAEJ,MAAM,cAAc,MAAM,QAAQ,SAAS,CAAC,GAAG,GAAG;CACtD,CAAC;CAED,OAAO;EAAE,QAAQ,OAAO,OAAO,UAAU,MAAM,WAAW,QAAQ;EAAG;CAAO;AAChF;;AAGA,SAAgB,aAAa,QAAmC;CAC5D,MAAM,UAA6C;EAAE,QAAQ;EAAK,QAAQ;EAAK,SAAS;CAAI;CAC5F,OAAO,OAAO,OACT,KAAK,UACF,CACI,GAAG,QAAQ,MAAM,QAAQ,GAAG,MAAM,SAClC,GAAG,MAAM,OAAO,KAAK,UAAU,OAAO,OAAO,CACjD,CAAC,CAAC,KAAK,IAAI,CACf,CAAC,CACA,KAAK,IAAI;AAClB"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/conformance/index.ts"],"sourcesContent":["import type { z } from 'zod';\n\nimport { type FilterRange, toSearchParams } from '../query';\nimport { Definition, type Resource } from '../schemas';\nimport {\n ActionResult,\n FieldCheckResponse,\n ItemResponse,\n ListResponse,\n Problem,\n SearchResponse,\n UploadSignResponse,\n} from '../schemas/api';\nimport {\n SEARCH_DEFAULT_LIMIT,\n SEARCH_MAX_LIMIT,\n type SearchQuery,\n isSearchableResource,\n toSearchQueryParams,\n} from '../search';\nimport { isFilled } from '../values';\nimport { PROBLEM_CONTENT_TYPE } from '../version';\n\nexport interface ConformanceOptions {\n /** Base URL of the API, the same as `api.baseUrl` of the definition, but absolute. */\n baseUrl: string;\n /** The `fetch` to use. Defaults to the global one. */\n fetch?: (input: string, init?: RequestInit) => Promise<Response>;\n /** Merged into every request, for authentication headers or cookies. */\n init?: RequestInit;\n /** Resource to test. Defaults to the first resource of the definition. */\n resource?: string;\n /**\n * Enables the checks that write data, on the tested resource. Use a test database: these\n * checks create, update and delete an item.\n */\n writes?: {\n /** Valid values for a new item. */\n create: Record<string, unknown>;\n /** Valid values for an update. */\n update: Record<string, unknown>;\n /** Values that must be refused with a `422`. Defaults to `{}` when a field is required. */\n invalid?: Record<string, unknown>;\n };\n}\n\nexport type ConformanceStatus = 'passed' | 'failed' | 'skipped';\n\nexport interface ConformanceCheck {\n id: string;\n title: string;\n status: ConformanceStatus;\n /** What went wrong, or why the check was skipped. */\n issues: string[];\n}\n\nexport interface ConformanceReport {\n passed: boolean;\n checks: ConformanceCheck[];\n}\n\ninterface Reply {\n status: number;\n contentType: string;\n etag: string | null;\n body: unknown;\n}\n\nconst UNKNOWN = '__panneau_conformance_unknown__';\n\nfunction describeIssues(error: z.ZodError): string[] {\n return error.issues.slice(0, 10).map((issue) => {\n const path = issue.path.map(String).join('.');\n return path === '' ? issue.message : `${path}: ${issue.message}`;\n });\n}\n\n/** The first text of a value: a string, or the first string of a localized value. */\nfunction textOf(value: unknown): string | null {\n if (typeof value === 'string') {\n return value;\n }\n if (value !== null && typeof value === 'object' && !Array.isArray(value)) {\n for (const entry of Object.values(value)) {\n if (typeof entry === 'string' && entry.trim() !== '') {\n return entry;\n }\n }\n }\n return null;\n}\n\nfunction normalizeEtag(value: string): string {\n return value.replace(/^W\\//, '');\n}\n\nclass Check {\n issues: string[] = [];\n skippedBecause: string | null = null;\n\n expect(condition: boolean, issue: string): boolean {\n if (!condition) {\n this.issues.push(issue);\n }\n return condition;\n }\n\n expectStatus(reply: Reply, status: number): boolean {\n return this.expect(\n reply.status === status,\n `Expected status ${status}, got ${reply.status}.`,\n );\n }\n\n expectSchema<T extends z.ZodType>(schema: T, value: unknown, label: string): z.infer<T> | null {\n const result = schema.safeParse(value);\n if (result.success) {\n return result.data;\n }\n this.issues.push(\n `${label} does not match the schema:`,\n ...describeIssues(result.error).map((issue) => ` ${issue}`),\n );\n return null;\n }\n\n expectProblem(reply: Reply, status: number): Problem | null {\n if (!this.expectStatus(reply, status)) {\n return null;\n }\n this.expect(\n reply.contentType.startsWith(PROBLEM_CONTENT_TYPE),\n `Expected content type ${PROBLEM_CONTENT_TYPE}, got \"${reply.contentType}\".`,\n );\n return this.expectSchema(Problem, reply.body, 'The error');\n }\n\n skip(reason: string) {\n this.skippedBecause = reason;\n }\n}\n\n/**\n * Runs the protocol's conformance checks against a live backend. Read-only checks always run;\n * checks that write data run only with `writes`.\n */\nexport async function runConformance(options: ConformanceOptions): Promise<ConformanceReport> {\n const fetcher = options.fetch ?? ((input: string, init?: RequestInit) => fetch(input, init));\n const baseUrl = options.baseUrl.replace(/\\/$/, '');\n const checks: ConformanceCheck[] = [];\n\n const request = async (path: string, init: RequestInit = {}): Promise<Reply> => {\n const headers = new Headers(options.init?.headers);\n headers.set('Accept', 'application/json');\n new Headers(init.headers).forEach((value, key) => headers.set(key, value));\n if (init.body !== undefined) {\n headers.set('Content-Type', 'application/json');\n }\n const response = await fetcher(`${baseUrl}${path}`, { ...options.init, ...init, headers });\n const text = await response.text();\n const parse = (): unknown => {\n if (text === '') {\n return null;\n }\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n };\n const body = parse();\n return {\n status: response.status,\n contentType: response.headers.get('Content-Type') ?? '',\n etag: response.headers.get('ETag'),\n body,\n };\n };\n\n const run = async (id: string, title: string, body: (check: Check) => Promise<void>) => {\n const check = new Check();\n try {\n await body(check);\n } catch (error) {\n check.issues.push(\n `Request failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n const status: ConformanceStatus =\n check.issues.length > 0\n ? 'failed'\n : check.skippedBecause !== null\n ? 'skipped'\n : 'passed';\n checks.push({\n id,\n title,\n status,\n issues:\n status === 'skipped' && check.skippedBecause !== null\n ? [check.skippedBecause]\n : check.issues,\n });\n };\n\n let definition: Definition | null = null;\n let resource: Resource | null = null;\n let firstItemId: string | null = null;\n\n await run('definition', 'GET /definition returns a valid definition', async (check) => {\n const reply = await request('/definition');\n if (check.expectStatus(reply, 200)) {\n definition = check.expectSchema(Definition, reply.body, 'The definition');\n }\n });\n\n await run('definition.resource', 'The definition has the resource to test', async (check) => {\n const current = definition as Definition | null;\n if (current === null) {\n check.skip('No valid definition.');\n return;\n }\n resource =\n (options.resource !== undefined\n ? current.resources.find((candidate) => candidate.id === options.resource)\n : current.resources[0]) ?? null;\n check.expect(\n resource !== null,\n options.resource !== undefined\n ? `The definition has no resource \"${options.resource}\".`\n : 'The definition has no resources.',\n );\n });\n\n const resourcePath = () =>\n `/resources/${encodeURIComponent((resource as Resource | null)?.id ?? '')}`;\n\n await run('list', 'GET /resources/{resource} returns a page of items', async (check) => {\n if (resource === null) {\n check.skip('No resource to test.');\n return;\n }\n const reply = await request(`${resourcePath()}?page=1&perPage=2`);\n if (!check.expectStatus(reply, 200)) {\n return;\n }\n const list = check.expectSchema(ListResponse, reply.body, 'The list');\n if (list === null) {\n return;\n }\n check.expect(list.data.length <= 2, `perPage=2 returned ${list.data.length} items.`);\n check.expect(list.meta.page === 1, `meta.page is ${list.meta.page}, expected 1.`);\n check.expect(list.meta.perPage === 2, `meta.perPage is ${list.meta.perPage}, expected 2.`);\n firstItemId = list.data[0]?.id ?? null;\n });\n\n await run('list.sort', 'GET /resources/{resource} accepts a sort', async (check) => {\n const sort = (resource as Resource | null)?.list?.sorts?.[0];\n if (sort === undefined) {\n check.skip('The resource declares no sorts.');\n return;\n }\n const reply = await request(`${resourcePath()}?sort=${encodeURIComponent(`-${sort}`)}`);\n if (check.expectStatus(reply, 200)) {\n check.expectSchema(ListResponse, reply.body, 'The sorted list');\n }\n });\n\n await run(\n 'list.filter-range',\n 'GET /resources/{resource} reads the bounds of a range filter',\n async (check) => {\n const filter = (resource as Resource | null)?.list?.filters?.find(\n (candidate) => candidate.type === 'date-range',\n );\n if (filter === undefined) {\n check.skip('The resource declares no date-range filter.');\n return;\n }\n const list = async (range: FilterRange | null, label: string) => {\n const query =\n range !== null ? `?${toSearchParams({ filter: { [filter.id]: range } })}` : '';\n const reply = await request(`${resourcePath()}${query}`);\n if (!check.expectStatus(reply, 200)) {\n return null;\n }\n return check.expectSchema(ListResponse, reply.body, label);\n };\n const all = await list(null, 'The list');\n if (all === null) {\n return;\n }\n // Items of the first page with a value: a range that covers every day includes them.\n const dated = all.data.filter((item) => isFilled(item[filter.id])).length;\n const ranges: [FilterRange, string][] = [\n [{ from: '0001-01-01', to: '9999-12-31' }, 'from 0001-01-01 to 9999-12-31'],\n [{ to: '9999-12-31' }, 'up to 9999-12-31 (an open start)'],\n ];\n for (const [range, label] of ranges) {\n const reply = await list(range, `The list filtered ${label}`);\n if (reply !== null) {\n check.expect(\n reply.meta.total >= dated,\n `Filtered ${label}, the list has ${reply.meta.total} items; the first page alone has ${dated} with a ${filter.id}.`,\n );\n }\n }\n const future = await list({ from: '9999-12-31' }, 'The list filtered from 9999-12-31');\n if (future !== null) {\n check.expect(\n future.meta.total === 0,\n `Filtered from 9999-12-31, the list has ${future.meta.total} items, expected 0.`,\n );\n }\n },\n );\n\n await run(\n 'action.dry-run',\n 'A dry run of an action counts what it would do, and changes nothing',\n async (check) => {\n const action = (resource as Resource | null)?.actions?.find(\n (candidate) =>\n candidate.dryRun === true &&\n candidate.kind === 'server' &&\n candidate.scope.includes('selection'),\n );\n if (action === undefined) {\n check.skip('The resource declares no selection action with dryRun.');\n return;\n }\n const page = async (label: string) => {\n const reply = await request(`${resourcePath()}?page=1&perPage=20`);\n return check.expectStatus(reply, 200)\n ? check.expectSchema(ListResponse, reply.body, label)\n : null;\n };\n const before = await page('The list before the dry run');\n const reply = await request(\n `${resourcePath()}/actions/${encodeURIComponent(action.id)}`,\n {\n method: 'POST',\n body: JSON.stringify({ selection: { query: {} }, dryRun: true }),\n },\n );\n if (!check.expectStatus(reply, 200)) {\n return;\n }\n const result = check.expectSchema(ActionResult, reply.body, 'The dry run result');\n if (result !== null && before !== null) {\n check.expect(\n result.summary !== undefined,\n 'A dry run result must have a summary of the items that would succeed and be skipped.',\n );\n if (result.summary !== undefined) {\n const counted =\n result.summary.succeeded + result.summary.failed + result.summary.skipped;\n check.expect(\n counted === before.meta.total,\n `The dry run counts ${counted} items; the list has ${before.meta.total}.`,\n );\n }\n }\n const after = await page('The list after the dry run');\n if (before !== null && after !== null) {\n const versions = (list: typeof before) =>\n JSON.stringify(list.data.map((item) => [item.id, item.version ?? null]));\n check.expect(\n after.meta.total === before.meta.total && versions(after) === versions(before),\n 'A dry run must not change the items.',\n );\n }\n },\n );\n\n await run(\n 'uploads.sign',\n 'POST /uploads/sign signs the first request of a file, and only the keys of open uploads',\n async (check) => {\n const uploads = (definition as Definition | null)?.uploads;\n if (uploads?.mode !== 'presigned') {\n check.skip('The definition has no presigned uploads.');\n return;\n }\n if (uploads.endpoint !== undefined) {\n check.skip('The upload routes are not at {api.baseUrl}/uploads.');\n return;\n }\n const sign = (body: unknown) =>\n request('/uploads/sign', { method: 'POST', body: JSON.stringify(body) });\n const reply = await sign({ method: 'POST', key: 'panneau-conformance.txt' });\n if (check.expectStatus(reply, 200)) {\n const signed = check.expectSchema(\n UploadSignResponse,\n reply.body,\n 'The signed request',\n );\n if (signed !== null) {\n check.expect(\n signed.key !== undefined && signed.upload !== undefined,\n 'The first request of a file must answer the key and the upload the backend chose.',\n );\n }\n }\n check.expectProblem(\n await sign({\n method: 'PUT',\n key: 'not-an-upload-of-this-user',\n uploadId: 'unknown',\n partNumber: 1,\n }),\n 404,\n );\n check.expectProblem(\n await request(`/uploads/${UNKNOWN}/complete`, { method: 'POST' }),\n 404,\n );\n },\n );\n\n await run(\n 'fields.check',\n 'POST /resources/{resource}/fields/{field}/check checks a value of a field with `check`',\n async (check) => {\n const field = resource?.fields.find((candidate) => candidate.check === true);\n if (resource === null || field === undefined) {\n check.skip('The resource has no field with `check`.');\n return;\n }\n const checkPath = (name: string) =>\n `${resourcePath()}/fields/${encodeURIComponent(name)}/check`;\n const locale = field.localized === true ? definition?.locale : undefined;\n const reply = await request(checkPath(field.name), {\n method: 'POST',\n body: JSON.stringify({\n value: 'panneau-conformance',\n ...(locale !== undefined ? { locale } : {}),\n id: null,\n values: {},\n }),\n });\n if (check.expectStatus(reply, 200)) {\n check.expectSchema(FieldCheckResponse, reply.body, 'The check');\n }\n check.expectProblem(\n await request(checkPath(UNKNOWN), { method: 'POST', body: '{\"value\":\"x\"}' }),\n 404,\n );\n },\n );\n\n await run('list.unknown-resource', 'An unknown resource is a 404 problem', async (check) => {\n check.expectProblem(await request(`/resources/${UNKNOWN}`), 404);\n });\n\n await run(\n 'item',\n 'GET /resources/{resource}/{id} returns the item with its ETag',\n async (check) => {\n if (firstItemId === null) {\n check.skip('The list has no items.');\n return;\n }\n const reply = await request(`${resourcePath()}/${encodeURIComponent(firstItemId)}`);\n if (!check.expectStatus(reply, 200)) {\n return;\n }\n const item = check.expectSchema(ItemResponse, reply.body, 'The item');\n if (item === null) {\n return;\n }\n check.expect(\n item.data.id === firstItemId,\n `Asked for item \"${firstItemId}\", got \"${item.data.id}\".`,\n );\n if (item.data.version !== null) {\n check.expect(\n reply.etag !== null && normalizeEtag(reply.etag) === `\"${item.data.version}\"`,\n `Expected ETag \"${item.data.version}\" (quoted), got ${reply.etag ?? 'none'}.`,\n );\n }\n },\n );\n\n await run('item.missing', 'A missing item is a 404 problem', async (check) => {\n if (resource === null) {\n check.skip('No resource to test.');\n return;\n }\n check.expectProblem(await request(`${resourcePath()}/${UNKNOWN}`), 404);\n });\n\n // The global search: a word of the title of an item of a searchable resource finds it.\n let searched: { resource: Resource; term: string } | null = null;\n const searchSkip = (): string | null => {\n const current = definition as Definition | null;\n if (current === null) {\n return 'No valid definition.';\n }\n if (current.search?.enabled === false) {\n return 'The definition disables the global search.';\n }\n return searched === null\n ? 'No searchable resource has an item with a title to search for.'\n : null;\n };\n const search = (query: SearchQuery) => request(`/search?${toSearchQueryParams(query)}`);\n\n await run('search', 'GET /search finds items of the searchable resources', async (check) => {\n const current = definition as Definition | null;\n const tested = resource as Resource | null;\n if (current === null || current.search?.enabled === false) {\n check.skip(searchSkip() ?? '');\n return;\n }\n const candidates = current.resources\n .filter(\n (candidate) =>\n isSearchableResource(candidate) && candidate.abilities?.viewAny !== false,\n )\n // The tested resource first\n .sort((a, b) => Number(b.id === tested?.id) - Number(a.id === tested?.id));\n for (const candidate of candidates) {\n if (candidate.itemTitle === undefined || searched !== null) {\n continue;\n }\n const reply = await request(\n `/resources/${encodeURIComponent(candidate.id)}?page=1&perPage=1`,\n );\n const list = ListResponse.safeParse(reply.body);\n const title = list.success ? textOf(list.data.data[0]?.[candidate.itemTitle]) : null;\n const term = title?.split(/\\s+/).find((word) => word.length >= 2);\n if (term !== undefined) {\n searched = { resource: candidate, term };\n }\n }\n const reason = searchSkip();\n if (reason !== null || searched === null) {\n check.skip(reason ?? '');\n return;\n }\n const { resource: target, term } = searched as { resource: Resource; term: string };\n const reply = await search({ q: term });\n if (!check.expectStatus(reply, 200)) {\n return;\n }\n const results = check.expectSchema(SearchResponse, reply.body, 'The results');\n if (results === null) {\n return;\n }\n const ids = candidates.map((candidate) => candidate.id);\n for (const group of results.data) {\n check.expect(\n ids.includes(group.resource),\n `Results of \"${group.resource}\", which is not a searchable resource.`,\n );\n check.expect(\n group.items.length <= SEARCH_DEFAULT_LIMIT,\n `\"${group.resource}\" has ${group.items.length} results; without a limit, at most ${SEARCH_DEFAULT_LIMIT}.`,\n );\n }\n const count = results.data.reduce((total, group) => total + group.items.length, 0);\n check.expect(\n count <= SEARCH_MAX_LIMIT,\n `The results have ${count} items, at most ${SEARCH_MAX_LIMIT}.`,\n );\n check.expect(\n results.data.some((group) => group.resource === target.id && group.items.length > 0),\n `Searching \"${term}\", a word of the title of an item of \"${target.id}\", finds none of its items.`,\n );\n });\n\n await run('search.scope', 'GET /search reads limit and resources', async (check) => {\n const reason = searchSkip();\n if (reason !== null || searched === null) {\n check.skip(reason ?? '');\n return;\n }\n const { resource: target, term } = searched as { resource: Resource; term: string };\n const reply = await search({ q: term, limit: 1, resources: [target.id] });\n if (!check.expectStatus(reply, 200)) {\n return;\n }\n const results = check.expectSchema(SearchResponse, reply.body, 'The results');\n if (results !== null) {\n check.expect(\n results.data.length === 1 &&\n results.data[0]?.resource === target.id &&\n results.data[0].items.length === 1,\n `With limit=1&resources=${target.id}, expected one item of \"${target.id}\", got ${JSON.stringify(results.data.map((group) => [group.resource, group.items.length]))}.`,\n );\n }\n });\n\n await run(\n 'search.invalid',\n 'An invalid search is a 422 problem, with errors keyed by parameter',\n async (check) => {\n const reason = searchSkip();\n if (reason !== null || searched === null) {\n check.skip(reason ?? '');\n return;\n }\n const { term } = searched as { resource: Resource; term: string };\n const invalid: [string, SearchQuery][] = [\n ['q', { q: term.slice(0, 1) }],\n ['limit', { q: term, limit: 0 }],\n ['limit', { q: term, limit: SEARCH_MAX_LIMIT + 1 }],\n ['resources', { q: term, resources: [UNKNOWN] }],\n ];\n for (const [parameter, query] of invalid) {\n const failure = check.expectProblem(await search(query), 422);\n if (failure !== null) {\n check.expect(\n failure.errors?.[parameter] !== undefined,\n `${toSearchQueryParams(query)}: the 422 problem must have errors.${parameter}.`,\n );\n }\n }\n },\n );\n\n const writes = options.writes;\n let createdId: string | null = null;\n let createdVersion: string | null = null;\n\n await run('create.invalid', 'Invalid values are refused with a 422 problem', async (check) => {\n if (writes === undefined || resource === null) {\n check.skip('Write checks are disabled.');\n return;\n }\n const hasRequired = resource.fields.some((field) =>\n (field.rules ?? []).some((rule) => rule.rule === 'required'),\n );\n if (writes.invalid === undefined && !hasRequired) {\n check.skip('No invalid values given, and the resource has no required field.');\n return;\n }\n const reply = await request(resourcePath(), {\n method: 'POST',\n body: JSON.stringify(writes.invalid ?? {}),\n });\n const failure = check.expectProblem(reply, 422);\n if (failure !== null) {\n check.expect(\n failure.errors !== undefined && Object.keys(failure.errors).length > 0,\n 'A 422 problem must list the invalid values in `errors`.',\n );\n }\n });\n\n await run('create', 'POST /resources/{resource} creates an item', async (check) => {\n if (writes === undefined || resource === null) {\n check.skip('Write checks are disabled.');\n return;\n }\n const reply = await request(resourcePath(), {\n method: 'POST',\n body: JSON.stringify(writes.create),\n });\n if (!check.expectStatus(reply, 201)) {\n return;\n }\n const item = check.expectSchema(ItemResponse, reply.body, 'The created item');\n createdId = item?.data.id ?? null;\n createdVersion = item?.data.version ?? null;\n });\n\n const itemPath = () => `${resourcePath()}/${encodeURIComponent(createdId ?? '')}`;\n\n await run('update', 'PATCH /resources/{resource}/{id} updates the item', async (check) => {\n if (createdId === null || writes === undefined) {\n check.skip('No item was created.');\n return;\n }\n const reply = await request(itemPath(), {\n method: 'PATCH',\n body: JSON.stringify(writes.update),\n headers: createdVersion !== null ? { 'If-Match': `\"${createdVersion}\"` } : {},\n });\n if (!check.expectStatus(reply, 200)) {\n return;\n }\n const item = check.expectSchema(ItemResponse, reply.body, 'The updated item');\n if (item !== null && createdVersion !== null) {\n check.expect(\n item.data.version !== createdVersion,\n 'The version did not change after an update.',\n );\n }\n });\n\n await run(\n 'update.conflict',\n 'A stale If-Match is a 412 problem with the current item',\n async (check) => {\n if (createdId === null || writes === undefined) {\n check.skip('No item was created.');\n return;\n }\n if (createdVersion === null) {\n check.skip('The backend has no versions.');\n return;\n }\n const reply = await request(itemPath(), {\n method: 'PATCH',\n body: JSON.stringify(writes.update),\n headers: { 'If-Match': '\"__panneau_conformance_stale__\"' },\n });\n const failure = check.expectProblem(reply, 412);\n if (failure !== null) {\n check.expect(\n failure.current !== undefined,\n 'A 412 problem must include the current item.',\n );\n }\n },\n );\n\n await run('delete', 'DELETE /resources/{resource}/{id} deletes the item', async (check) => {\n if (createdId === null) {\n check.skip('No item was created.');\n return;\n }\n if (!check.expectStatus(await request(itemPath(), { method: 'DELETE' }), 204)) {\n return;\n }\n check.expectProblem(await request(itemPath()), 404);\n });\n\n return { passed: checks.every((check) => check.status !== 'failed'), checks };\n}\n\n/** Formats a report as text, one line per check. */\nexport function formatReport(report: ConformanceReport): string {\n const symbols: Record<ConformanceStatus, string> = { passed: '✓', failed: '✗', skipped: '-' };\n return report.checks\n .map((check) =>\n [\n `${symbols[check.status]} ${check.title}`,\n ...check.issues.map((issue) => ` ${issue}`),\n ].join('\\n'),\n )\n .join('\\n');\n}\n"],"mappings":"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"}
@@ -2,10 +2,10 @@
2
2
  export declare const BUILT_IN_ACTIONS: readonly ["show", "edit", "duplicate", "delete", "restore", "export", "import"];
3
3
  export type BuiltInAction = (typeof BUILT_IN_ACTIONS)[number];
4
4
  /** Field types every Panneau frontend renders. Plugins add others. */
5
- export declare const BUILT_IN_FIELD_TYPES: readonly ["text", "textarea", "richtext", "number", "boolean", "date", "datetime", "time", "select", "email", "url", "color", "media", "relation", "group", "repeater", "slug"];
5
+ export declare const BUILT_IN_FIELD_TYPES: readonly ["text", "textarea", "richtext", "number", "boolean", "date", "datetime", "time", "select", "email", "url", "color", "media", "image", "relation", "group", "repeater", "slug"];
6
6
  export type BuiltInFieldType = (typeof BUILT_IN_FIELD_TYPES)[number];
7
7
  /** Validation rules that both frontends and backends check. */
8
- export declare const PORTABLE_RULES: readonly ["required", "min", "max", "pattern", "email", "url", "in", "accept", "maxSize", "before", "after"];
8
+ export declare const PORTABLE_RULES: readonly ["required", "min", "max", "pattern", "email", "url", "in", "accept", "maxSize", "before", "after", "crop"];
9
9
  export type PortableRule = (typeof PORTABLE_RULES)[number];
10
10
  /** Layout node types every Panneau frontend renders. */
11
11
  export declare const BUILT_IN_LAYOUT_NODE_TYPES: readonly ["row", "column", "section", "tabs", "tab", "sidebar", "field", "text", "component"];
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAGA,2BAA2B;AAC3B,eAAO,MAAM,gBAAgB,iFAQnB,CAAC;AACX,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE9D,sEAAsE;AACtE,eAAO,MAAM,oBAAoB,iLAkBvB,CAAC;AACX,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AAErE,+DAA+D;AAC/D,eAAO,MAAM,cAAc,8GAYjB,CAAC;AACX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC;AAE3D,wDAAwD;AACxD,eAAO,MAAM,0BAA0B,+FAU7B,CAAC;AACX,MAAM,MAAM,qBAAqB,GAAG,CAAC,OAAO,0BAA0B,CAAC,CAAC,MAAM,CAAC,CAAC;AAEhF,uEAAuE;AACvE,eAAO,MAAM,qBAAqB,oDAAqD,CAAC;AACxF,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvE;;;GAGG;AACH,eAAO,MAAM,sBAAsB,iBAAiB,CAAC"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAGA,2BAA2B;AAC3B,eAAO,MAAM,gBAAgB,iFAQnB,CAAC;AACX,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE9D,sEAAsE;AACtE,eAAO,MAAM,oBAAoB,0LAmBvB,CAAC;AACX,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AAErE,+DAA+D;AAC/D,eAAO,MAAM,cAAc,sHAajB,CAAC;AACX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC;AAE3D,wDAAwD;AACxD,eAAO,MAAM,0BAA0B,+FAU7B,CAAC;AACX,MAAM,MAAM,qBAAqB,GAAG,CAAC,OAAO,0BAA0B,CAAC,CAAC,MAAM,CAAC,CAAC;AAEhF,uEAAuE;AACvE,eAAO,MAAM,qBAAqB,oDAAqD,CAAC;AACxF,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvE;;;GAGG;AACH,eAAO,MAAM,sBAAsB,iBAAiB,CAAC"}
package/dist/constants.js CHANGED
@@ -24,6 +24,7 @@ var BUILT_IN_FIELD_TYPES = [
24
24
  "url",
25
25
  "color",
26
26
  "media",
27
+ "image",
27
28
  "relation",
28
29
  "group",
29
30
  "repeater",
@@ -41,7 +42,8 @@ var PORTABLE_RULES = [
41
42
  "accept",
42
43
  "maxSize",
43
44
  "before",
44
- "after"
45
+ "after",
46
+ "crop"
45
47
  ];
46
48
  /** Layout node types every Panneau frontend renders. */
47
49
  var BUILT_IN_LAYOUT_NODE_TYPES = [
@@ -1 +1 @@
1
- {"version":3,"file":"constants.js","names":[],"sources":["../src/constants.ts"],"sourcesContent":["// Constants of the protocol. This module doesn't import zod, so that the main entry point stays\n// light: see `src/index.ts`.\n\n/** Built-in action ids. */\nexport const BUILT_IN_ACTIONS = [\n 'show',\n 'edit',\n 'duplicate',\n 'delete',\n 'restore',\n 'export',\n 'import',\n] as const;\nexport type BuiltInAction = (typeof BUILT_IN_ACTIONS)[number];\n\n/** Field types every Panneau frontend renders. Plugins add others. */\nexport const BUILT_IN_FIELD_TYPES = [\n 'text',\n 'textarea',\n 'richtext',\n 'number',\n 'boolean',\n 'date',\n 'datetime',\n 'time',\n 'select',\n 'email',\n 'url',\n 'color',\n 'media',\n 'relation',\n 'group',\n 'repeater',\n 'slug',\n] as const;\nexport type BuiltInFieldType = (typeof BUILT_IN_FIELD_TYPES)[number];\n\n/** Validation rules that both frontends and backends check. */\nexport const PORTABLE_RULES = [\n 'required',\n 'min',\n 'max',\n 'pattern',\n 'email',\n 'url',\n 'in',\n 'accept',\n 'maxSize',\n 'before',\n 'after',\n] as const;\nexport type PortableRule = (typeof PORTABLE_RULES)[number];\n\n/** Layout node types every Panneau frontend renders. */\nexport const BUILT_IN_LAYOUT_NODE_TYPES = [\n 'row',\n 'column',\n 'section',\n 'tabs',\n 'tab',\n 'sidebar',\n 'field',\n 'text',\n 'component',\n] as const;\nexport type BuiltInLayoutNodeType = (typeof BUILT_IN_LAYOUT_NODE_TYPES)[number];\n\n/** List layouts every Panneau frontend renders. Plugins add others. */\nexport const BUILT_IN_LIST_LAYOUTS = ['table', 'cards', 'gallery', 'calendar'] as const;\nexport type BuiltInListLayout = (typeof BUILT_IN_LIST_LAYOUTS)[number];\n\n/**\n * Id of the element that holds the boot config in the page: a\n * `<script type=\"application/json\" id=\"panneau-boot\">` element whose text is the JSON object.\n */\nexport const BOOT_CONFIG_ELEMENT_ID = 'panneau-boot';\n"],"mappings":";;AAIA,IAAa,mBAAmB;CAC5B;CACA;CACA;CACA;CACA;CACA;CACA;AACJ;;AAIA,IAAa,uBAAuB;CAChC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACJ;;AAIA,IAAa,iBAAiB;CAC1B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACJ;;AAIA,IAAa,6BAA6B;CACtC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACJ;;AAIA,IAAa,wBAAwB;CAAC;CAAS;CAAS;CAAW;AAAU;;;;;AAO7E,IAAa,yBAAyB"}
1
+ {"version":3,"file":"constants.js","names":[],"sources":["../src/constants.ts"],"sourcesContent":["// Constants of the protocol. This module doesn't import zod, so that the main entry point stays\n// light: see `src/index.ts`.\n\n/** Built-in action ids. */\nexport const BUILT_IN_ACTIONS = [\n 'show',\n 'edit',\n 'duplicate',\n 'delete',\n 'restore',\n 'export',\n 'import',\n] as const;\nexport type BuiltInAction = (typeof BUILT_IN_ACTIONS)[number];\n\n/** Field types every Panneau frontend renders. Plugins add others. */\nexport const BUILT_IN_FIELD_TYPES = [\n 'text',\n 'textarea',\n 'richtext',\n 'number',\n 'boolean',\n 'date',\n 'datetime',\n 'time',\n 'select',\n 'email',\n 'url',\n 'color',\n 'media',\n 'image',\n 'relation',\n 'group',\n 'repeater',\n 'slug',\n] as const;\nexport type BuiltInFieldType = (typeof BUILT_IN_FIELD_TYPES)[number];\n\n/** Validation rules that both frontends and backends check. */\nexport const PORTABLE_RULES = [\n 'required',\n 'min',\n 'max',\n 'pattern',\n 'email',\n 'url',\n 'in',\n 'accept',\n 'maxSize',\n 'before',\n 'after',\n 'crop',\n] as const;\nexport type PortableRule = (typeof PORTABLE_RULES)[number];\n\n/** Layout node types every Panneau frontend renders. */\nexport const BUILT_IN_LAYOUT_NODE_TYPES = [\n 'row',\n 'column',\n 'section',\n 'tabs',\n 'tab',\n 'sidebar',\n 'field',\n 'text',\n 'component',\n] as const;\nexport type BuiltInLayoutNodeType = (typeof BUILT_IN_LAYOUT_NODE_TYPES)[number];\n\n/** List layouts every Panneau frontend renders. Plugins add others. */\nexport const BUILT_IN_LIST_LAYOUTS = ['table', 'cards', 'gallery', 'calendar'] as const;\nexport type BuiltInListLayout = (typeof BUILT_IN_LIST_LAYOUTS)[number];\n\n/**\n * Id of the element that holds the boot config in the page: a\n * `<script type=\"application/json\" id=\"panneau-boot\">` element whose text is the JSON object.\n */\nexport const BOOT_CONFIG_ELEMENT_ID = 'panneau-boot';\n"],"mappings":";;AAIA,IAAa,mBAAmB;CAC5B;CACA;CACA;CACA;CACA;CACA;CACA;AACJ;;AAIA,IAAa,uBAAuB;CAChC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACJ;;AAIA,IAAa,iBAAiB;CAC1B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACJ;;AAIA,IAAa,6BAA6B;CACtC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACJ;;AAIA,IAAa,wBAAwB;CAAC;CAAS;CAAS;CAAW;AAAU;;;;;AAO7E,IAAa,yBAAyB"}
package/dist/index.js CHANGED
@@ -2,9 +2,12 @@ import { PROBLEM_CONTENT_TYPE, PROTOCOL_VERSION } from "./version.js";
2
2
  import { BOOT_CONFIG_ELEMENT_ID, BUILT_IN_ACTIONS, BUILT_IN_FIELD_TYPES, BUILT_IN_LAYOUT_NODE_TYPES, BUILT_IN_LIST_LAYOUTS, PORTABLE_RULES } from "./constants.js";
3
3
  import { getValue, isFilled } from "./values.js";
4
4
  import { evaluateCondition } from "./conditions.js";
5
+ import { matchesRatio, parseRatio, renderedCropSize } from "./transforms.js";
5
6
  import { checkRules, checkValues } from "./rules.js";
7
+ import { getMediaLibraries } from "./libraries.js";
6
8
  import { lintDefinition } from "./lint.js";
7
9
  import { fromSearchParams, isFilterRange, matchesFilterRange, toSearchParams } from "./query.js";
10
+ import { SEARCH_DEFAULT_LIMIT, SEARCH_MAX_LENGTH, SEARCH_MAX_LIMIT, SEARCH_MIN_LENGTH, isSearchableResource, readSearchQuery, toSearchQueryParams } from "./search.js";
8
11
  import { isProblem } from "./problem.js";
9
12
  import { mergeThemes } from "./themes.js";
10
13
  import { Abilities, Condition, Id, Option, RouteReference } from "./schemas/common.js";
@@ -15,9 +18,9 @@ import { Column, Filter, ListDefinition } from "./schemas/list.js";
15
18
  import { Resource, ResourceFeatures } from "./schemas/resource.js";
16
19
  import { DefinitionTheme, Theme, ThemeFont, ThemeFontFace, ThemeFontStylesheet, ThemeTokens, ToneInput, ToneValues } from "./schemas/theme.js";
17
20
  import { Auth, AuthProvider, Definition, MenuItem, Page, Uploads, User, Widget } from "./schemas/definition.js";
18
- import { ActionItemResult, ActionRequest, ActionResult, ActionRun, ActionRunResponse, Item, ItemResponse, ListResponse, MediaInfo, MediaItem, MediaReference, MediaTransform, Pagination, PreviewResponse, Problem, SearchResponse, Selection, Session, StoredFile, Upload, UploadReference, UploadResponse, UploadSignRequest, UploadSignResponse } from "./schemas/api.js";
21
+ import { ActionItemResult, ActionRequest, ActionResult, ActionRun, ActionRunResponse, FieldCheckRequest, FieldCheckResponse, Item, ItemResponse, ListResponse, MediaInfo, MediaItem, MediaReference, MediaTransform, Pagination, PreviewResponse, Problem, SearchResponse, Selection, Session, StoredFile, Upload, UploadReference, UploadResponse, UploadSignRequest, UploadSignResponse } from "./schemas/api.js";
19
22
  import { BootConfig } from "./schemas/boot.js";
20
23
  import { createSchemaRegistry, namedSchemas } from "./schemas/index.js";
21
24
  import { validate, validateBootConfig, validateDefinition } from "./validate.js";
22
25
  import { createBootConfigJsonSchema, createComponentSchemas, createDefinitionJsonSchema, createOpenApiDocument, createThemeJsonSchema } from "./openapi.js";
23
- export { Abilities, Action, ActionConfirm, ActionItemResult, ActionPlacement, ActionRequest, ActionResult, ActionRun, ActionRunResponse, ActionScope, Auth, AuthProvider, BOOT_CONFIG_ELEMENT_ID, BUILT_IN_ACTIONS, BUILT_IN_FIELD_TYPES, BUILT_IN_LAYOUT_NODE_TYPES, BUILT_IN_LIST_LAYOUTS, BlockType, BootConfig, Column, Condition, Definition, DefinitionTheme, Field, Filter, Id, Item, ItemResponse, LayoutNode, ListDefinition, ListResponse, MediaInfo, MediaItem, MediaReference, MediaTransform, MenuItem, Option, PORTABLE_RULES, PROBLEM_CONTENT_TYPE, PROTOCOL_VERSION, Page, Pagination, PreviewResponse, Problem, Resource, ResourceFeatures, RouteReference, Rule, SearchResponse, Selection, Session, StoredFile, Theme, ThemeFont, ThemeFontFace, ThemeFontStylesheet, ThemeTokens, ToneInput, ToneValues, Upload, UploadReference, UploadResponse, UploadSignRequest, UploadSignResponse, Uploads, User, Widget, checkRules, checkValues, createBootConfigJsonSchema, createComponentSchemas, createDefinitionJsonSchema, createOpenApiDocument, createSchemaRegistry, createThemeJsonSchema, evaluateCondition, fromSearchParams, getValue, isFilled, isFilterRange, isProblem, lintDefinition, matchesFilterRange, mergeThemes, namedSchemas, toSearchParams, validate, validateBootConfig, validateDefinition };
26
+ export { Abilities, Action, ActionConfirm, ActionItemResult, ActionPlacement, ActionRequest, ActionResult, ActionRun, ActionRunResponse, ActionScope, Auth, AuthProvider, BOOT_CONFIG_ELEMENT_ID, BUILT_IN_ACTIONS, BUILT_IN_FIELD_TYPES, BUILT_IN_LAYOUT_NODE_TYPES, BUILT_IN_LIST_LAYOUTS, BlockType, BootConfig, Column, Condition, Definition, DefinitionTheme, Field, FieldCheckRequest, FieldCheckResponse, Filter, Id, Item, ItemResponse, LayoutNode, ListDefinition, ListResponse, MediaInfo, MediaItem, MediaReference, MediaTransform, MenuItem, Option, PORTABLE_RULES, PROBLEM_CONTENT_TYPE, PROTOCOL_VERSION, Page, Pagination, PreviewResponse, Problem, Resource, ResourceFeatures, RouteReference, Rule, SEARCH_DEFAULT_LIMIT, SEARCH_MAX_LENGTH, SEARCH_MAX_LIMIT, SEARCH_MIN_LENGTH, SearchResponse, Selection, Session, StoredFile, Theme, ThemeFont, ThemeFontFace, ThemeFontStylesheet, ThemeTokens, ToneInput, ToneValues, Upload, UploadReference, UploadResponse, UploadSignRequest, UploadSignResponse, Uploads, User, Widget, checkRules, checkValues, createBootConfigJsonSchema, createComponentSchemas, createDefinitionJsonSchema, createOpenApiDocument, createSchemaRegistry, createThemeJsonSchema, evaluateCondition, fromSearchParams, getMediaLibraries, getValue, isFilled, isFilterRange, isProblem, isSearchableResource, lintDefinition, matchesFilterRange, matchesRatio, mergeThemes, namedSchemas, parseRatio, readSearchQuery, renderedCropSize, toSearchParams, toSearchQueryParams, validate, validateBootConfig, validateDefinition };
@@ -0,0 +1,16 @@
1
+ /** What `getMediaLibraries()` reads of a definition. */
2
+ export interface MediaLibrariesDefinition {
3
+ resources: readonly {
4
+ id: string;
5
+ kind?: string | undefined;
6
+ }[];
7
+ }
8
+ /**
9
+ * The ids of the libraries a `media` field allows (decision 0027), in the order of its `resource`:
10
+ * the one it names, the ones it lists, or every resource of kind `media` of the definition when it
11
+ * names none. Ids that aren't libraries are left out: `lintDefinition()` reports them.
12
+ */
13
+ export declare function getMediaLibraries(definition: MediaLibrariesDefinition | null, field: {
14
+ resource?: string | readonly string[] | undefined;
15
+ }): string[];
16
+ //# sourceMappingURL=libraries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"libraries.d.ts","sourceRoot":"","sources":["../src/libraries.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,MAAM,WAAW,wBAAwB;IACrC,SAAS,EAAE,SAAS;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,EAAE,CAAC;CACnE;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC7B,UAAU,EAAE,wBAAwB,GAAG,IAAI,EAC3C,KAAK,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,CAAA;CAAE,GAC7D,MAAM,EAAE,CAUV"}
@@ -0,0 +1,15 @@
1
+ //#region src/libraries.ts
2
+ /**
3
+ * The ids of the libraries a `media` field allows (decision 0027), in the order of its `resource`:
4
+ * the one it names, the ones it lists, or every resource of kind `media` of the definition when it
5
+ * names none. Ids that aren't libraries are left out: `lintDefinition()` reports them.
6
+ */
7
+ function getMediaLibraries(definition, field) {
8
+ const libraries = (definition?.resources ?? []).filter((resource) => resource.kind === "media").map((resource) => resource.id);
9
+ if (field.resource === void 0) return libraries;
10
+ return (typeof field.resource === "string" ? [field.resource] : field.resource).filter((id) => libraries.includes(id));
11
+ }
12
+ //#endregion
13
+ export { getMediaLibraries };
14
+
15
+ //# sourceMappingURL=libraries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"libraries.js","names":[],"sources":["../src/libraries.ts"],"sourcesContent":["/** What `getMediaLibraries()` reads of a definition. */\nexport interface MediaLibrariesDefinition {\n resources: readonly { id: string; kind?: string | undefined }[];\n}\n\n/**\n * The ids of the libraries a `media` field allows (decision 0027), in the order of its `resource`:\n * the one it names, the ones it lists, or every resource of kind `media` of the definition when it\n * names none. Ids that aren't libraries are left out: `lintDefinition()` reports them.\n */\nexport function getMediaLibraries(\n definition: MediaLibrariesDefinition | null,\n field: { resource?: string | readonly string[] | undefined },\n): string[] {\n const libraries = (definition?.resources ?? [])\n .filter((resource) => resource.kind === 'media')\n .map((resource) => resource.id);\n if (field.resource === undefined) {\n return libraries;\n }\n const named: readonly string[] =\n typeof field.resource === 'string' ? [field.resource] : field.resource;\n return named.filter((id) => libraries.includes(id));\n}\n"],"mappings":";;;;;;AAUA,SAAgB,kBACZ,YACA,OACQ;CACR,MAAM,aAAa,YAAY,aAAa,CAAC,EAAA,CACxC,QAAQ,aAAa,SAAS,SAAS,OAAO,CAAC,CAC/C,KAAK,aAAa,SAAS,EAAE;CAClC,IAAI,MAAM,aAAa,KAAA,GACnB,OAAO;CAIX,QADI,OAAO,MAAM,aAAa,WAAW,CAAC,MAAM,QAAQ,IAAI,MAAM,SAAA,CACrD,QAAQ,OAAO,UAAU,SAAS,EAAE,CAAC;AACtD"}
package/dist/lint.d.ts CHANGED
@@ -2,8 +2,8 @@ import type { DefinitionInput } from './schemas';
2
2
  import type { ValidationIssue } from './validate';
3
3
  /**
4
4
  * Checks what the schema can't: references between parts of a definition (layout nodes and
5
- * columns that name existing fields, relations and media fields that name a resource, unique
6
- * names), and the `file` field of `media` resources.
5
+ * columns that name existing fields, relations, `media` and `image` fields that name a resource,
6
+ * unique names), and the `file` field of `media` resources.
7
7
  */
8
8
  export declare function lintDefinition(definition: DefinitionInput): ValidationIssue[];
9
9
  //# sourceMappingURL=lint.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"lint.d.ts","sourceRoot":"","sources":["../src/lint.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAoC,MAAM,WAAW,CAAC;AACnF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AA0FlD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,eAAe,GAAG,eAAe,EAAE,CAyC7E"}
1
+ {"version":3,"file":"lint.d.ts","sourceRoot":"","sources":["../src/lint.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAoC,MAAM,WAAW,CAAC;AACnF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AA+FlD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,eAAe,GAAG,eAAe,EAAE,CAgD7E"}
package/dist/lint.js CHANGED
@@ -59,12 +59,16 @@ function checkResource(resource, path, issues) {
59
59
  path: `${path}.fields.${index}.resource`,
60
60
  message: "A `relation` field needs a `resource`."
61
61
  });
62
+ else if (field.type === "relation" && Array.isArray(field.resource)) issues.push({
63
+ path: `${path}.fields.${index}.resource`,
64
+ message: "A `relation` field names one resource."
65
+ });
62
66
  });
63
67
  }
64
68
  /**
65
69
  * Checks what the schema can't: references between parts of a definition (layout nodes and
66
- * columns that name existing fields, relations and media fields that name a resource, unique
67
- * names), and the `file` field of `media` resources.
70
+ * columns that name existing fields, relations, `media` and `image` fields that name a resource,
71
+ * unique names), and the `file` field of `media` resources.
68
72
  */
69
73
  function lintDefinition(definition) {
70
74
  const issues = [];
@@ -73,20 +77,22 @@ function lintDefinition(definition) {
73
77
  definition.resources.forEach((resource, index) => {
74
78
  checkResource(resource, `resources.${index}`, issues);
75
79
  resource.fields.forEach((field, fieldIndex) => {
76
- if (field.type === "relation" && field.resource !== void 0 && !resourceIds.has(field.resource)) issues.push({
80
+ if (field.type === "relation" && typeof field.resource === "string" && !resourceIds.has(field.resource)) issues.push({
77
81
  path: `resources.${index}.fields.${fieldIndex}.resource`,
78
82
  message: `No resource \`${field.resource}\`.`
79
83
  });
80
- if (field.type === "media") {
84
+ if (field.type === "media" || field.type === "image") {
81
85
  const fieldPath = `resources.${index}.fields.${fieldIndex}.resource`;
82
86
  if (field.resource === void 0) {
83
- if (libraries.length !== 1) issues.push({
87
+ if (libraries.length === 0) issues.push({
84
88
  path: fieldPath,
85
- message: libraries.length === 0 ? "A `media` field needs a resource of kind `media`." : "A `media` field needs a `resource` when there are several libraries."
89
+ message: `${field.type === "image" ? "An" : "A"} \`${field.type}\` field needs a resource of kind \`media\`.`
90
+ });
91
+ } else (typeof field.resource === "string" ? [field.resource] : field.resource).forEach((id, idIndex) => {
92
+ if (!libraries.includes(id)) issues.push({
93
+ path: typeof field.resource === "string" ? fieldPath : `${fieldPath}.${String(idIndex)}`,
94
+ message: `No resource of kind \`media\` named \`${id}\`.`
86
95
  });
87
- } else if (!libraries.includes(field.resource)) issues.push({
88
- path: fieldPath,
89
- message: `No resource of kind \`media\` named \`${field.resource}\`.`
90
96
  });
91
97
  }
92
98
  });
package/dist/lint.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"lint.js","names":[],"sources":["../src/lint.ts"],"sourcesContent":["import type { DefinitionInput, Field, LayoutNode, ResourceInput } from './schemas';\nimport type { ValidationIssue } from './validate';\n\nfunction fieldNames(fields: Field[]): Set<string> {\n return new Set(fields.map((field) => field.name));\n}\n\nfunction checkLayout(\n nodes: LayoutNode[],\n names: Set<string>,\n path: string,\n issues: ValidationIssue[],\n) {\n nodes.forEach((node, index) => {\n const nodePath = `${path}.${index}`;\n if (node.type === 'field') {\n if (node.name === undefined) {\n issues.push({ path: nodePath, message: 'A `field` node needs a `name`.' });\n } else if (!names.has(node.name)) {\n issues.push({\n path: `${nodePath}.name`,\n message: `No field named \\`${node.name}\\`.`,\n });\n }\n }\n if (node.type === 'component' && node.component === undefined) {\n issues.push({ path: nodePath, message: 'A `component` node needs a `component`.' });\n }\n if (node.type === 'tabs' && node.children?.some((child) => child.type !== 'tab')) {\n issues.push({\n path: `${nodePath}.children`,\n message: 'The children of `tabs` must be `tab` nodes.',\n });\n }\n if (node.children !== undefined) {\n checkLayout(node.children, names, `${nodePath}.children`, issues);\n }\n });\n}\n\nfunction checkResource(resource: ResourceInput, path: string, issues: ValidationIssue[]) {\n const names = fieldNames(resource.fields);\n if (names.size !== resource.fields.length) {\n issues.push({ path: `${path}.fields`, message: 'Field names must be unique.' });\n }\n if (resource.form?.layout !== undefined) {\n checkLayout(resource.form.layout, names, `${path}.form.layout`, issues);\n }\n (resource.list?.columns ?? []).forEach((column, index) => {\n if (column.field !== undefined && !names.has(column.field)) {\n issues.push({\n path: `${path}.list.columns.${index}.field`,\n message: `No field named \\`${column.field}\\`.`,\n });\n }\n });\n (resource.actions ?? []).forEach((action, index) => {\n if (action.layout !== undefined) {\n checkLayout(\n action.layout,\n fieldNames(action.fields ?? []),\n `${path}.actions.${index}.layout`,\n issues,\n );\n }\n });\n if (resource.kind === 'media') {\n const file = resource.fields.findIndex((field) => field.name === 'file');\n const field = resource.fields[file];\n if (field === undefined) {\n issues.push({\n path: `${path}.fields`,\n message: 'A resource of kind `media` needs a `file` field.',\n });\n } else if (field.type !== 'file' || field.multiple === true) {\n issues.push({\n path: `${path}.fields.${file}`,\n message: 'The `file` field of a `media` resource must be a single `file`.',\n });\n }\n }\n resource.fields.forEach((field, index) => {\n if (field.type === 'relation' && field.resource === undefined) {\n issues.push({\n path: `${path}.fields.${index}.resource`,\n message: 'A `relation` field needs a `resource`.',\n });\n }\n });\n}\n\n/**\n * Checks what the schema can't: references between parts of a definition (layout nodes and\n * columns that name existing fields, relations and media fields that name a resource, unique\n * names), and the `file` field of `media` resources.\n */\nexport function lintDefinition(definition: DefinitionInput): ValidationIssue[] {\n const issues: ValidationIssue[] = [];\n const resourceIds = new Set(definition.resources.map((resource) => resource.id));\n const libraries = definition.resources\n .filter((resource) => resource.kind === 'media')\n .map((resource) => resource.id);\n definition.resources.forEach((resource, index) => {\n checkResource(resource, `resources.${index}`, issues);\n resource.fields.forEach((field, fieldIndex) => {\n if (\n field.type === 'relation' &&\n field.resource !== undefined &&\n !resourceIds.has(field.resource)\n ) {\n issues.push({\n path: `resources.${index}.fields.${fieldIndex}.resource`,\n message: `No resource \\`${field.resource}\\`.`,\n });\n }\n if (field.type === 'media') {\n const fieldPath = `resources.${index}.fields.${fieldIndex}.resource`;\n if (field.resource === undefined) {\n if (libraries.length !== 1) {\n issues.push({\n path: fieldPath,\n message:\n libraries.length === 0\n ? 'A `media` field needs a resource of kind `media`.'\n : 'A `media` field needs a `resource` when there are several libraries.',\n });\n }\n } else if (!libraries.includes(field.resource)) {\n issues.push({\n path: fieldPath,\n message: `No resource of kind \\`media\\` named \\`${field.resource}\\`.`,\n });\n }\n }\n });\n });\n return issues;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"lint.js","names":[],"sources":["../src/lint.ts"],"sourcesContent":["import type { DefinitionInput, Field, LayoutNode, ResourceInput } from './schemas';\nimport type { ValidationIssue } from './validate';\n\nfunction fieldNames(fields: Field[]): Set<string> {\n return new Set(fields.map((field) => field.name));\n}\n\nfunction checkLayout(\n nodes: LayoutNode[],\n names: Set<string>,\n path: string,\n issues: ValidationIssue[],\n) {\n nodes.forEach((node, index) => {\n const nodePath = `${path}.${index}`;\n if (node.type === 'field') {\n if (node.name === undefined) {\n issues.push({ path: nodePath, message: 'A `field` node needs a `name`.' });\n } else if (!names.has(node.name)) {\n issues.push({\n path: `${nodePath}.name`,\n message: `No field named \\`${node.name}\\`.`,\n });\n }\n }\n if (node.type === 'component' && node.component === undefined) {\n issues.push({ path: nodePath, message: 'A `component` node needs a `component`.' });\n }\n if (node.type === 'tabs' && node.children?.some((child) => child.type !== 'tab')) {\n issues.push({\n path: `${nodePath}.children`,\n message: 'The children of `tabs` must be `tab` nodes.',\n });\n }\n if (node.children !== undefined) {\n checkLayout(node.children, names, `${nodePath}.children`, issues);\n }\n });\n}\n\nfunction checkResource(resource: ResourceInput, path: string, issues: ValidationIssue[]) {\n const names = fieldNames(resource.fields);\n if (names.size !== resource.fields.length) {\n issues.push({ path: `${path}.fields`, message: 'Field names must be unique.' });\n }\n if (resource.form?.layout !== undefined) {\n checkLayout(resource.form.layout, names, `${path}.form.layout`, issues);\n }\n (resource.list?.columns ?? []).forEach((column, index) => {\n if (column.field !== undefined && !names.has(column.field)) {\n issues.push({\n path: `${path}.list.columns.${index}.field`,\n message: `No field named \\`${column.field}\\`.`,\n });\n }\n });\n (resource.actions ?? []).forEach((action, index) => {\n if (action.layout !== undefined) {\n checkLayout(\n action.layout,\n fieldNames(action.fields ?? []),\n `${path}.actions.${index}.layout`,\n issues,\n );\n }\n });\n if (resource.kind === 'media') {\n const file = resource.fields.findIndex((field) => field.name === 'file');\n const field = resource.fields[file];\n if (field === undefined) {\n issues.push({\n path: `${path}.fields`,\n message: 'A resource of kind `media` needs a `file` field.',\n });\n } else if (field.type !== 'file' || field.multiple === true) {\n issues.push({\n path: `${path}.fields.${file}`,\n message: 'The `file` field of a `media` resource must be a single `file`.',\n });\n }\n }\n resource.fields.forEach((field, index) => {\n if (field.type === 'relation' && field.resource === undefined) {\n issues.push({\n path: `${path}.fields.${index}.resource`,\n message: 'A `relation` field needs a `resource`.',\n });\n } else if (field.type === 'relation' && Array.isArray(field.resource)) {\n issues.push({\n path: `${path}.fields.${index}.resource`,\n message: 'A `relation` field names one resource.',\n });\n }\n });\n}\n\n/**\n * Checks what the schema can't: references between parts of a definition (layout nodes and\n * columns that name existing fields, relations, `media` and `image` fields that name a resource,\n * unique names), and the `file` field of `media` resources.\n */\nexport function lintDefinition(definition: DefinitionInput): ValidationIssue[] {\n const issues: ValidationIssue[] = [];\n const resourceIds = new Set(definition.resources.map((resource) => resource.id));\n const libraries = definition.resources\n .filter((resource) => resource.kind === 'media')\n .map((resource) => resource.id);\n definition.resources.forEach((resource, index) => {\n checkResource(resource, `resources.${index}`, issues);\n resource.fields.forEach((field, fieldIndex) => {\n if (\n field.type === 'relation' &&\n typeof field.resource === 'string' &&\n !resourceIds.has(field.resource)\n ) {\n issues.push({\n path: `resources.${index}.fields.${fieldIndex}.resource`,\n message: `No resource \\`${field.resource}\\`.`,\n });\n }\n if (field.type === 'media' || field.type === 'image') {\n const fieldPath = `resources.${index}.fields.${fieldIndex}.resource`;\n if (field.resource === undefined) {\n // Every library of the definition (decision 0027).\n if (libraries.length === 0) {\n issues.push({\n path: fieldPath,\n message: `${field.type === 'image' ? 'An' : 'A'} \\`${field.type}\\` field needs a resource of kind \\`media\\`.`,\n });\n }\n } else {\n const named =\n typeof field.resource === 'string' ? [field.resource] : field.resource;\n named.forEach((id, idIndex) => {\n if (!libraries.includes(id)) {\n issues.push({\n path:\n typeof field.resource === 'string'\n ? fieldPath\n : `${fieldPath}.${String(idIndex)}`,\n message: `No resource of kind \\`media\\` named \\`${id}\\`.`,\n });\n }\n });\n }\n }\n });\n });\n return issues;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"example.d.ts","sourceRoot":"","sources":["../../src/mock/example.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD;;;;;GAKG;AACH,wBAAgB,uBAAuB,IAAI,eAAe,CA0LzD"}
1
+ {"version":3,"file":"example.d.ts","sourceRoot":"","sources":["../../src/mock/example.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD;;;;;GAKG;AACH,wBAAgB,uBAAuB,IAAI,eAAe,CAkMzD"}
@@ -95,6 +95,14 @@ function createExampleDefinition() {
95
95
  rule: "accept",
96
96
  value: ["application/pdf"]
97
97
  }]
98
+ },
99
+ {
100
+ name: "slug",
101
+ type: "slug",
102
+ from: "title",
103
+ localized: true,
104
+ check: true,
105
+ settings: { prefix: "https://example.com/" }
98
106
  }
99
107
  ],
100
108
  form: { layout: [{