@panneau/protocol 5.0.0-alpha.3 → 5.0.0-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/conformance/index.d.ts.map +1 -1
- package/dist/conformance/index.js +151 -1
- package/dist/conformance/index.js.map +1 -1
- package/dist/constants.d.ts +2 -2
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +3 -1
- package/dist/constants.js.map +1 -1
- package/dist/index.js +5 -2
- package/dist/libraries.d.ts +16 -0
- package/dist/libraries.d.ts.map +1 -0
- package/dist/libraries.js +15 -0
- package/dist/libraries.js.map +1 -0
- package/dist/lint.d.ts +2 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +34 -2
- package/dist/lint.js.map +1 -1
- package/dist/mock/example.d.ts +2 -1
- package/dist/mock/example.d.ts.map +1 -1
- package/dist/mock/example.js +227 -143
- package/dist/mock/example.js.map +1 -1
- package/dist/mock/index.d.ts +9 -1
- package/dist/mock/index.d.ts.map +1 -1
- package/dist/mock/index.js +257 -11
- package/dist/mock/index.js.map +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/openapi.js +55 -101
- package/dist/openapi.js.map +1 -1
- package/dist/rules.d.ts.map +1 -1
- package/dist/rules.js +17 -0
- package/dist/rules.js.map +1 -1
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +4 -1
- package/dist/schemas/action.d.ts +17 -1
- package/dist/schemas/action.d.ts.map +1 -1
- package/dist/schemas/api.d.ts +155 -37
- package/dist/schemas/api.d.ts.map +1 -1
- package/dist/schemas/api.js +89 -28
- package/dist/schemas/api.js.map +1 -1
- package/dist/schemas/definition.d.ts +39 -2
- package/dist/schemas/definition.d.ts.map +1 -1
- package/dist/schemas/definition.js +4 -3
- package/dist/schemas/definition.js.map +1 -1
- package/dist/schemas/field.d.ts +24 -1
- package/dist/schemas/field.d.ts.map +1 -1
- package/dist/schemas/field.js +22 -4
- package/dist/schemas/field.js.map +1 -1
- package/dist/schemas/index.d.ts +262 -40
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +13 -5
- package/dist/schemas/index.js.map +1 -1
- package/dist/schemas/resource.d.ts +37 -2
- package/dist/schemas/resource.d.ts.map +1 -1
- package/dist/schemas/resource.js +4 -1
- package/dist/schemas/resource.js.map +1 -1
- package/dist/search.d.ts +51 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +53 -0
- package/dist/search.js.map +1 -0
- package/dist/transforms.d.ts +17 -0
- package/dist/transforms.d.ts.map +1 -0
- package/dist/transforms.js +34 -0
- package/dist/transforms.js.map +1 -0
- package/package.json +1 -1
- package/schema/boot.schema.json +84 -1
- package/schema/definition.schema.json +84 -1
- package/schema/openapi.json +771 -605
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/conformance/index.ts"],"names":[],"mappings":"
|
|
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 } 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
|
}
|
|
@@ -187,6 +196,59 @@ async function runConformance(options) {
|
|
|
187
196
|
check.expect(after.meta.total === before.meta.total && versions(after) === versions(before), "A dry run must not change the items.");
|
|
188
197
|
}
|
|
189
198
|
});
|
|
199
|
+
await run("uploads.sign", "POST /uploads/sign signs the first request of a file, and only the keys of open uploads", async (check) => {
|
|
200
|
+
const uploads = definition?.uploads;
|
|
201
|
+
if (uploads?.mode !== "presigned") {
|
|
202
|
+
check.skip("The definition has no presigned uploads.");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (uploads.endpoint !== void 0) {
|
|
206
|
+
check.skip("The upload routes are not at {api.baseUrl}/uploads.");
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const sign = (body) => request("/uploads/sign", {
|
|
210
|
+
method: "POST",
|
|
211
|
+
body: JSON.stringify(body)
|
|
212
|
+
});
|
|
213
|
+
const reply = await sign({
|
|
214
|
+
method: "POST",
|
|
215
|
+
key: "panneau-conformance.txt"
|
|
216
|
+
});
|
|
217
|
+
if (check.expectStatus(reply, 200)) {
|
|
218
|
+
const signed = check.expectSchema(UploadSignResponse, reply.body, "The signed request");
|
|
219
|
+
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.");
|
|
220
|
+
}
|
|
221
|
+
check.expectProblem(await sign({
|
|
222
|
+
method: "PUT",
|
|
223
|
+
key: "not-an-upload-of-this-user",
|
|
224
|
+
uploadId: "unknown",
|
|
225
|
+
partNumber: 1
|
|
226
|
+
}), 404);
|
|
227
|
+
check.expectProblem(await request(`/uploads/${UNKNOWN}/complete`, { method: "POST" }), 404);
|
|
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
|
+
});
|
|
190
252
|
await run("list.unknown-resource", "An unknown resource is a 404 problem", async (check) => {
|
|
191
253
|
check.expectProblem(await request(`/resources/${UNKNOWN}`), 404);
|
|
192
254
|
});
|
|
@@ -209,6 +271,94 @@ async function runConformance(options) {
|
|
|
209
271
|
}
|
|
210
272
|
check.expectProblem(await request(`${resourcePath()}/${UNKNOWN}`), 404);
|
|
211
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
|
+
});
|
|
212
362
|
const writes = options.writes;
|
|
213
363
|
let createdId = null;
|
|
214
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 { ActionResult, 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(\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('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 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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"}
|
package/dist/constants.d.ts
CHANGED
|
@@ -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"];
|
package/dist/constants.d.ts.map
CHANGED
|
@@ -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,
|
|
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 = [
|
package/dist/constants.js.map
CHANGED
|
@@ -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,
|
|
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,
|
|
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,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,
|
|
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.
|
|
6
7
|
*/
|
|
7
8
|
export declare function lintDefinition(definition: DefinitionInput): ValidationIssue[];
|
|
8
9
|
//# sourceMappingURL=lint.d.ts.map
|
package/dist/lint.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
@@ -42,27 +42,59 @@ 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`,
|
|
48
60
|
message: "A `relation` field needs a `resource`."
|
|
49
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
|
+
});
|
|
50
66
|
});
|
|
51
67
|
}
|
|
52
68
|
/**
|
|
53
69
|
* 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,
|
|
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.
|
|
55
72
|
*/
|
|
56
73
|
function lintDefinition(definition) {
|
|
57
74
|
const issues = [];
|
|
58
75
|
const resourceIds = new Set(definition.resources.map((resource) => resource.id));
|
|
76
|
+
const libraries = definition.resources.filter((resource) => resource.kind === "media").map((resource) => resource.id);
|
|
59
77
|
definition.resources.forEach((resource, index) => {
|
|
60
78
|
checkResource(resource, `resources.${index}`, issues);
|
|
61
79
|
resource.fields.forEach((field, fieldIndex) => {
|
|
62
|
-
if (field.type === "relation" && field.resource
|
|
80
|
+
if (field.type === "relation" && typeof field.resource === "string" && !resourceIds.has(field.resource)) issues.push({
|
|
63
81
|
path: `resources.${index}.fields.${fieldIndex}.resource`,
|
|
64
82
|
message: `No resource \`${field.resource}\`.`
|
|
65
83
|
});
|
|
84
|
+
if (field.type === "media" || field.type === "image") {
|
|
85
|
+
const fieldPath = `resources.${index}.fields.${fieldIndex}.resource`;
|
|
86
|
+
if (field.resource === void 0) {
|
|
87
|
+
if (libraries.length === 0) issues.push({
|
|
88
|
+
path: fieldPath,
|
|
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}\`.`
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
66
98
|
});
|
|
67
99
|
});
|
|
68
100
|
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
|
|
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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"}
|