@panneau/protocol 5.0.0-alpha.2 → 5.0.0-alpha.4

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 (44) hide show
  1. package/dist/conformance/index.d.ts.map +1 -1
  2. package/dist/conformance/index.js +64 -1
  3. package/dist/conformance/index.js.map +1 -1
  4. package/dist/index.js +2 -2
  5. package/dist/lint.d.ts +2 -1
  6. package/dist/lint.d.ts.map +1 -1
  7. package/dist/lint.js +27 -1
  8. package/dist/lint.js.map +1 -1
  9. package/dist/mock/example.d.ts +2 -1
  10. package/dist/mock/example.d.ts.map +1 -1
  11. package/dist/mock/example.js +218 -141
  12. package/dist/mock/example.js.map +1 -1
  13. package/dist/mock/index.d.ts.map +1 -1
  14. package/dist/mock/index.js +165 -6
  15. package/dist/mock/index.js.map +1 -1
  16. package/dist/openapi.d.ts.map +1 -1
  17. package/dist/openapi.js +20 -95
  18. package/dist/openapi.js.map +1 -1
  19. package/dist/schemas/action.d.ts +1 -0
  20. package/dist/schemas/action.d.ts.map +1 -1
  21. package/dist/schemas/action.js +1 -0
  22. package/dist/schemas/action.js.map +1 -1
  23. package/dist/schemas/api.d.ts +114 -82
  24. package/dist/schemas/api.d.ts.map +1 -1
  25. package/dist/schemas/api.js +70 -27
  26. package/dist/schemas/api.js.map +1 -1
  27. package/dist/schemas/definition.d.ts +2 -0
  28. package/dist/schemas/definition.d.ts.map +1 -1
  29. package/dist/schemas/definition.js +2 -2
  30. package/dist/schemas/definition.js.map +1 -1
  31. package/dist/schemas/field.js +3 -3
  32. package/dist/schemas/field.js.map +1 -1
  33. package/dist/schemas/index.d.ts +110 -79
  34. package/dist/schemas/index.d.ts.map +1 -1
  35. package/dist/schemas/index.js +11 -5
  36. package/dist/schemas/index.js.map +1 -1
  37. package/dist/schemas/resource.d.ts +2 -0
  38. package/dist/schemas/resource.d.ts.map +1 -1
  39. package/dist/schemas/resource.js +1 -0
  40. package/dist/schemas/resource.js.map +1 -1
  41. package/package.json +1 -1
  42. package/schema/boot.schema.json +7 -0
  43. package/schema/definition.schema.json +7 -0
  44. package/schema/openapi.json +317 -462
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/conformance/index.ts"],"names":[],"mappings":"AAQA,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,CAiU5F;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":"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"}
@@ -2,7 +2,7 @@ import { PROBLEM_CONTENT_TYPE } from "../version.js";
2
2
  import { isFilled } from "../values.js";
3
3
  import { toSearchParams } from "../query.js";
4
4
  import { Definition } from "../schemas/definition.js";
5
- import { ItemResponse, ListResponse, Problem } from "../schemas/api.js";
5
+ import { ActionResult, ItemResponse, ListResponse, Problem, UploadSignResponse } from "../schemas/api.js";
6
6
  //#region src/conformance/index.ts
7
7
  var UNKNOWN = "__panneau_conformance_unknown__";
8
8
  function describeIssues(error) {
@@ -154,6 +154,69 @@ async function runConformance(options) {
154
154
  const future = await list({ from: "9999-12-31" }, "The list filtered from 9999-12-31");
155
155
  if (future !== null) check.expect(future.meta.total === 0, `Filtered from 9999-12-31, the list has ${future.meta.total} items, expected 0.`);
156
156
  });
