@panneau/protocol 0.0.0-stage → 5.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -2
- package/dist/conditions.d.ts +7 -0
- package/dist/conditions.d.ts.map +1 -0
- package/dist/conditions.js +21 -0
- package/dist/conditions.js.map +1 -0
- package/dist/conformance/index.d.ts +42 -0
- package/dist/conformance/index.d.ts.map +1 -0
- package/dist/conformance/index.js +243 -0
- package/dist/conformance/index.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/lint.d.ts +8 -0
- package/dist/lint.d.ts.map +1 -0
- package/dist/lint.js +73 -0
- package/dist/lint.js.map +1 -0
- package/dist/mock/example.d.ts +8 -0
- package/dist/mock/example.d.ts.map +1 -0
- package/dist/mock/example.js +186 -0
- package/dist/mock/example.js.map +1 -0
- package/dist/mock/index.d.ts +24 -0
- package/dist/mock/index.d.ts.map +1 -0
- package/dist/mock/index.js +250 -0
- package/dist/mock/index.js.map +1 -0
- package/dist/openapi.d.ts +11 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +540 -0
- package/dist/openapi.js.map +1 -0
- package/dist/query.d.ts +17 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +49 -0
- package/dist/query.js.map +1 -0
- package/dist/rules.d.ts +29 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +143 -0
- package/dist/rules.js.map +1 -0
- package/dist/schemas/action.d.ts +166 -0
- package/dist/schemas/action.d.ts.map +1 -0
- package/dist/schemas/action.js +66 -0
- package/dist/schemas/action.js.map +1 -0
- package/dist/schemas/api.d.ts +371 -0
- package/dist/schemas/api.d.ts.map +1 -0
- package/dist/schemas/api.js +140 -0
- package/dist/schemas/api.js.map +1 -0
- package/dist/schemas/boot.d.ts +19 -0
- package/dist/schemas/boot.d.ts.map +1 -0
- package/dist/schemas/boot.js +21 -0
- package/dist/schemas/boot.js.map +1 -0
- package/dist/schemas/common.d.ts +28 -0
- package/dist/schemas/common.d.ts.map +1 -0
- package/dist/schemas/common.js +35 -0
- package/dist/schemas/common.js.map +1 -0
- package/dist/schemas/definition.d.ts +462 -0
- package/dist/schemas/definition.d.ts.map +1 -0
- package/dist/schemas/definition.js +101 -0
- package/dist/schemas/definition.js.map +1 -0
- package/dist/schemas/field.d.ts +145 -0
- package/dist/schemas/field.d.ts.map +1 -0
- package/dist/schemas/field.js +127 -0
- package/dist/schemas/field.js.map +1 -0
- package/dist/schemas/index.d.ts +1536 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +68 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/layout.d.ts +29 -0
- package/dist/schemas/layout.d.ts.map +1 -0
- package/dist/schemas/layout.js +35 -0
- package/dist/schemas/layout.js.map +1 -0
- package/dist/schemas/list.d.ts +72 -0
- package/dist/schemas/list.d.ts.map +1 -0
- package/dist/schemas/list.js +48 -0
- package/dist/schemas/list.js.map +1 -0
- package/dist/schemas/resource.d.ts +328 -0
- package/dist/schemas/resource.d.ts.map +1 -0
- package/dist/schemas/resource.js +38 -0
- package/dist/schemas/resource.js.map +1 -0
- package/dist/validate.d.ts +31 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +57 -0
- package/dist/validate.js.map +1 -0
- package/dist/values.d.ts +7 -0
- package/dist/values.d.ts.map +1 -0
- package/dist/values.js +29 -0
- package/dist/values.js.map +1 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +9 -0
- package/dist/version.js.map +1 -0
- package/package.json +48 -5
- package/schema/boot.schema.json +1229 -0
- package/schema/definition.schema.json +1195 -0
- package/schema/openapi.json +3838 -0
package/README.md
CHANGED
|
@@ -1,3 +1,32 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @panneau/protocol
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The contract between Panneau frontends and backends: the definition format and the HTTP API, written as schemas, with the tools to check an implementation.
|
|
4
|
+
|
|
5
|
+
The protocol is explained in [`docs/protocol.md`](../../docs/protocol.md).
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import {
|
|
9
|
+
type Definition,
|
|
10
|
+
checkValues,
|
|
11
|
+
toSearchParams,
|
|
12
|
+
validateDefinition,
|
|
13
|
+
} from '@panneau/protocol';
|
|
14
|
+
import { runConformance } from '@panneau/protocol/conformance';
|
|
15
|
+
import { createMockBackend } from '@panneau/protocol/mock';
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
| Export | Content |
|
|
19
|
+
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
20
|
+
| `@panneau/protocol` | Schemas (Zod) and types, `validateDefinition()`, `validateBootConfig()`, `lintDefinition()`, `checkValues()` (portable rules), `evaluateCondition()`, list query helpers, OpenAPI and JSON Schema generators |
|
|
21
|
+
| `@panneau/protocol/mock` | `createMockBackend()`: an in-memory backend for tests and development |
|
|
22
|
+
| `@panneau/protocol/conformance` | `runConformance()`: checks a live backend against the protocol |
|
|
23
|
+
| `@panneau/protocol/definition.schema.json` | JSON Schema of the definition |
|
|
24
|
+
| `@panneau/protocol/openapi.json` | OpenAPI 3.1 document of the API |
|
|
25
|
+
| `@panneau/protocol/boot.schema.json` | JSON Schema of the boot config, which a server writes in the page of the app |
|
|
26
|
+
|
|
27
|
+
## Changing the protocol
|
|
28
|
+
|
|
29
|
+
1. Change the schemas in `src/schemas` (and the rules, mock or conformance checks they affect), with tests.
|
|
30
|
+
2. Run `npm run build` in this package. It regenerates `schema/`, and a test fails if the files are out of date.
|
|
31
|
+
3. Update `docs/protocol.md`.
|
|
32
|
+
4. Within protocol version 1, changes are additive only: new optional properties, new endpoints, new rules (which older clients ignore).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Condition } from './schemas/common';
|
|
2
|
+
/**
|
|
3
|
+
* Evaluates a `visibleWhen` condition against form values. An empty condition is true. Paths are
|
|
4
|
+
* relative to `values`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function evaluateCondition(condition: Condition, values: unknown): boolean;
|
|
7
|
+
//# sourceMappingURL=conditions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conditions.d.ts","sourceRoot":"","sources":["../src/conditions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGlD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAoChF"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { getValue, isEqual, isFilled } from "./values.js";
|
|
2
|
+
//#region src/conditions.ts
|
|
3
|
+
/**
|
|
4
|
+
* Evaluates a `visibleWhen` condition against form values. An empty condition is true. Paths are
|
|
5
|
+
* relative to `values`.
|
|
6
|
+
*/
|
|
7
|
+
function evaluateCondition(condition, values) {
|
|
8
|
+
if (condition.all !== void 0 && !condition.all.every((child) => evaluateCondition(child, values))) return false;
|
|
9
|
+
if (condition.any !== void 0 && !condition.any.some((child) => evaluateCondition(child, values))) return false;
|
|
10
|
+
if (condition.field === void 0) return true;
|
|
11
|
+
const value = getValue(values, condition.field);
|
|
12
|
+
if ("equals" in condition && !isEqual(value, condition.equals)) return false;
|
|
13
|
+
if (condition.in !== void 0 && !condition.in.some((candidate) => isEqual(value, candidate))) return false;
|
|
14
|
+
if (condition.notIn !== void 0 && condition.notIn.some((candidate) => isEqual(value, candidate))) return false;
|
|
15
|
+
if (condition.filled !== void 0 && isFilled(value) !== condition.filled) return false;
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
export { evaluateCondition };
|
|
20
|
+
|
|
21
|
+
//# sourceMappingURL=conditions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conditions.js","names":[],"sources":["../src/conditions.ts"],"sourcesContent":["import type { Condition } from './schemas/common';\nimport { getValue, isEqual, isFilled } from './values';\n\n/**\n * Evaluates a `visibleWhen` condition against form values. An empty condition is true. Paths are\n * relative to `values`.\n */\nexport function evaluateCondition(condition: Condition, values: unknown): boolean {\n if (\n condition.all !== undefined &&\n !condition.all.every((child) => evaluateCondition(child, values))\n ) {\n return false;\n }\n if (\n condition.any !== undefined &&\n !condition.any.some((child) => evaluateCondition(child, values))\n ) {\n return false;\n }\n if (condition.field === undefined) {\n return true;\n }\n const value = getValue(values, condition.field);\n if ('equals' in condition && !isEqual(value, condition.equals)) {\n return false;\n }\n if (\n condition.in !== undefined &&\n !condition.in.some((candidate) => isEqual(value, candidate))\n ) {\n return false;\n }\n if (\n condition.notIn !== undefined &&\n condition.notIn.some((candidate) => isEqual(value, candidate))\n ) {\n return false;\n }\n if (condition.filled !== undefined && isFilled(value) !== condition.filled) {\n return false;\n }\n return true;\n}\n"],"mappings":";;;;;;AAOA,SAAgB,kBAAkB,WAAsB,QAA0B;CAC9E,IACI,UAAU,QAAQ,KAAA,KAClB,CAAC,UAAU,IAAI,OAAO,UAAU,kBAAkB,OAAO,MAAM,CAAC,GAEhE,OAAO;CAEX,IACI,UAAU,QAAQ,KAAA,KAClB,CAAC,UAAU,IAAI,MAAM,UAAU,kBAAkB,OAAO,MAAM,CAAC,GAE/D,OAAO;CAEX,IAAI,UAAU,UAAU,KAAA,GACpB,OAAO;CAEX,MAAM,QAAQ,SAAS,QAAQ,UAAU,KAAK;CAC9C,IAAI,YAAY,aAAa,CAAC,QAAQ,OAAO,UAAU,MAAM,GACzD,OAAO;CAEX,IACI,UAAU,OAAO,KAAA,KACjB,CAAC,UAAU,GAAG,MAAM,cAAc,QAAQ,OAAO,SAAS,CAAC,GAE3D,OAAO;CAEX,IACI,UAAU,UAAU,KAAA,KACpB,UAAU,MAAM,MAAM,cAAc,QAAQ,OAAO,SAAS,CAAC,GAE7D,OAAO;CAEX,IAAI,UAAU,WAAW,KAAA,KAAa,SAAS,KAAK,MAAM,UAAU,QAChE,OAAO;CAEX,OAAO;AACX"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface ConformanceOptions {
|
|
2
|
+
/** Base URL of the API, the same as `api.baseUrl` of the definition, but absolute. */
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
/** The `fetch` to use. Defaults to the global one. */
|
|
5
|
+
fetch?: (input: string, init?: RequestInit) => Promise<Response>;
|
|
6
|
+
/** Merged into every request, for authentication headers or cookies. */
|
|
7
|
+
init?: RequestInit;
|
|
8
|
+
/** Resource to test. Defaults to the first resource of the definition. */
|
|
9
|
+
resource?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Enables the checks that write data, on the tested resource. Use a test database: these
|
|
12
|
+
* checks create, update and delete an item.
|
|
13
|
+
*/
|
|
14
|
+
writes?: {
|
|
15
|
+
/** Valid values for a new item. */
|
|
16
|
+
create: Record<string, unknown>;
|
|
17
|
+
/** Valid values for an update. */
|
|
18
|
+
update: Record<string, unknown>;
|
|
19
|
+
/** Values that must be refused with a `422`. Defaults to `{}` when a field is required. */
|
|
20
|
+
invalid?: Record<string, unknown>;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export type ConformanceStatus = 'passed' | 'failed' | 'skipped';
|
|
24
|
+
export interface ConformanceCheck {
|
|
25
|
+
id: string;
|
|
26
|
+
title: string;
|
|
27
|
+
status: ConformanceStatus;
|
|
28
|
+
/** What went wrong, or why the check was skipped. */
|
|
29
|
+
issues: string[];
|
|
30
|
+
}
|
|
31
|
+
export interface ConformanceReport {
|
|
32
|
+
passed: boolean;
|
|
33
|
+
checks: ConformanceCheck[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Runs the protocol's conformance checks against a live backend. Read-only checks always run;
|
|
37
|
+
* checks that write data run only with `writes`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function runConformance(options: ConformanceOptions): Promise<ConformanceReport>;
|
|
40
|
+
/** Formats a report as text, one line per check. */
|
|
41
|
+
export declare function formatReport(report: ConformanceReport): string;
|
|
42
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/conformance/index.ts"],"names":[],"mappings":"AAMA,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,CAgR5F;AAED,oDAAoD;AACpD,wBAAgB,YAAY,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAU9D"}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { PROBLEM_CONTENT_TYPE } from "../version.js";
|
|
2
|
+
import { Definition } from "../schemas/definition.js";
|
|
3
|
+
import { ItemResponse, ListResponse, Problem } from "../schemas/api.js";
|
|
4
|
+
//#region src/conformance/index.ts
|
|
5
|
+
var UNKNOWN = "__panneau_conformance_unknown__";
|
|
6
|
+
function describeIssues(error) {
|
|
7
|
+
return error.issues.slice(0, 10).map((issue) => {
|
|
8
|
+
const path = issue.path.map(String).join(".");
|
|
9
|
+
return path === "" ? issue.message : `${path}: ${issue.message}`;
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
function normalizeEtag(value) {
|
|
13
|
+
return value.replace(/^W\//, "");
|
|
14
|
+
}
|
|
15
|
+
var Check = class {
|
|
16
|
+
issues = [];
|
|
17
|
+
skippedBecause = null;
|
|
18
|
+
expect(condition, issue) {
|
|
19
|
+
if (!condition) this.issues.push(issue);
|
|
20
|
+
return condition;
|
|
21
|
+
}
|
|
22
|
+
expectStatus(reply, status) {
|
|
23
|
+
return this.expect(reply.status === status, `Expected status ${status}, got ${reply.status}.`);
|
|
24
|
+
}
|
|
25
|
+
expectSchema(schema, value, label) {
|
|
26
|
+
const result = schema.safeParse(value);
|
|
27
|
+
if (result.success) return result.data;
|
|
28
|
+
this.issues.push(`${label} does not match the schema:`, ...describeIssues(result.error).map((issue) => ` ${issue}`));
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
expectProblem(reply, status) {
|
|
32
|
+
if (!this.expectStatus(reply, status)) return null;
|
|
33
|
+
this.expect(reply.contentType.startsWith(PROBLEM_CONTENT_TYPE), `Expected content type ${PROBLEM_CONTENT_TYPE}, got "${reply.contentType}".`);
|
|
34
|
+
return this.expectSchema(Problem, reply.body, "The error");
|
|
35
|
+
}
|
|
36
|
+
skip(reason) {
|
|
37
|
+
this.skippedBecause = reason;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Runs the protocol's conformance checks against a live backend. Read-only checks always run;
|
|
42
|
+
* checks that write data run only with `writes`.
|
|
43
|
+
*/
|
|
44
|
+
async function runConformance(options) {
|
|
45
|
+
const fetcher = options.fetch ?? ((input, init) => fetch(input, init));
|
|
46
|
+
const baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
47
|
+
const checks = [];
|
|
48
|
+
const request = async (path, init = {}) => {
|
|
49
|
+
const headers = new Headers(options.init?.headers);
|
|
50
|
+
headers.set("Accept", "application/json");
|
|
51
|
+
new Headers(init.headers).forEach((value, key) => headers.set(key, value));
|
|
52
|
+
if (init.body !== void 0) headers.set("Content-Type", "application/json");
|
|
53
|
+
const response = await fetcher(`${baseUrl}${path}`, {
|
|
54
|
+
...options.init,
|
|
55
|
+
...init,
|
|
56
|
+
headers
|
|
57
|
+
});
|
|
58
|
+
const text = await response.text();
|
|
59
|
+
const parse = () => {
|
|
60
|
+
if (text === "") return null;
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(text);
|
|
63
|
+
} catch {
|
|
64
|
+
return text;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const body = parse();
|
|
68
|
+
return {
|
|
69
|
+
status: response.status,
|
|
70
|
+
contentType: response.headers.get("Content-Type") ?? "",
|
|
71
|
+
etag: response.headers.get("ETag"),
|
|
72
|
+
body
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
const run = async (id, title, body) => {
|
|
76
|
+
const check = new Check();
|
|
77
|
+
try {
|
|
78
|
+
await body(check);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
check.issues.push(`Request failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
81
|
+
}
|
|
82
|
+
const status = check.issues.length > 0 ? "failed" : check.skippedBecause !== null ? "skipped" : "passed";
|
|
83
|
+
checks.push({
|
|
84
|
+
id,
|
|
85
|
+
title,
|
|
86
|
+
status,
|
|
87
|
+
issues: status === "skipped" && check.skippedBecause !== null ? [check.skippedBecause] : check.issues
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
let definition = null;
|
|
91
|
+
let resource = null;
|
|
92
|
+
let firstItemId = null;
|
|
93
|
+
await run("definition", "GET /definition returns a valid definition", async (check) => {
|
|
94
|
+
const reply = await request("/definition");
|
|
95
|
+
if (check.expectStatus(reply, 200)) definition = check.expectSchema(Definition, reply.body, "The definition");
|
|
96
|
+
});
|
|
97
|
+
await run("definition.resource", "The definition has the resource to test", async (check) => {
|
|
98
|
+
const current = definition;
|
|
99
|
+
if (current === null) {
|
|
100
|
+
check.skip("No valid definition.");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
resource = (options.resource !== void 0 ? current.resources.find((candidate) => candidate.id === options.resource) : current.resources[0]) ?? null;
|
|
104
|
+
check.expect(resource !== null, options.resource !== void 0 ? `The definition has no resource "${options.resource}".` : "The definition has no resources.");
|
|
105
|
+
});
|
|
106
|
+
const resourcePath = () => `/resources/${encodeURIComponent(resource?.id ?? "")}`;
|
|
107
|
+
await run("list", "GET /resources/{resource} returns a page of items", async (check) => {
|
|
108
|
+
if (resource === null) {
|
|
109
|
+
check.skip("No resource to test.");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const reply = await request(`${resourcePath()}?page=1&perPage=2`);
|
|
113
|
+
if (!check.expectStatus(reply, 200)) return;
|
|
114
|
+
const list = check.expectSchema(ListResponse, reply.body, "The list");
|
|
115
|
+
if (list === null) return;
|
|
116
|
+
check.expect(list.data.length <= 2, `perPage=2 returned ${list.data.length} items.`);
|
|
117
|
+
check.expect(list.meta.page === 1, `meta.page is ${list.meta.page}, expected 1.`);
|
|
118
|
+
check.expect(list.meta.perPage === 2, `meta.perPage is ${list.meta.perPage}, expected 2.`);
|
|
119
|
+
firstItemId = list.data[0]?.id ?? null;
|
|
120
|
+
});
|
|
121
|
+
await run("list.sort", "GET /resources/{resource} accepts a sort", async (check) => {
|
|
122
|
+
const sort = resource?.list?.sorts?.[0];
|
|
123
|
+
if (sort === void 0) {
|
|
124
|
+
check.skip("The resource declares no sorts.");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const reply = await request(`${resourcePath()}?sort=${encodeURIComponent(`-${sort}`)}`);
|
|
128
|
+
if (check.expectStatus(reply, 200)) check.expectSchema(ListResponse, reply.body, "The sorted list");
|
|
129
|
+
});
|
|
130
|
+
await run("list.unknown-resource", "An unknown resource is a 404 problem", async (check) => {
|
|
131
|
+
check.expectProblem(await request(`/resources/${UNKNOWN}`), 404);
|
|
132
|
+
});
|
|
133
|
+
await run("item", "GET /resources/{resource}/{id} returns the item with its ETag", async (check) => {
|
|
134
|
+
if (firstItemId === null) {
|
|
135
|
+
check.skip("The list has no items.");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const reply = await request(`${resourcePath()}/${encodeURIComponent(firstItemId)}`);
|
|
139
|
+
if (!check.expectStatus(reply, 200)) return;
|
|
140
|
+
const item = check.expectSchema(ItemResponse, reply.body, "The item");
|
|
141
|
+
if (item === null) return;
|
|
142
|
+
check.expect(item.data.id === firstItemId, `Asked for item "${firstItemId}", got "${item.data.id}".`);
|
|
143
|
+
if (item.data.version !== null) check.expect(reply.etag !== null && normalizeEtag(reply.etag) === `"${item.data.version}"`, `Expected ETag "${item.data.version}" (quoted), got ${reply.etag ?? "none"}.`);
|
|
144
|
+
});
|
|
145
|
+
await run("item.missing", "A missing item is a 404 problem", async (check) => {
|
|
146
|
+
if (resource === null) {
|
|
147
|
+
check.skip("No resource to test.");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
check.expectProblem(await request(`${resourcePath()}/${UNKNOWN}`), 404);
|
|
151
|
+
});
|
|
152
|
+
const writes = options.writes;
|
|
153
|
+
let createdId = null;
|
|
154
|
+
let createdVersion = null;
|
|
155
|
+
await run("create.invalid", "Invalid values are refused with a 422 problem", async (check) => {
|
|
156
|
+
if (writes === void 0 || resource === null) {
|
|
157
|
+
check.skip("Write checks are disabled.");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const hasRequired = resource.fields.some((field) => (field.rules ?? []).some((rule) => rule.rule === "required"));
|
|
161
|
+
if (writes.invalid === void 0 && !hasRequired) {
|
|
162
|
+
check.skip("No invalid values given, and the resource has no required field.");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const reply = await request(resourcePath(), {
|
|
166
|
+
method: "POST",
|
|
167
|
+
body: JSON.stringify(writes.invalid ?? {})
|
|
168
|
+
});
|
|
169
|
+
const failure = check.expectProblem(reply, 422);
|
|
170
|
+
if (failure !== null) check.expect(failure.errors !== void 0 && Object.keys(failure.errors).length > 0, "A 422 problem must list the invalid values in `errors`.");
|
|
171
|
+
});
|
|
172
|
+
await run("create", "POST /resources/{resource} creates an item", async (check) => {
|
|
173
|
+
if (writes === void 0 || resource === null) {
|
|
174
|
+
check.skip("Write checks are disabled.");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const reply = await request(resourcePath(), {
|
|
178
|
+
method: "POST",
|
|
179
|
+
body: JSON.stringify(writes.create)
|
|
180
|
+
});
|
|
181
|
+
if (!check.expectStatus(reply, 201)) return;
|
|
182
|
+
const item = check.expectSchema(ItemResponse, reply.body, "The created item");
|
|
183
|
+
createdId = item?.data.id ?? null;
|
|
184
|
+
createdVersion = item?.data.version ?? null;
|
|
185
|
+
});
|
|
186
|
+
const itemPath = () => `${resourcePath()}/${encodeURIComponent(createdId ?? "")}`;
|
|
187
|
+
await run("update", "PATCH /resources/{resource}/{id} updates the item", async (check) => {
|
|
188
|
+
if (createdId === null || writes === void 0) {
|
|
189
|
+
check.skip("No item was created.");
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const reply = await request(itemPath(), {
|
|
193
|
+
method: "PATCH",
|
|
194
|
+
body: JSON.stringify(writes.update),
|
|
195
|
+
headers: createdVersion !== null ? { "If-Match": `"${createdVersion}"` } : {}
|
|
196
|
+
});
|
|
197
|
+
if (!check.expectStatus(reply, 200)) return;
|
|
198
|
+
const item = check.expectSchema(ItemResponse, reply.body, "The updated item");
|
|
199
|
+
if (item !== null && createdVersion !== null) check.expect(item.data.version !== createdVersion, "The version did not change after an update.");
|
|
200
|
+
});
|
|
201
|
+
await run("update.conflict", "A stale If-Match is a 412 problem with the current item", async (check) => {
|
|
202
|
+
if (createdId === null || writes === void 0) {
|
|
203
|
+
check.skip("No item was created.");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (createdVersion === null) {
|
|
207
|
+
check.skip("The backend has no versions.");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const reply = await request(itemPath(), {
|
|
211
|
+
method: "PATCH",
|
|
212
|
+
body: JSON.stringify(writes.update),
|
|
213
|
+
headers: { "If-Match": "\"__panneau_conformance_stale__\"" }
|
|
214
|
+
});
|
|
215
|
+
const failure = check.expectProblem(reply, 412);
|
|
216
|
+
if (failure !== null) check.expect(failure.current !== void 0, "A 412 problem must include the current item.");
|
|
217
|
+
});
|
|
218
|
+
await run("delete", "DELETE /resources/{resource}/{id} deletes the item", async (check) => {
|
|
219
|
+
if (createdId === null) {
|
|
220
|
+
check.skip("No item was created.");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (!check.expectStatus(await request(itemPath(), { method: "DELETE" }), 204)) return;
|
|
224
|
+
check.expectProblem(await request(itemPath()), 404);
|
|
225
|
+
});
|
|
226
|
+
return {
|
|
227
|
+
passed: checks.every((check) => check.status !== "failed"),
|
|
228
|
+
checks
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/** Formats a report as text, one line per check. */
|
|
232
|
+
function formatReport(report) {
|
|
233
|
+
const symbols = {
|
|
234
|
+
passed: "✓",
|
|
235
|
+
failed: "✗",
|
|
236
|
+
skipped: "-"
|
|
237
|
+
};
|
|
238
|
+
return report.checks.map((check) => [`${symbols[check.status]} ${check.title}`, ...check.issues.map((issue) => ` ${issue}`)].join("\n")).join("\n");
|
|
239
|
+
}
|
|
240
|
+
//#endregion
|
|
241
|
+
export { formatReport, runConformance };
|
|
242
|
+
|
|
243
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/conformance/index.ts"],"sourcesContent":["import type { z } from 'zod';\n\nimport { Definition, type Resource } from '../schemas';\nimport { ItemResponse, ListResponse, Problem } from '../schemas/api';\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('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.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { PROBLEM_CONTENT_TYPE, PROTOCOL_VERSION } from './version';
|
|
2
|
+
export * from './schemas';
|
|
3
|
+
export { type ValidationIssue, type ValidationResult, isProblem, validate, validateBootConfig, validateDefinition, } from './validate';
|
|
4
|
+
export { type CheckOptions, type RuleFailure, checkRules, checkValues } from './rules';
|
|
5
|
+
export { evaluateCondition } from './conditions';
|
|
6
|
+
export { lintDefinition } from './lint';
|
|
7
|
+
export { type FilterValue, type ListQuery, fromSearchParams, toSearchParams } from './query';
|
|
8
|
+
export { getValue, isFilled } from './values';
|
|
9
|
+
export { createBootConfigJsonSchema, createComponentSchemas, createDefinitionJsonSchema, createOpenApiDocument, } from './openapi';
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AACnE,cAAc,WAAW,CAAC;AAC1B,OAAO,EACH,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,SAAS,EACT,QAAQ,EACR,kBAAkB,EAClB,kBAAkB,GACrB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACvF,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC7F,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,EACH,0BAA0B,EAC1B,sBAAsB,EACtB,0BAA0B,EAC1B,qBAAqB,GACxB,MAAM,WAAW,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { PROBLEM_CONTENT_TYPE, PROTOCOL_VERSION } from "./version.js";
|
|
2
|
+
import { Abilities, Condition, Id, Option, RouteReference } from "./schemas/common.js";
|
|
3
|
+
import { BUILT_IN_FIELD_TYPES, BlockType, Field, PORTABLE_RULES, Rule } from "./schemas/field.js";
|
|
4
|
+
import { BUILT_IN_LAYOUT_NODE_TYPES, LayoutNode } from "./schemas/layout.js";
|
|
5
|
+
import { Action, ActionConfirm, ActionPlacement, ActionScope, BUILT_IN_ACTIONS } from "./schemas/action.js";
|
|
6
|
+
import { BUILT_IN_LIST_LAYOUTS, Column, Filter, ListDefinition } from "./schemas/list.js";
|
|
7
|
+
import { Resource, ResourceFeatures } from "./schemas/resource.js";
|
|
8
|
+
import { Auth, AuthProvider, Definition, MenuItem, Page, Uploads, User, Widget } from "./schemas/definition.js";
|
|
9
|
+
import { ActionItemResult, ActionRequest, ActionResult, ActionRun, ActionRunResponse, Item, ItemResponse, ListResponse, Media, MediaListResponse, MediaResponse, MediaTransform, Pagination, PreviewResponse, Problem, SearchResponse, Selection, Session } from "./schemas/api.js";
|
|
10
|
+
import { BOOT_CONFIG_ELEMENT_ID, BootConfig } from "./schemas/boot.js";
|
|
11
|
+
import { createSchemaRegistry, namedSchemas } from "./schemas/index.js";
|
|
12
|
+
import { lintDefinition } from "./lint.js";
|
|
13
|
+
import { isProblem, validate, validateBootConfig, validateDefinition } from "./validate.js";
|
|
14
|
+
import { getValue, isFilled } from "./values.js";
|
|
15
|
+
import { evaluateCondition } from "./conditions.js";
|
|
16
|
+
import { checkRules, checkValues } from "./rules.js";
|
|
17
|
+
import { fromSearchParams, toSearchParams } from "./query.js";
|
|
18
|
+
import { createBootConfigJsonSchema, createComponentSchemas, createDefinitionJsonSchema, createOpenApiDocument } from "./openapi.js";
|
|
19
|
+
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, 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, Uploads, User, Widget, checkRules, checkValues, createBootConfigJsonSchema, createComponentSchemas, createDefinitionJsonSchema, createOpenApiDocument, createSchemaRegistry, evaluateCondition, fromSearchParams, getValue, isFilled, isProblem, lintDefinition, namedSchemas, toSearchParams, validate, validateBootConfig, validateDefinition };
|
package/dist/lint.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DefinitionInput } from './schemas';
|
|
2
|
+
import type { ValidationIssue } from './validate';
|
|
3
|
+
/**
|
|
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).
|
|
6
|
+
*/
|
|
7
|
+
export declare function lintDefinition(definition: DefinitionInput): ValidationIssue[];
|
|
8
|
+
//# sourceMappingURL=lint.d.ts.map
|
|
@@ -0,0 +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"}
|
package/dist/lint.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
//#region src/lint.ts
|
|
2
|
+
function fieldNames(fields) {
|
|
3
|
+
return new Set(fields.map((field) => field.name));
|
|
4
|
+
}
|
|
5
|
+
function checkLayout(nodes, names, path, issues) {
|
|
6
|
+
nodes.forEach((node, index) => {
|
|
7
|
+
const nodePath = `${path}.${index}`;
|
|
8
|
+
if (node.type === "field") {
|
|
9
|
+
if (node.name === void 0) issues.push({
|
|
10
|
+
path: nodePath,
|
|
11
|
+
message: "A `field` node needs a `name`."
|
|
12
|
+
});
|
|
13
|
+
else if (!names.has(node.name)) issues.push({
|
|
14
|
+
path: `${nodePath}.name`,
|
|
15
|
+
message: `No field named \`${node.name}\`.`
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
if (node.type === "component" && node.component === void 0) issues.push({
|
|
19
|
+
path: nodePath,
|
|
20
|
+
message: "A `component` node needs a `component`."
|
|
21
|
+
});
|
|
22
|
+
if (node.type === "tabs" && node.children?.some((child) => child.type !== "tab")) issues.push({
|
|
23
|
+
path: `${nodePath}.children`,
|
|
24
|
+
message: "The children of `tabs` must be `tab` nodes."
|
|
25
|
+
});
|
|
26
|
+
if (node.children !== void 0) checkLayout(node.children, names, `${nodePath}.children`, issues);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function checkResource(resource, path, issues) {
|
|
30
|
+
const names = fieldNames(resource.fields);
|
|
31
|
+
if (names.size !== resource.fields.length) issues.push({
|
|
32
|
+
path: `${path}.fields`,
|
|
33
|
+
message: "Field names must be unique."
|
|
34
|
+
});
|
|
35
|
+
if (resource.form?.layout !== void 0) checkLayout(resource.form.layout, names, `${path}.form.layout`, issues);
|
|
36
|
+
(resource.list?.columns ?? []).forEach((column, index) => {
|
|
37
|
+
if (column.field !== void 0 && !names.has(column.field)) issues.push({
|
|
38
|
+
path: `${path}.list.columns.${index}.field`,
|
|
39
|
+
message: `No field named \`${column.field}\`.`
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
(resource.actions ?? []).forEach((action, index) => {
|
|
43
|
+
if (action.layout !== void 0) checkLayout(action.layout, fieldNames(action.fields ?? []), `${path}.actions.${index}.layout`, issues);
|
|
44
|
+
});
|
|
45
|
+
resource.fields.forEach((field, index) => {
|
|
46
|
+
if (field.type === "relation" && field.resource === void 0) issues.push({
|
|
47
|
+
path: `${path}.fields.${index}.resource`,
|
|
48
|
+
message: "A `relation` field needs a `resource`."
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 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).
|
|
55
|
+
*/
|
|
56
|
+
function lintDefinition(definition) {
|
|
57
|
+
const issues = [];
|
|
58
|
+
const resourceIds = new Set(definition.resources.map((resource) => resource.id));
|
|
59
|
+
definition.resources.forEach((resource, index) => {
|
|
60
|
+
checkResource(resource, `resources.${index}`, issues);
|
|
61
|
+
resource.fields.forEach((field, fieldIndex) => {
|
|
62
|
+
if (field.type === "relation" && field.resource !== void 0 && !resourceIds.has(field.resource)) issues.push({
|
|
63
|
+
path: `resources.${index}.fields.${fieldIndex}.resource`,
|
|
64
|
+
message: `No resource \`${field.resource}\`.`
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
return issues;
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
export { lintDefinition };
|
|
72
|
+
|
|
73
|
+
//# sourceMappingURL=lint.js.map
|
package/dist/lint.js.map
ADDED
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DefinitionInput } from '../schemas';
|
|
2
|
+
/**
|
|
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
|
|
5
|
+
* and stories, with `createMockBackend()`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createExampleDefinition(): DefinitionInput;
|
|
8
|
+
//# sourceMappingURL=example.d.ts.map
|
|
@@ -0,0 +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,CAuIzD"}
|