@panneau/protocol 0.0.0-stage → 5.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +35 -2
  2. package/dist/conditions.d.ts +7 -0
  3. package/dist/conditions.d.ts.map +1 -0
  4. package/dist/conditions.js +21 -0
  5. package/dist/conditions.js.map +1 -0
  6. package/dist/conformance/index.d.ts +42 -0
  7. package/dist/conformance/index.d.ts.map +1 -0
  8. package/dist/conformance/index.js +270 -0
  9. package/dist/conformance/index.js.map +1 -0
  10. package/dist/constants.d.ts +21 -0
  11. package/dist/constants.d.ts.map +1 -0
  12. package/dist/constants.js +73 -0
  13. package/dist/constants.js.map +1 -0
  14. package/dist/index.d.ts +5 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +21 -0
  17. package/dist/lint.d.ts +8 -0
  18. package/dist/lint.d.ts.map +1 -0
  19. package/dist/lint.js +73 -0
  20. package/dist/lint.js.map +1 -0
  21. package/dist/mock/example.d.ts +8 -0
  22. package/dist/mock/example.d.ts.map +1 -0
  23. package/dist/mock/example.js +190 -0
  24. package/dist/mock/example.js.map +1 -0
  25. package/dist/mock/index.d.ts +24 -0
  26. package/dist/mock/index.d.ts.map +1 -0
  27. package/dist/mock/index.js +254 -0
  28. package/dist/mock/index.js.map +1 -0
  29. package/dist/openapi.d.ts +11 -0
  30. package/dist/openapi.d.ts.map +1 -0
  31. package/dist/openapi.js +540 -0
  32. package/dist/openapi.js.map +1 -0
  33. package/dist/problem.d.ts +7 -0
  34. package/dist/problem.d.ts.map +1 -0
  35. package/dist/problem.js +22 -0
  36. package/dist/problem.js.map +1 -0
  37. package/dist/query.d.ts +41 -0
  38. package/dist/query.d.ts.map +1 -0
  39. package/dist/query.js +80 -0
  40. package/dist/query.js.map +1 -0
  41. package/dist/rules.d.ts +29 -0
  42. package/dist/rules.d.ts.map +1 -0
  43. package/dist/rules.js +143 -0
  44. package/dist/rules.js.map +1 -0
  45. package/dist/runtime/index.d.ts +10 -0
  46. package/dist/runtime/index.d.ts.map +1 -0
  47. package/dist/runtime/index.js +9 -0
  48. package/dist/schemas/action.d.ts +163 -0
  49. package/dist/schemas/action.d.ts.map +1 -0
  50. package/dist/schemas/action.js +56 -0
  51. package/dist/schemas/action.js.map +1 -0
  52. package/dist/schemas/api.d.ts +371 -0
  53. package/dist/schemas/api.d.ts.map +1 -0
  54. package/dist/schemas/api.js +140 -0
  55. package/dist/schemas/api.js.map +1 -0
  56. package/dist/schemas/boot.d.ts +14 -0
  57. package/dist/schemas/boot.d.ts.map +1 -0
  58. package/dist/schemas/boot.js +16 -0
  59. package/dist/schemas/boot.js.map +1 -0
  60. package/dist/schemas/common.d.ts +28 -0
  61. package/dist/schemas/common.d.ts.map +1 -0
  62. package/dist/schemas/common.js +35 -0
  63. package/dist/schemas/common.js.map +1 -0
  64. package/dist/schemas/definition.d.ts +462 -0
  65. package/dist/schemas/definition.d.ts.map +1 -0
  66. package/dist/schemas/definition.js +101 -0
  67. package/dist/schemas/definition.js.map +1 -0
  68. package/dist/schemas/field.d.ts +139 -0
  69. package/dist/schemas/field.d.ts.map +1 -0
  70. package/dist/schemas/field.js +94 -0
  71. package/dist/schemas/field.js.map +1 -0
  72. package/dist/schemas/index.d.ts +1536 -0
  73. package/dist/schemas/index.d.ts.map +1 -0
  74. package/dist/schemas/index.js +68 -0
  75. package/dist/schemas/index.js.map +1 -0
  76. package/dist/schemas/layout.d.ts +26 -0
  77. package/dist/schemas/layout.d.ts.map +1 -0
  78. package/dist/schemas/layout.js +23 -0
  79. package/dist/schemas/layout.js.map +1 -0
  80. package/dist/schemas/list.d.ts +69 -0
  81. package/dist/schemas/list.d.ts.map +1 -0
  82. package/dist/schemas/list.js +41 -0
  83. package/dist/schemas/list.js.map +1 -0
  84. package/dist/schemas/resource.d.ts +328 -0
  85. package/dist/schemas/resource.d.ts.map +1 -0
  86. package/dist/schemas/resource.js +38 -0
  87. package/dist/schemas/resource.js.map +1 -0
  88. package/dist/validate.d.ts +28 -0
  89. package/dist/validate.d.ts.map +1 -0
  90. package/dist/validate.js +52 -0
  91. package/dist/validate.js.map +1 -0
  92. package/dist/values.d.ts +7 -0
  93. package/dist/values.d.ts.map +1 -0
  94. package/dist/values.js +29 -0
  95. package/dist/values.js.map +1 -0
  96. package/dist/version.d.ts +5 -0
  97. package/dist/version.d.ts.map +1 -0
  98. package/dist/version.js +9 -0
  99. package/dist/version.js.map +1 -0
  100. package/package.json +53 -5
  101. package/schema/boot.schema.json +1229 -0
  102. package/schema/definition.schema.json +1195 -0
  103. package/schema/openapi.json +3839 -0