157
+ await run("action.dry-run", "A dry run of an action counts what it would do, and changes nothing", async (check) => {
158
+ const action = resource?.actions?.find((candidate) => candidate.dryRun === true && candidate.kind === "server" && candidate.scope.includes("selection"));
159
+ if (action === void 0) {
160
+ check.skip("The resource declares no selection action with dryRun.");
161
+ return;
162
+ }
163
+ const page = async (label) => {
164
+ const reply = await request(`${resourcePath()}?page=1&perPage=20`);
165
+ return check.expectStatus(reply, 200) ? check.expectSchema(ListResponse, reply.body, label) : null;
166
+ };
167
+ const before = await page("The list before the dry run");
168
+ const reply = await request(`${resourcePath()}/actions/${encodeURIComponent(action.id)}`, {
169
+ method: "POST",
170
+ body: JSON.stringify({
171
+ selection: { query: {} },
172
+ dryRun: true
173
+ })
174
+ });
175
+ if (!check.expectStatus(reply, 200)) return;
176
+ const result = check.expectSchema(ActionResult, reply.body, "The dry run result");
177
+ if (result !== null && before !== null) {
178
+ check.expect(result.summary !== void 0, "A dry run result must have a summary of the items that would succeed and be skipped.");
179
+ if (result.summary !== void 0) {
180
+ const counted = result.summary.succeeded + result.summary.failed + result.summary.skipped;
181
+ check.expect(counted === before.meta.total, `The dry run counts ${counted} items; the list has ${before.meta.total}.`);
182
+ }
183
+ }
184
+ const after = await page("The list after the dry run");
185
+ if (before !== null && after !== null) {
186
+ const versions = (list) => JSON.stringify(list.data.map((item) => [item.id, item.version ?? null]));
187
+ check.expect(after.meta.total === before.meta.total && versions(after) === versions(before), "A dry run must not change the items.");
188
+ }
189
+ });
190
+ await run("uploads.sign", "POST /uploads/sign signs the first request of a file, and only the keys of open uploads", async (check) => {
191
+ const uploads = definition?.uploads;
192
+ if (uploads?.mode !== "presigned") {
193
+ check.skip("The definition has no presigned uploads.");
194
+ return;
195
+ }
196
+ if (uploads.endpoint !== void 0) {
197
+ check.skip("The upload routes are not at {api.baseUrl}/uploads.");
198
+ return;
199
+ }
200
+ const sign = (body) => request("/uploads/sign", {
201
+ method: "POST",
202
+ body: JSON.stringify(body)
203
+ });
204
+ const reply = await sign({
205
+ method: "POST",
206
+ key: "panneau-conformance.txt"
207
+ });
208
+ if (check.expectStatus(reply, 200)) {
209
+ const signed = check.expectSchema(UploadSignResponse, reply.body, "The signed request");
210
+ if (signed !== null) check.expect(signed.key !== void 0 && signed.upload !== void 0, "The first request of a file must answer the key and the upload the backend chose.");
211
+ }
212
+ check.expectProblem(await sign({
213
+ method: "PUT",
214
+ key: "not-an-upload-of-this-user",
215
+ uploadId: "unknown",
216
+ partNumber: 1
217
+ }), 404);
218
+ check.expectProblem(await request(`/uploads/${UNKNOWN}/complete`, { method: "POST" }), 404);
219
+ });
157
220
  await run("list.unknown-resource", "An unknown resource is a 404 problem", async (check) => {
158
221
  check.expectProblem(await request(`/resources/${UNKNOWN}`), 404);
159
222
  });
@@ -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 { ItemResponse, ListResponse, Problem } 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('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":"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"}
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":"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"}
package/dist/index.js CHANGED
@@ -15,9 +15,9 @@ import { Column, Filter, ListDefinition } from "./schemas/list.js";
15
15
  import { Resource, ResourceFeatures } from "./schemas/resource.js";
16
16
  import { DefinitionTheme, Theme, ThemeFont, ThemeFontFace, ThemeFontStylesheet, ThemeTokens, ToneInput, ToneValues } from "./schemas/theme.js";
17
17
  import { Auth, AuthProvider, Definition, MenuItem, Page, Uploads, User, Widget } from "./schemas/definition.js";
18
- import { ActionItemResult, ActionRequest, ActionResult, ActionRun, ActionRunResponse, Item, ItemResponse, ListResponse, Media, MediaListResponse, MediaResponse, MediaTransform, Pagination, PreviewResponse, Problem, SearchResponse, Selection, Session } from "./schemas/api.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";
19
19
  import { BootConfig } from "./schemas/boot.js";
20
20
  import { createSchemaRegistry, namedSchemas } from "./schemas/index.js";
21
21
  import { validate, validateBootConfig, validateDefinition } from "./validate.js";
22
22
  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, Media, MediaListResponse, MediaResponse, MediaTransform, MenuItem, Option, PORTABLE_RULES, PROBLEM_CONTENT_TYPE, PROTOCOL_VERSION, Page, Pagination, PreviewResponse, Problem, Resource, ResourceFeatures, RouteReference, Rule, SearchResponse, Selection, Session, Theme, ThemeFont, ThemeFontFace, ThemeFontStylesheet, ThemeTokens, ToneInput, ToneValues, 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 };
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 };
package/dist/lint.d.ts CHANGED
@@ -2,7 +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 that name a resource, unique names).
5
+ * columns that name existing fields, relations and media fields that name a resource, unique
6
+ * names), and the `file` field of `media` resources.
6
7
  */
7
8
  export declare function lintDefinition(definition: DefinitionInput): ValidationIssue[];
8
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;AA2ElD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,eAAe,GAAG,eAAe,EAAE,CAmB7E"}
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"}
package/dist/lint.js CHANGED
@@ -42,6 +42,18 @@ function checkResource(resource, path, issues) {
42
42
  (resource.actions ?? []).forEach((action, index) => {
43
43
  if (action.layout !== void 0) checkLayout(action.layout, fieldNames(action.fields ?? []), `${path}.actions.${index}.layout`, issues);
44
44
  });
45
+ if (resource.kind === "media") {
46
+ const file = resource.fields.findIndex((field) => field.name === "file");
47
+ const field = resource.fields[file];
48
+ if (field === void 0) issues.push({
49
+ path: `${path}.fields`,
50
+ message: "A resource of kind `media` needs a `file` field."
51
+ });
52
+ else if (field.type !== "file" || field.multiple === true) issues.push({
53
+ path: `${path}.fields.${file}`,
54
+ message: "The `file` field of a `media` resource must be a single `file`."
55
+ });
56
+ }
45
57
  resource.fields.forEach((field, index) => {
46
58
  if (field.type === "relation" && field.resource === void 0) issues.push({
47
59
  path: `${path}.fields.${index}.resource`,
@@ -51,11 +63,13 @@ function checkResource(resource, path, issues) {
51
63
  }
52
64
  /**
53
65
  * Checks what the schema can't: references between parts of a definition (layout nodes and
54
- * columns that name existing fields, relations that name a resource, unique names).
66
+ * columns that name existing fields, relations and media fields that name a resource, unique
67
+ * names), and the `file` field of `media` resources.
55
68
  */
56
69
  function lintDefinition(definition) {
57
70
  const issues = [];
58
71
  const resourceIds = new Set(definition.resources.map((resource) => resource.id));
72
+ const libraries = definition.resources.filter((resource) => resource.kind === "media").map((resource) => resource.id);
59
73
  definition.resources.forEach((resource, index) => {
60
74
  checkResource(resource, `resources.${index}`, issues);
61
75
  resource.fields.forEach((field, fieldIndex) => {
@@ -63,6 +77,18 @@ function lintDefinition(definition) {
63
77
  path: `resources.${index}.fields.${fieldIndex}.resource`,
64
78
  message: `No resource \`${field.resource}\`.`
65
79
  });
80
+ if (field.type === "media") {
81
+ const fieldPath = `resources.${index}.fields.${fieldIndex}.resource`;
82
+ if (field.resource === void 0) {
83
+ if (libraries.length !== 1) issues.push({
84
+ 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."
86
+ });
87
+ } else if (!libraries.includes(field.resource)) issues.push({
88
+ path: fieldPath,
89
+ message: `No resource of kind \`media\` named \`${field.resource}\`.`
90
+ });
91
+ }
66
92
  });
67
93
  });
68
94
  return issues;
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 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 that name a resource, unique names).\n */\nexport function lintDefinition(definition: DefinitionInput): ValidationIssue[] {\n const issues: ValidationIssue[] = [];\n const resourceIds = new Set(definition.resources.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 });\n });\n return issues;\n}\n"],"mappings":";AAGA,SAAS,WAAW,QAA8B;CAC9C,OAAO,IAAI,IAAI,OAAO,KAAK,UAAU,MAAM,IAAI,CAAC;AACpD;AAEA,SAAS,YACL,OACA,OACA,MACA,QACF;CACE,MAAM,SAAS,MAAM,UAAU;EAC3B,MAAM,WAAW,GAAG,KAAK,GAAG;EAC5B,IAAI,KAAK,SAAS,SAAS;GACvB,IAAI,KAAK,SAAS,KAAA,GACd,OAAO,KAAK;IAAE,MAAM;IAAU,SAAS;GAAiC,CAAC;QACtE,IAAI,CAAC,MAAM,IAAI,KAAK,IAAI,GAC3B,OAAO,KAAK;IACR,MAAM,GAAG,SAAS;IAClB,SAAS,oBAAoB,KAAK,KAAK;GAC3C,CAAC;EAET;EACA,IAAI,KAAK,SAAS,eAAe,KAAK,cAAc,KAAA,GAChD,OAAO,KAAK;GAAE,MAAM;GAAU,SAAS;EAA0C,CAAC;EAEtF,IAAI,KAAK,SAAS,UAAU,KAAK,UAAU,MAAM,UAAU,MAAM,SAAS,KAAK,GAC3E,OAAO,KAAK;GACR,MAAM,GAAG,SAAS;GAClB,SAAS;EACb,CAAC;EAEL,IAAI,KAAK,aAAa,KAAA,GAClB,YAAY,KAAK,UAAU,OAAO,GAAG,SAAS,YAAY,MAAM;CAExE,CAAC;AACL;AAEA,SAAS,cAAc,UAAyB,MAAc,QAA2B;CACrF,MAAM,QAAQ,WAAW,SAAS,MAAM;CACxC,IAAI,MAAM,SAAS,SAAS,OAAO,QAC/B,OAAO,KAAK;EAAE,MAAM,GAAG,KAAK;EAAU,SAAS;CAA8B,CAAC;CAElF,IAAI,SAAS,MAAM,WAAW,KAAA,GAC1B,YAAY,SAAS,KAAK,QAAQ,OAAO,GAAG,KAAK,eAAe,MAAM;CAE1E,CAAC,SAAS,MAAM,WAAW,CAAC,EAAA,CAAG,SAAS,QAAQ,UAAU;EACtD,IAAI,OAAO,UAAU,KAAA,KAAa,CAAC,MAAM,IAAI,OAAO,KAAK,GACrD,OAAO,KAAK;GACR,MAAM,GAAG,KAAK,gBAAgB,MAAM;GACpC,SAAS,oBAAoB,OAAO,MAAM;EAC9C,CAAC;CAET,CAAC;CACD,CAAC,SAAS,WAAW,CAAC,EAAA,CAAG,SAAS,QAAQ,UAAU;EAChD,IAAI,OAAO,WAAW,KAAA,GAClB,YACI,OAAO,QACP,WAAW,OAAO,UAAU,CAAC,CAAC,GAC9B,GAAG,KAAK,WAAW,MAAM,UACzB,MACJ;CAER,CAAC;CACD,SAAS,OAAO,SAAS,OAAO,UAAU;EACtC,IAAI,MAAM,SAAS,cAAc,MAAM,aAAa,KAAA,GAChD,OAAO,KAAK;GACR,MAAM,GAAG,KAAK,UAAU,MAAM;GAC9B,SAAS;EACb,CAAC;CAET,CAAC;AACL;;;;;AAMA,SAAgB,eAAe,YAAgD;CAC3E,MAAM,SAA4B,CAAC;CACnC,MAAM,cAAc,IAAI,IAAI,WAAW,UAAU,KAAK,aAAa,SAAS,EAAE,CAAC;CAC/E,WAAW,UAAU,SAAS,UAAU,UAAU;EAC9C,cAAc,UAAU,aAAa,SAAS,MAAM;EACpD,SAAS,OAAO,SAAS,OAAO,eAAe;GAC3C,IACI,MAAM,SAAS,cACf,MAAM,aAAa,KAAA,KACnB,CAAC,YAAY,IAAI,MAAM,QAAQ,GAE/B,OAAO,KAAK;IACR,MAAM,aAAa,MAAM,UAAU,WAAW;IAC9C,SAAS,iBAAiB,MAAM,SAAS;GAC7C,CAAC;EAET,CAAC;CACL,CAAC;CACD,OAAO;AACX"}
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,7 +1,8 @@
1
1
  import type { DefinitionInput } from '../schemas';
2
2
  /**
3
3
  * A small but complete definition: pages with localized titles, a status, a publication date shown
4
- * for published pages, an author and typed blocks, in tabs and a sidebar, plus authors. For tests
4
+ * for published pages, an author, an attachment and typed blocks, in tabs and a sidebar, plus
5
+ * authors with a photo, and a media library. For tests
5
6
  * and stories, with `createMockBackend()`.
6
7
  */
7
8
  export declare function createExampleDefinition(): DefinitionInput;
@@ -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;;;;GAIG;AACH,wBAAgB,uBAAuB,IAAI,eAAe,CA0IzD"}
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"}