package/README.md CHANGED
@@ -1,3 +1,36 @@
1
- # Temporary Holding Version
1
+ # @panneau/protocol
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+ import { checkValues, evaluateCondition } from '@panneau/protocol/runtime';
17
+ ```
18
+
19
+ **In the browser,** import the helpers from `@panneau/protocol/runtime`, and only types from `@panneau/protocol`. The main entry point loads zod and builds every schema, which bundlers can't drop (about 360 KB of source). `@panneau/core`, `fields`, `forms` and `lists` follow this rule, and ESLint enforces it in them.
20
+
21
+ | Export | Content |
22
+ | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
23
+ | `@panneau/protocol` | Schemas (Zod) and types, `validateDefinition()`, `validateBootConfig()`, `lintDefinition()`, `checkValues()` (portable rules), `evaluateCondition()`, list query helpers, OpenAPI and JSON Schema generators |
24
+ | `@panneau/protocol/runtime` | The constants and the helpers that run in the browser, without zod: `checkValues()`, `checkRules()`, `evaluateCondition()`, `lintDefinition()`, `getValue()`, `isFilled()`, `isProblem()`, `toSearchParams()`, `fromSearchParams()`, `isFilterRange()`, `matchesFilterRange()`. The main entry point also exports them |
25
+ | `@panneau/protocol/mock` | `createMockBackend()`: an in-memory backend for tests and development |
26
+ | `@panneau/protocol/conformance` | `runConformance()`: checks a live backend against the protocol |
27
+ | `@panneau/protocol/definition.schema.json` | JSON Schema of the definition |
28
+ | `@panneau/protocol/openapi.json` | OpenAPI 3.1 document of the API |
29
+ | `@panneau/protocol/boot.schema.json` | JSON Schema of the boot config, which a server writes in the page of the app |
30
+
31
+ ## Changing the protocol
32
+
33
+ 1. Change the schemas in `src/schemas` (and the rules, mock or conformance checks they affect), with tests.
34
+ 2. Run `npm run build` in this package. It regenerates `schema/`, and a test fails if the files are out of date.
35
+ 3. Update `docs/protocol.md`.
36
+ 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":"AAQA,MAAM,WAAW,kBAAkB;IAC/B,sFAAsF;IACtF,OAAO,EAAE,MAAM,CAAC;IAChB,sDAAsD;IACtD,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IACjE,wEAAwE;IACxE,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,MAAM,CAAC,EAAE;QACL,mCAAmC;QACnC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAChC,kCAAkC;QAClC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAChC,2FAA2F;QAC3F,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACrC,CAAC;CACL;AAED,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEhE,MAAM,WAAW,gBAAgB;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,iBAAiB,CAAC;IAC1B,qDAAqD;IACrD,MAAM,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAC9B,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,gBAAgB,EAAE,CAAC;CAC9B;AAoED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAiU5F;AAED,oDAAoD;AACpD,wBAAgB,YAAY,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAU9D"}
@@ -0,0 +1,270 @@
1
+ import { PROBLEM_CONTENT_TYPE } from "../version.js";
2
+ import { isFilled } from "../values.js";
3
+ import { toSearchParams } from "../query.js";
4
+ import { Definition } from "../schemas/definition.js";
5
+ import { ItemResponse, ListResponse, Problem } from "../schemas/api.js";
6
+ //#region src/conformance/index.ts
7
+ var UNKNOWN = "__panneau_conformance_unknown__";
8
+ function describeIssues(error) {
9
+ return error.issues.slice(0, 10).map((issue) => {
10
+ const path = issue.path.map(String).join(".");
11
+ return path === "" ? issue.message : `${path}: ${issue.message}`;
12
+ });
13
+ }
14
+ function normalizeEtag(value) {
15
+ return value.replace(/^W\//, "");
16
+ }
17
+ var Check = class {
18
+ issues = [];
19
+ skippedBecause = null;
20
+ expect(condition, issue) {
21
+ if (!condition) this.issues.push(issue);
22
+ return condition;
23
+ }
24
+ expectStatus(reply, status) {
25
+ return this.expect(reply.status === status, `Expected status ${status}, got ${reply.status}.`);
26
+ }
27
+ expectSchema(schema, value, label) {
28
+ const result = schema.safeParse(value);
29
+ if (result.success) return result.data;
30
+ this.issues.push(`${label} does not match the schema:`, ...describeIssues(result.error).map((issue) => ` ${issue}`));
31
+ return null;
32
+ }
33
+ expectProblem(reply, status) {
34
+ if (!this.expectStatus(reply, status)) return null;
35
+ this.expect(reply.contentType.startsWith(PROBLEM_CONTENT_TYPE), `Expected content type ${PROBLEM_CONTENT_TYPE}, got "${reply.contentType}".`);
36
+ return this.expectSchema(Problem, reply.body, "The error");
37
+ }
38
+ skip(reason) {
39
+ this.skippedBecause = reason;
40
+ }
41
+ };
42
+ /**
43
+ * Runs the protocol's conformance checks against a live backend. Read-only checks always run;
44
+ * checks that write data run only with `writes`.
45
+ */
46
+ async function runConformance(options) {
47
+ const fetcher = options.fetch ?? ((input, init) => fetch(input, init));
48
+ const baseUrl = options.baseUrl.replace(/\/$/, "");
49
+ const checks = [];
50
+ const request = async (path, init = {}) => {
51
+ const headers = new Headers(options.init?.headers);
52
+ headers.set("Accept", "application/json");
53
+ new Headers(init.headers).forEach((value, key) => headers.set(key, value));
54
+ if (init.body !== void 0) headers.set("Content-Type", "application/json");
55
+ const response = await fetcher(`${baseUrl}${path}`, {
56
+ ...options.init,
57
+ ...init,
58
+ headers
59
+ });
60
+ const text = await response.text();
61
+ const parse = () => {
62
+ if (text === "") return null;
63
+ try {
64
+ return JSON.parse(text);
65
+ } catch {
66
+ return text;
67
+ }
68
+ };
69
+ const body = parse();
70
+ return {
71
+ status: response.status,
72
+ contentType: response.headers.get("Content-Type") ?? "",
73
+ etag: response.headers.get("ETag"),
74
+ body
75
+ };
76
+ };
77
+ const run = async (id, title, body) => {
78
+ const check = new Check();
79
+ try {
80
+ await body(check);
81
+ } catch (error) {
82
+ check.issues.push(`Request failed: ${error instanceof Error ? error.message : String(error)}`);
83
+ }
84
+ const status = check.issues.length > 0 ? "failed" : check.skippedBecause !== null ? "skipped" : "passed";
85
+ checks.push({
86
+ id,
87
+ title,
88
+ status,
89
+ issues: status === "skipped" && check.skippedBecause !== null ? [check.skippedBecause] : check.issues
90
+ });
91
+ };
92
+ let definition = null;
93
+ let resource = null;
94
+ let firstItemId = null;
95
+ await run("definition", "GET /definition returns a valid definition", async (check) => {
96
+ const reply = await request("/definition");
97
+ if (check.expectStatus(reply, 200)) definition = check.expectSchema(Definition, reply.body, "The definition");
98
+ });
99
+ await run("definition.resource", "The definition has the resource to test", async (check) => {
100
+ const current = definition;
101
+ if (current === null) {
102
+ check.skip("No valid definition.");
103
+ return;
104
+ }
105
+ resource = (options.resource !== void 0 ? current.resources.find((candidate) => candidate.id === options.resource) : current.resources[0]) ?? null;
106
+ check.expect(resource !== null, options.resource !== void 0 ? `The definition has no resource "${options.resource}".` : "The definition has no resources.");
107
+ });
108
+ const resourcePath = () => `/resources/${encodeURIComponent(resource?.id ?? "")}`;
109
+ await run("list", "GET /resources/{resource} returns a page of items", async (check) => {
110
+ if (resource === null) {
111
+ check.skip("No resource to test.");
112
+ return;
113
+ }
114
+ const reply = await request(`${resourcePath()}?page=1&perPage=2`);
115
+ if (!check.expectStatus(reply, 200)) return;
116
+ const list = check.expectSchema(ListResponse, reply.body, "The list");
117
+ if (list === null) return;
118
+ check.expect(list.data.length <= 2, `perPage=2 returned ${list.data.length} items.`);
119
+ check.expect(list.meta.page === 1, `meta.page is ${list.meta.page}, expected 1.`);
120
+ check.expect(list.meta.perPage === 2, `meta.perPage is ${list.meta.perPage}, expected 2.`);
121
+ firstItemId = list.data[0]?.id ?? null;
122
+ });
123
+ await run("list.sort", "GET /resources/{resource} accepts a sort", async (check) => {
124
+ const sort = resource?.list?.sorts?.[0];
125
+ if (sort === void 0) {
126
+ check.skip("The resource declares no sorts.");
127
+ return;
128
+ }
129
+ const reply = await request(`${resourcePath()}?sort=${encodeURIComponent(`-${sort}`)}`);
130
+ if (check.expectStatus(reply, 200)) check.expectSchema(ListResponse, reply.body, "The sorted list");
131
+ });
132
+ await run("list.filter-range", "GET /resources/{resource} reads the bounds of a range filter", async (check) => {
133
+ const filter = resource?.list?.filters?.find((candidate) => candidate.type === "date-range");
134
+ if (filter === void 0) {
135
+ check.skip("The resource declares no date-range filter.");
136
+ return;
137
+ }
138
+ const list = async (range, label) => {
139
+ const query = range !== null ? `?${toSearchParams({ filter: { [filter.id]: range } })}` : "";
140
+ const reply = await request(`${resourcePath()}${query}`);
141
+ if (!check.expectStatus(reply, 200)) return null;
142
+ return check.expectSchema(ListResponse, reply.body, label);
143
+ };
144
+ const all = await list(null, "The list");
145
+ if (all === null) return;
146
+ const dated = all.data.filter((item) => isFilled(item[filter.id])).length;
147
+ for (const [range, label] of [[{
148
+ from: "0001-01-01",
149
+ to: "9999-12-31"
150
+ }, "from 0001-01-01 to 9999-12-31"], [{ to: "9999-12-31" }, "up to 9999-12-31 (an open start)"]]) {
151
+ const reply = await list(range, `The list filtered ${label}`);
152
+ if (reply !== null) check.expect(reply.meta.total >= dated, `Filtered ${label}, the list has ${reply.meta.total} items; the first page alone has ${dated} with a ${filter.id}.`);
153
+ }
154
+ const future = await list({ from: "9999-12-31" }, "The list filtered from 9999-12-31");
155
+ if (future !== null) check.expect(future.meta.total === 0, `Filtered from 9999-12-31, the list has ${future.meta.total} items, expected 0.`);
156
+ });
157
+ await run("list.unknown-resource", "An unknown resource is a 404 problem", async (check) => {
158
+ check.expectProblem(await request(`/resources/${UNKNOWN}`), 404);
159
+ });
160
+ await run("item", "GET /resources/{resource}/{id} returns the item with its ETag", async (check) => {
161
+ if (firstItemId === null) {
162
+ check.skip("The list has no items.");
163
+ return;
164
+ }
165
+ const reply = await request(`${resourcePath()}/${encodeURIComponent(firstItemId)}`);
166
+ if (!check.expectStatus(reply, 200)) return;
167
+ const item = check.expectSchema(ItemResponse, reply.body, "The item");
168
+ if (item === null) return;
169
+ check.expect(item.data.id === firstItemId, `Asked for item "${firstItemId}", got "${item.data.id}".`);
170
+ 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"}.`);
171
+ });
172
+ await run("item.missing", "A missing item is a 404 problem", async (check) => {
173
+ if (resource === null) {
174
+ check.skip("No resource to test.");
175
+ return;
176
+ }
177
+ check.expectProblem(await request(`${resourcePath()}/${UNKNOWN}`), 404);
178
+ });
179
+ const writes = options.writes;
180
+ let createdId = null;
181
+ let createdVersion = null;
182
+ await run("create.invalid", "Invalid values are refused with a 422 problem", async (check) => {
183
+ if (writes === void 0 || resource === null) {
184
+ check.skip("Write checks are disabled.");
185
+ return;
186
+ }
187
+ const hasRequired = resource.fields.some((field) => (field.rules ?? []).some((rule) => rule.rule === "required"));
188
+ if (writes.invalid === void 0 && !hasRequired) {
189
+ check.skip("No invalid values given, and the resource has no required field.");
190
+ return;
191
+ }
192
+ const reply = await request(resourcePath(), {
193
+ method: "POST",
194
+ body: JSON.stringify(writes.invalid ?? {})
195
+ });
196
+ const failure = check.expectProblem(reply, 422);
197
+ 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`.");
198
+ });
199
+ await run("create", "POST /resources/{resource} creates an item", async (check) => {
200
+ if (writes === void 0 || resource === null) {
201
+ check.skip("Write checks are disabled.");
202
+ return;
203
+ }
204
+ const reply = await request(resourcePath(), {
205
+ method: "POST",
206
+ body: JSON.stringify(writes.create)
207
+ });
208
+ if (!check.expectStatus(reply, 201)) return;
209
+ const item = check.expectSchema(ItemResponse, reply.body, "The created item");
210
+ createdId = item?.data.id ?? null;
211
+ createdVersion = item?.data.version ?? null;
212
+ });
213
+ const itemPath = () => `${resourcePath()}/${encodeURIComponent(createdId ?? "")}`;
214
+ await run("update", "PATCH /resources/{resource}/{id} updates the item", async (check) => {
215
+ if (createdId === null || writes === void 0) {
216
+ check.skip("No item was created.");
217
+ return;
218
+ }
219
+ const reply = await request(itemPath(), {
220
+ method: "PATCH",
221
+ body: JSON.stringify(writes.update),
222
+ headers: createdVersion !== null ? { "If-Match": `"${createdVersion}"` } : {}
223
+ });
224
+ if (!check.expectStatus(reply, 200)) return;
225
+ const item = check.expectSchema(ItemResponse, reply.body, "The updated item");
226
+ if (item !== null && createdVersion !== null) check.expect(item.data.version !== createdVersion, "The version did not change after an update.");
227
+ });
228
+ await run("update.conflict", "A stale If-Match is a 412 problem with the current item", async (check) => {
229
+ if (createdId === null || writes === void 0) {
230
+ check.skip("No item was created.");
231
+ return;
232
+ }
233
+ if (createdVersion === null) {
234
+ check.skip("The backend has no versions.");
235
+ return;
236
+ }
237
+ const reply = await request(itemPath(), {
238
+ method: "PATCH",
239
+ body: JSON.stringify(writes.update),
240
+ headers: { "If-Match": "\"__panneau_conformance_stale__\"" }
241
+ });
242
+ const failure = check.expectProblem(reply, 412);
243
+ if (failure !== null) check.expect(failure.current !== void 0, "A 412 problem must include the current item.");
244
+ });
245
+ await run("delete", "DELETE /resources/{resource}/{id} deletes the item", async (check) => {
246
+ if (createdId === null) {
247
+ check.skip("No item was created.");
248
+ return;
249
+ }
250
+ if (!check.expectStatus(await request(itemPath(), { method: "DELETE" }), 204)) return;
251
+ check.expectProblem(await request(itemPath()), 404);
252
+ });
253
+ return {
254
+ passed: checks.every((check) => check.status !== "failed"),
255
+ checks
256
+ };
257
+ }
258
+ /** Formats a report as text, one line per check. */
259
+ function formatReport(report) {
260
+ const symbols = {
261
+ passed: "✓",
262
+ failed: "✗",
263
+ skipped: "-"
264
+ };
265
+ return report.checks.map((check) => [`${symbols[check.status]} ${check.title}`, ...check.issues.map((issue) => ` ${issue}`)].join("\n")).join("\n");
266
+ }
267
+ //#endregion
268
+ export { formatReport, runConformance };
269
+
270
+ //# 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 { type FilterRange, toSearchParams } from '../query';\nimport { Definition, type Resource } from '../schemas';\nimport { ItemResponse, ListResponse, Problem } from '../schemas/api';\nimport { isFilled } from '../values';\nimport { PROBLEM_CONTENT_TYPE } from '../version';\n\nexport interface ConformanceOptions {\n /** Base URL of the API, the same as `api.baseUrl` of the definition, but absolute. */\n baseUrl: string;\n /** The `fetch` to use. Defaults to the global one. */\n fetch?: (input: string, init?: RequestInit) => Promise<Response>;\n /** Merged into every request, for authentication headers or cookies. */\n init?: RequestInit;\n /** Resource to test. Defaults to the first resource of the definition. */\n resource?: string;\n /**\n * Enables the checks that write data, on the tested resource. Use a test database: these\n * checks create, update and delete an item.\n */\n writes?: {\n /** Valid values for a new item. */\n create: Record<string, unknown>;\n /** Valid values for an update. */\n update: Record<string, unknown>;\n /** Values that must be refused with a `422`. Defaults to `{}` when a field is required. */\n invalid?: Record<string, unknown>;\n };\n}\n\nexport type ConformanceStatus = 'passed' | 'failed' | 'skipped';\n\nexport interface ConformanceCheck {\n id: string;\n title: string;\n status: ConformanceStatus;\n /** What went wrong, or why the check was skipped. */\n issues: string[];\n}\n\nexport interface ConformanceReport {\n passed: boolean;\n checks: ConformanceCheck[];\n}\n\ninterface Reply {\n status: number;\n contentType: string;\n etag: string | null;\n body: unknown;\n}\n\nconst UNKNOWN = '__panneau_conformance_unknown__';\n\nfunction describeIssues(error: z.ZodError): string[] {\n return error.issues.slice(0, 10).map((issue) => {\n const path = issue.path.map(String).join('.');\n return path === '' ? issue.message : `${path}: ${issue.message}`;\n });\n}\n\nfunction normalizeEtag(value: string): string {\n return value.replace(/^W\\//, '');\n}\n\nclass Check {\n issues: string[] = [];\n skippedBecause: string | null = null;\n\n expect(condition: boolean, issue: string): boolean {\n if (!condition) {\n this.issues.push(issue);\n }\n return condition;\n }\n\n expectStatus(reply: Reply, status: number): boolean {\n return this.expect(\n reply.status === status,\n `Expected status ${status}, got ${reply.status}.`,\n );\n }\n\n expectSchema<T extends z.ZodType>(schema: T, value: unknown, label: string): z.infer<T> | null {\n const result = schema.safeParse(value);\n if (result.success) {\n return result.data;\n }\n this.issues.push(\n `${label} does not match the schema:`,\n ...describeIssues(result.error).map((issue) => ` ${issue}`),\n );\n return null;\n }\n\n expectProblem(reply: Reply, status: number): Problem | null {\n if (!this.expectStatus(reply, status)) {\n return null;\n }\n this.expect(\n reply.contentType.startsWith(PROBLEM_CONTENT_TYPE),\n `Expected content type ${PROBLEM_CONTENT_TYPE}, got \"${reply.contentType}\".`,\n );\n return this.expectSchema(Problem, reply.body, 'The error');\n }\n\n skip(reason: string) {\n this.skippedBecause = reason;\n }\n}\n\n/**\n * Runs the protocol's conformance checks against a live backend. Read-only checks always run;\n * checks that write data run only with `writes`.\n */\nexport async function runConformance(options: ConformanceOptions): Promise<ConformanceReport> {\n const fetcher = options.fetch ?? ((input: string, init?: RequestInit) => fetch(input, init));\n const baseUrl = options.baseUrl.replace(/\\/$/, '');\n const checks: ConformanceCheck[] = [];\n\n const request = async (path: string, init: RequestInit = {}): Promise<Reply> => {\n const headers = new Headers(options.init?.headers);\n headers.set('Accept', 'application/json');\n new Headers(init.headers).forEach((value, key) => headers.set(key, value));\n if (init.body !== undefined) {\n headers.set('Content-Type', 'application/json');\n }\n const response = await fetcher(`${baseUrl}${path}`, { ...options.init, ...init, headers });\n const text = await response.text();\n const parse = (): unknown => {\n if (text === '') {\n return null;\n }\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n };\n const body = parse();\n return {\n status: response.status,\n contentType: response.headers.get('Content-Type') ?? '',\n etag: response.headers.get('ETag'),\n body,\n };\n };\n\n const run = async (id: string, title: string, body: (check: Check) => Promise<void>) => {\n const check = new Check();\n try {\n await body(check);\n } catch (error) {\n check.issues.push(\n `Request failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n const status: ConformanceStatus =\n check.issues.length > 0\n ? 'failed'\n : check.skippedBecause !== null\n ? 'skipped'\n : 'passed';\n checks.push({\n id,\n title,\n status,\n issues:\n status === 'skipped' && check.skippedBecause !== null\n ? [check.skippedBecause]\n : check.issues,\n });\n };\n\n let definition: Definition | null = null;\n let resource: Resource | null = null;\n let firstItemId: string | null = null;\n\n await run('definition', 'GET /definition returns a valid definition', async (check) => {\n const reply = await request('/definition');\n if (check.expectStatus(reply, 200)) {\n definition = check.expectSchema(Definition, reply.body, 'The definition');\n }\n });\n\n await run('definition.resource', 'The definition has the resource to test', async (check) => {\n const current = definition as Definition | null;\n if (current === null) {\n check.skip('No valid definition.');\n return;\n }\n resource =\n (options.resource !== undefined\n ? current.resources.find((candidate) => candidate.id === options.resource)\n : current.resources[0]) ?? null;\n check.expect(\n resource !== null,\n options.resource !== undefined\n ? `The definition has no resource \"${options.resource}\".`\n : 'The definition has no resources.',\n );\n });\n\n const resourcePath = () =>\n `/resources/${encodeURIComponent((resource as Resource | null)?.id ?? '')}`;\n\n await run('list', 'GET /resources/{resource} returns a page of items', async (check) => {\n if (resource === null) {\n check.skip('No resource to test.');\n return;\n }\n const reply = await request(`${resourcePath()}?page=1&perPage=2`);\n if (!check.expectStatus(reply, 200)) {\n return;\n }\n const list = check.expectSchema(ListResponse, reply.body, 'The list');\n if (list === null) {\n return;\n }\n check.expect(list.data.length <= 2, `perPage=2 returned ${list.data.length} items.`);\n check.expect(list.meta.page === 1, `meta.page is ${list.meta.page}, expected 1.`);\n check.expect(list.meta.perPage === 2, `meta.perPage is ${list.meta.perPage}, expected 2.`);\n firstItemId = list.data[0]?.id ?? null;\n });\n\n await run('list.sort', 'GET /resources/{resource} accepts a sort', async (check) => {\n const sort = (resource as Resource | null)?.list?.sorts?.[0];\n if (sort === undefined) {\n check.skip('The resource declares no sorts.');\n return;\n }\n const reply = await request(`${resourcePath()}?sort=${encodeURIComponent(`-${sort}`)}`);\n if (check.expectStatus(reply, 200)) {\n check.expectSchema(ListResponse, reply.body, 'The sorted list');\n }\n });\n\n await run(\n 'list.filter-range',\n 'GET /resources/{resource} reads the bounds of a range filter',\n async (check) => {\n const filter = (resource as Resource | null)?.list?.filters?.find(\n (candidate) => candidate.type === 'date-range',\n );\n if (filter === undefined) {\n check.skip('The resource declares no date-range filter.');\n return;\n }\n const list = async (range: FilterRange | null, label: string) => {\n const query =\n range !== null ? `?${toSearchParams({ filter: { [filter.id]: range } })}` : '';\n const reply = await request(`${resourcePath()}${query}`);\n if (!check.expectStatus(reply, 200)) {\n return null;\n }\n return check.expectSchema(ListResponse, reply.body, label);\n };\n const all = await list(null, 'The list');\n if (all === null) {\n return;\n }\n // Items of the first page with a value: a range that covers every day includes them.\n const dated = all.data.filter((item) => isFilled(item[filter.id])).length;\n const ranges: [FilterRange, string][] = [\n [{ from: '0001-01-01', to: '9999-12-31' }, 'from 0001-01-01 to 9999-12-31'],\n [{ to: '9999-12-31' }, 'up to 9999-12-31 (an open start)'],\n ];\n for (const [range, label] of ranges) {\n const reply = await list(range, `The list filtered ${label}`);\n if (reply !== null) {\n check.expect(\n reply.meta.total >= dated,\n `Filtered ${label}, the list has ${reply.meta.total} items; the first page alone has ${dated} with a ${filter.id}.`,\n );\n }\n }\n const future = await list({ from: '9999-12-31' }, 'The list filtered from 9999-12-31');\n if (future !== null) {\n check.expect(\n future.meta.total === 0,\n `Filtered from 9999-12-31, the list has ${future.meta.total} items, expected 0.`,\n );\n }\n },\n );\n\n await run('list.unknown-resource', 'An unknown resource is a 404 problem', async (check) => {\n check.expectProblem(await request(`/resources/${UNKNOWN}`), 404);\n });\n\n await run(\n 'item',\n 'GET /resources/{resource}/{id} returns the item with its ETag',\n async (check) => {\n if (firstItemId === null) {\n check.skip('The list has no items.');\n return;\n }\n const reply = await request(`${resourcePath()}/${encodeURIComponent(firstItemId)}`);\n if (!check.expectStatus(reply, 200)) {\n return;\n }\n const item = check.expectSchema(ItemResponse, reply.body, 'The item');\n if (item === null) {\n return;\n }\n check.expect(\n item.data.id === firstItemId,\n `Asked for item \"${firstItemId}\", got \"${item.data.id}\".`,\n );\n if (item.data.version !== null) {\n check.expect(\n reply.etag !== null && normalizeEtag(reply.etag) === `\"${item.data.version}\"`,\n `Expected ETag \"${item.data.version}\" (quoted), got ${reply.etag ?? 'none'}.`,\n );\n }\n },\n );\n\n await run('item.missing', 'A missing item is a 404 problem', async (check) => {\n if (resource === null) {\n check.skip('No resource to test.');\n return;\n }\n check.expectProblem(await request(`${resourcePath()}/${UNKNOWN}`), 404);\n });\n\n const writes = options.writes;\n let createdId: string | null = null;\n let createdVersion: string | null = null;\n\n await run('create.invalid', 'Invalid values are refused with a 422 problem', async (check) => {\n if (writes === undefined || resource === null) {\n check.skip('Write checks are disabled.');\n return;\n }\n const hasRequired = resource.fields.some((field) =>\n (field.rules ?? []).some((rule) => rule.rule === 'required'),\n );\n if (writes.invalid === undefined && !hasRequired) {\n check.skip('No invalid values given, and the resource has no required field.');\n return;\n }\n const reply = await request(resourcePath(), {\n method: 'POST',\n body: JSON.stringify(writes.invalid ?? {}),\n });\n const failure = check.expectProblem(reply, 422);\n if (failure !== null) {\n check.expect(\n failure.errors !== undefined && Object.keys(failure.errors).length > 0,\n 'A 422 problem must list the invalid values in `errors`.',\n );\n }\n });\n\n await run('create', 'POST /resources/{resource} creates an item', async (check) => {\n if (writes === undefined || resource === null) {\n check.skip('Write checks are disabled.');\n return;\n }\n const reply = await request(resourcePath(), {\n method: 'POST',\n body: JSON.stringify(writes.create),\n });\n if (!check.expectStatus(reply, 201)) {\n return;\n }\n const item = check.expectSchema(ItemResponse, reply.body, 'The created item');\n createdId = item?.data.id ?? null;\n createdVersion = item?.data.version ?? null;\n });\n\n const itemPath = () => `${resourcePath()}/${encodeURIComponent(createdId ?? '')}`;\n\n await run('update', 'PATCH /resources/{resource}/{id} updates the item', async (check) => {\n if (createdId === null || writes === undefined) {\n check.skip('No item was created.');\n return;\n }\n const reply = await request(itemPath(), {\n method: 'PATCH',\n body: JSON.stringify(writes.update),\n headers: createdVersion !== null ? { 'If-Match': `\"${createdVersion}\"` } : {},\n });\n if (!check.expectStatus(reply, 200)) {\n return;\n }\n const item = check.expectSchema(ItemResponse, reply.body, 'The updated item');\n if (item !== null && createdVersion !== null) {\n check.expect(\n item.data.version !== createdVersion,\n 'The version did not change after an update.',\n );\n }\n });\n\n await run(\n 'update.conflict',\n 'A stale If-Match is a 412 problem with the current item',\n async (check) => {\n if (createdId === null || writes === undefined) {\n check.skip('No item was created.');\n return;\n }\n if (createdVersion === null) {\n check.skip('The backend has no versions.');\n return;\n }\n const reply = await request(itemPath(), {\n method: 'PATCH',\n body: JSON.stringify(writes.update),\n headers: { 'If-Match': '\"__panneau_conformance_stale__\"' },\n });\n const failure = check.expectProblem(reply, 412);\n if (failure !== null) {\n check.expect(\n failure.current !== undefined,\n 'A 412 problem must include the current item.',\n );\n }\n },\n );\n\n await run('delete', 'DELETE /resources/{resource}/{id} deletes the item', async (check) => {\n if (createdId === null) {\n check.skip('No item was created.');\n return;\n }\n if (!check.expectStatus(await request(itemPath(), { method: 'DELETE' }), 204)) {\n return;\n }\n check.expectProblem(await request(itemPath()), 404);\n });\n\n return { passed: checks.every((check) => check.status !== 'failed'), checks };\n}\n\n/** Formats a report as text, one line per check. */\nexport function formatReport(report: ConformanceReport): string {\n const symbols: Record<ConformanceStatus, string> = { passed: '✓', failed: '✗', skipped: '-' };\n return report.checks\n .map((check) =>\n [\n `${symbols[check.status]} ${check.title}`,\n ...check.issues.map((issue) => ` ${issue}`),\n ].join('\\n'),\n )\n .join('\\n');\n}\n"],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ /** Built-in action ids. */
2
+ export declare const BUILT_IN_ACTIONS: readonly ["show", "edit", "duplicate", "delete", "restore", "export", "import"];
3
+ export type BuiltInAction = (typeof BUILT_IN_ACTIONS)[number];
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"];
6
+ export type BuiltInFieldType = (typeof BUILT_IN_FIELD_TYPES)[number];
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"];
9
+ export type PortableRule = (typeof PORTABLE_RULES)[number];
10
+ /** Layout node types every Panneau frontend renders. */
11
+ export declare const BUILT_IN_LAYOUT_NODE_TYPES: readonly ["row", "column", "section", "tabs", "tab", "sidebar", "field", "text", "component"];
12
+ export type BuiltInLayoutNodeType = (typeof BUILT_IN_LAYOUT_NODE_TYPES)[number];
13
+ /** List layouts every Panneau frontend renders. Plugins add others. */
14
+ export declare const BUILT_IN_LIST_LAYOUTS: readonly ["table", "cards", "gallery", "calendar"];
15
+ export type BuiltInListLayout = (typeof BUILT_IN_LIST_LAYOUTS)[number];
16
+ /**
17
+ * Id of the element that holds the boot config in the page: a
18
+ * `<script type="application/json" id="panneau-boot">` element whose text is the JSON object.
19
+ */
20
+ export declare const BOOT_CONFIG_ELEMENT_ID = "panneau-boot";
21
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAGA,2BAA2B;AAC3B,eAAO,MAAM,gBAAgB,iFAQnB,CAAC;AACX,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE9D,sEAAsE;AACtE,eAAO,MAAM,oBAAoB,iLAkBvB,CAAC;AACX,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AAErE,+DAA+D;AAC/D,eAAO,MAAM,cAAc,8GAYjB,CAAC;AACX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC;AAE3D,wDAAwD;AACxD,eAAO,MAAM,0BAA0B,+FAU7B,CAAC;AACX,MAAM,MAAM,qBAAqB,GAAG,CAAC,OAAO,0BAA0B,CAAC,CAAC,MAAM,CAAC,CAAC;AAEhF,uEAAuE;AACvE,eAAO,MAAM,qBAAqB,oDAAqD,CAAC;AACxF,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvE;;;GAGG;AACH,eAAO,MAAM,sBAAsB,iBAAiB,CAAC"}
@@ -0,0 +1,73 @@
1
+ //#region src/constants.ts
2
+ /** Built-in action ids. */
3
+ var BUILT_IN_ACTIONS = [
4
+ "show",
5
+ "edit",
6
+ "duplicate",
7
+ "delete",
8
+ "restore",
9
+ "export",
10
+ "import"
11
+ ];
12
+ /** Field types every Panneau frontend renders. Plugins add others. */
13
+ var BUILT_IN_FIELD_TYPES = [
14
+ "text",
15
+ "textarea",
16
+ "richtext",
17
+ "number",
18
+ "boolean",
19
+ "date",
20
+ "datetime",
21
+ "time",
22
+ "select",
23
+ "email",
24
+ "url",
25
+ "color",
26
+ "media",
27
+ "relation",
28
+ "group",
29
+ "repeater",
30
+ "slug"
31
+ ];
32
+ /** Validation rules that both frontends and backends check. */
33
+ var PORTABLE_RULES = [
34
+ "required",
35
+ "min",
36
+ "max",
37
+ "pattern",
38
+ "email",
39
+ "url",
40
+ "in",
41
+ "accept",
42
+ "maxSize",
43
+ "before",
44
+ "after"
45
+ ];
46
+ /** Layout node types every Panneau frontend renders. */
47
+ var BUILT_IN_LAYOUT_NODE_TYPES = [
48
+ "row",
49
+ "column",
50
+ "section",
51
+ "tabs",
52
+ "tab",
53
+ "sidebar",
54
+ "field",
55
+ "text",
56
+ "component"
57
+ ];
58
+ /** List layouts every Panneau frontend renders. Plugins add others. */
59
+ var BUILT_IN_LIST_LAYOUTS = [
60
+ "table",
61
+ "cards",
62
+ "gallery",
63
+ "calendar"
64
+ ];
65
+ /**
66
+ * Id of the element that holds the boot config in the page: a
67
+ * `<script type="application/json" id="panneau-boot">` element whose text is the JSON object.
68
+ */
69
+ var BOOT_CONFIG_ELEMENT_ID = "panneau-boot";
70
+ //#endregion
71
+ export { BOOT_CONFIG_ELEMENT_ID, BUILT_IN_ACTIONS, BUILT_IN_FIELD_TYPES, BUILT_IN_LAYOUT_NODE_TYPES, BUILT_IN_LIST_LAYOUTS, PORTABLE_RULES };
72
+
73
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +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"}
@@ -0,0 +1,5 @@
1
+ export * from './runtime';
2
+ export * from './schemas';
3
+ export { type ValidationIssue, type ValidationResult, validate, validateBootConfig, validateDefinition, } from './validate';
4
+ export { createBootConfigJsonSchema, createComponentSchemas, createDefinitionJsonSchema, createOpenApiDocument, } from './openapi';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,OAAO,EACH,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,QAAQ,EACR,kBAAkB,EAClB,kBAAkB,GACrB,MAAM,YAAY,CAAC;AACpB,OAAO,EACH,0BAA0B,EAC1B,sBAAsB,EACtB,0BAA0B,EAC1B,qBAAqB,GACxB,MAAM,WAAW,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,21 @@
1
+ import { PROBLEM_CONTENT_TYPE, PROTOCOL_VERSION } from "./version.js";
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
+ import { getValue, isFilled } from "./values.js";
4
+ import { evaluateCondition } from "./conditions.js";
5
+ import { checkRules, checkValues } from "./rules.js";
6
+ import { lintDefinition } from "./lint.js";
7
+ import { fromSearchParams, isFilterRange, matchesFilterRange, toSearchParams } from "./query.js";
8
+ import { isProblem } from "./problem.js";
9
+ import { Abilities, Condition, Id, Option, RouteReference } from "./schemas/common.js";
10
+ import { BlockType, Field, Rule } from "./schemas/field.js";
11
+ import { LayoutNode } from "./schemas/layout.js";
12
+ import { Action, ActionConfirm, ActionPlacement, ActionScope } from "./schemas/action.js";
13
+ import { Column, Filter, ListDefinition } from "./schemas/list.js";
14
+ import { Resource, ResourceFeatures } from "./schemas/resource.js";
15
+ import { Auth, AuthProvider, Definition, MenuItem, Page, Uploads, User, Widget } from "./schemas/definition.js";
16
+ import { ActionItemResult, ActionRequest, ActionResult, ActionRun, ActionRunResponse, Item, ItemResponse, ListResponse, Media, MediaListResponse, MediaResponse, MediaTransform, Pagination, PreviewResponse, Problem, SearchResponse, Selection, Session } from "./schemas/api.js";
17
+ import { BootConfig } from "./schemas/boot.js";
18
+ import { createSchemaRegistry, namedSchemas } from "./schemas/index.js";
19
+ import { validate, validateBootConfig, validateDefinition } from "./validate.js";
20
+ import { createBootConfigJsonSchema, createComponentSchemas, createDefinitionJsonSchema, createOpenApiDocument } from "./openapi.js";
21
+ 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, isFilterRange, isProblem, lintDefinition, matchesFilterRange, 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"}