@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
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
//#region src/mock/example.ts
|
|
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
|
+
function createExampleDefinition() {
|
|
8
|
+
return {
|
|
9
|
+
protocol: 1,
|
|
10
|
+
name: "Test site",
|
|
11
|
+
locale: "fr",
|
|
12
|
+
locales: ["fr", "en"],
|
|
13
|
+
api: { baseUrl: "/panneau/api" },
|
|
14
|
+
resources: [{
|
|
15
|
+
id: "pages",
|
|
16
|
+
label: {
|
|
17
|
+
singular: "page",
|
|
18
|
+
plural: "pages"
|
|
19
|
+
},
|
|
20
|
+
itemTitle: "title",
|
|
21
|
+
fields: [
|
|
22
|
+
{
|
|
23
|
+
name: "title",
|
|
24
|
+
type: "text",
|
|
25
|
+
label: "Titre",
|
|
26
|
+
localized: true,
|
|
27
|
+
rules: [{ rule: "required" }, {
|
|
28
|
+
rule: "max",
|
|
29
|
+
value: 120
|
|
30
|
+
}]
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: "status",
|
|
34
|
+
type: "select",
|
|
35
|
+
options: [{
|
|
36
|
+
value: "draft",
|
|
37
|
+
label: "Brouillon"
|
|
38
|
+
}, {
|
|
39
|
+
value: "published",
|
|
40
|
+
label: "Publiée"
|
|
41
|
+
}],
|
|
42
|
+
rules: [{ rule: "required" }, {
|
|
43
|
+
rule: "in",
|
|
44
|
+
value: ["draft", "published"]
|
|
45
|
+
}]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "published_at",
|
|
49
|
+
type: "datetime",
|
|
50
|
+
visibleWhen: {
|
|
51
|
+
field: "status",
|
|
52
|
+
equals: "published"
|
|
53
|
+
},
|
|
54
|
+
rules: [{ rule: "required" }]
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "author",
|
|
58
|
+
type: "relation",
|
|
59
|
+
resource: "authors"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "blocks",
|
|
63
|
+
type: "repeater",
|
|
64
|
+
types: [{
|
|
65
|
+
id: "text",
|
|
66
|
+
label: "Texte",
|
|
67
|
+
fields: [{
|
|
68
|
+
name: "body",
|
|
69
|
+
type: "richtext",
|
|
70
|
+
rules: [{ rule: "required" }]
|
|
71
|
+
}]
|
|
72
|
+
}, {
|
|
73
|
+
id: "image",
|
|
74
|
+
label: "Image",
|
|
75
|
+
fields: [{
|
|
76
|
+
name: "image",
|
|
77
|
+
type: "media",
|
|
78
|
+
rules: [{
|
|
79
|
+
rule: "accept",
|
|
80
|
+
value: ["image/*"]
|
|
81
|
+
}, {
|
|
82
|
+
rule: "maxSize",
|
|
83
|
+
value: 1e3
|
|
84
|
+
}]
|
|
85
|
+
}]
|
|
86
|
+
}]
|
|
87
|
+
}
|
|
88
|
+
],
|
|
89
|
+
form: { layout: [{
|
|
90
|
+
type: "tabs",
|
|
91
|
+
children: [{
|
|
92
|
+
type: "tab",
|
|
93
|
+
label: "Contenu",
|
|
94
|
+
children: [{
|
|
95
|
+
type: "row",
|
|
96
|
+
children: [{
|
|
97
|
+
type: "field",
|
|
98
|
+
name: "title",
|
|
99
|
+
width: 8
|
|
100
|
+
}, {
|
|
101
|
+
type: "field",
|
|
102
|
+
name: "status",
|
|
103
|
+
width: 4
|
|
104
|
+
}]
|
|
105
|
+
}, {
|
|
106
|
+
type: "field",
|
|
107
|
+
name: "blocks"
|
|
108
|
+
}]
|
|
109
|
+
}]
|
|
110
|
+
}, {
|
|
111
|
+
type: "sidebar",
|
|
112
|
+
children: [{
|
|
113
|
+
type: "field",
|
|
114
|
+
name: "published_at"
|
|
115
|
+
}]
|
|
116
|
+
}] },
|
|
117
|
+
list: {
|
|
118
|
+
columns: [
|
|
119
|
+
{
|
|
120
|
+
field: "title",
|
|
121
|
+
sortable: true
|
|
122
|
+
},
|
|
123
|
+
{ field: "status" },
|
|
124
|
+
{ id: "updated" }
|
|
125
|
+
],
|
|
126
|
+
filters: [{
|
|
127
|
+
id: "status",
|
|
128
|
+
type: "select"
|
|
129
|
+
}],
|
|
130
|
+
sorts: ["title", "published_at"],
|
|
131
|
+
defaultSort: "title",
|
|
132
|
+
perPage: 20,
|
|
133
|
+
search: true
|
|
134
|
+
},
|
|
135
|
+
actions: [{
|
|
136
|
+
id: "publish",
|
|
137
|
+
label: "Publier",
|
|
138
|
+
scope: ["item", "selection"],
|
|
139
|
+
placement: ["row", "toolbar"],
|
|
140
|
+
confirm: { message: "Publier {count} pages ?" },
|
|
141
|
+
fields: [{
|
|
142
|
+
name: "notify",
|
|
143
|
+
type: "boolean"
|
|
144
|
+
}]
|
|
145
|
+
}, {
|
|
146
|
+
id: "edit",
|
|
147
|
+
label: "Modifier",
|
|
148
|
+
kind: "link",
|
|
149
|
+
url: "/pages/{id}/edit",
|
|
150
|
+
scope: ["item"]
|
|
151
|
+
}],
|
|
152
|
+
abilities: {
|
|
153
|
+
viewAny: true,
|
|
154
|
+
create: true
|
|
155
|
+
}
|
|
156
|
+
}, {
|
|
157
|
+
id: "authors",
|
|
158
|
+
label: {
|
|
159
|
+
singular: "auteur",
|
|
160
|
+
plural: "auteurs"
|
|
161
|
+
},
|
|
162
|
+
fields: [{
|
|
163
|
+
name: "name",
|
|
164
|
+
type: "text",
|
|
165
|
+
rules: [{ rule: "required" }]
|
|
166
|
+
}]
|
|
167
|
+
}],
|
|
168
|
+
menus: { main: [{ type: "resources" }, {
|
|
169
|
+
type: "link",
|
|
170
|
+
label: "Site",
|
|
171
|
+
url: "/"
|
|
172
|
+
}] },
|
|
173
|
+
auth: {
|
|
174
|
+
mode: "panneau",
|
|
175
|
+
password: true,
|
|
176
|
+
providers: [{
|
|
177
|
+
id: "google",
|
|
178
|
+
label: "Google"
|
|
179
|
+
}]
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
//#endregion
|
|
184
|
+
export { createExampleDefinition };
|
|
185
|
+
|
|
186
|
+
//# sourceMappingURL=example.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"example.js","names":[],"sources":["../../src/mock/example.ts"],"sourcesContent":["import type { DefinitionInput } from '../schemas';\n\n/**\n * A small but complete definition: pages with localized titles, a status, a publication date shown\n * for published pages, an author and typed blocks, in tabs and a sidebar, plus authors. For tests\n * and stories, with `createMockBackend()`.\n */\nexport function createExampleDefinition(): DefinitionInput {\n return {\n protocol: 1,\n name: 'Test site',\n locale: 'fr',\n locales: ['fr', 'en'],\n api: { baseUrl: '/panneau/api' },\n resources: [\n {\n id: 'pages',\n label: { singular: 'page', plural: 'pages' },\n itemTitle: 'title',\n fields: [\n {\n name: 'title',\n type: 'text',\n label: 'Titre',\n localized: true,\n rules: [{ rule: 'required' }, { rule: 'max', value: 120 }],\n },\n {\n name: 'status',\n type: 'select',\n options: [\n { value: 'draft', label: 'Brouillon' },\n { value: 'published', label: 'Publiée' },\n ],\n rules: [\n { rule: 'required' },\n { rule: 'in', value: ['draft', 'published'] },\n ],\n },\n {\n name: 'published_at',\n type: 'datetime',\n visibleWhen: { field: 'status', equals: 'published' },\n rules: [{ rule: 'required' }],\n },\n { name: 'author', type: 'relation', resource: 'authors' },\n {\n name: 'blocks',\n type: 'repeater',\n types: [\n {\n id: 'text',\n label: 'Texte',\n fields: [\n {\n name: 'body',\n type: 'richtext',\n rules: [{ rule: 'required' }],\n },\n ],\n },\n {\n id: 'image',\n label: 'Image',\n fields: [\n {\n name: 'image',\n type: 'media',\n rules: [\n { rule: 'accept', value: ['image/*'] },\n { rule: 'maxSize', value: 1000 },\n ],\n },\n ],\n },\n ],\n },\n ],\n form: {\n layout: [\n {\n type: 'tabs',\n children: [\n {\n type: 'tab',\n label: 'Contenu',\n children: [\n {\n type: 'row',\n children: [\n { type: 'field', name: 'title', width: 8 },\n { type: 'field', name: 'status', width: 4 },\n ],\n },\n { type: 'field', name: 'blocks' },\n ],\n },\n ],\n },\n { type: 'sidebar', children: [{ type: 'field', name: 'published_at' }] },\n ],\n },\n list: {\n columns: [\n { field: 'title', sortable: true },\n { field: 'status' },\n { id: 'updated' },\n ],\n filters: [{ id: 'status', type: 'select' }],\n sorts: ['title', 'published_at'],\n defaultSort: 'title',\n perPage: 20,\n search: true,\n },\n actions: [\n {\n id: 'publish',\n label: 'Publier',\n scope: ['item', 'selection'],\n placement: ['row', 'toolbar'],\n confirm: { message: 'Publier {count} pages ?' },\n fields: [{ name: 'notify', type: 'boolean' }],\n },\n {\n id: 'edit',\n label: 'Modifier',\n kind: 'link',\n url: '/pages/{id}/edit',\n scope: ['item'],\n },\n ],\n abilities: { viewAny: true, create: true },\n },\n {\n id: 'authors',\n label: { singular: 'auteur', plural: 'auteurs' },\n fields: [{ name: 'name', type: 'text', rules: [{ rule: 'required' }] }],\n },\n ],\n menus: { main: [{ type: 'resources' }, { type: 'link', label: 'Site', url: '/' }] },\n auth: { mode: 'panneau', password: true, providers: [{ id: 'google', label: 'Google' }] },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { DefinitionInput } from '../schemas';
|
|
2
|
+
import type { Item } from '../schemas/api';
|
|
3
|
+
type Values = Record<string, unknown>;
|
|
4
|
+
export interface MockBackendOptions {
|
|
5
|
+
/** The definition served by `GET /definition`. Its `api.baseUrl` is the prefix of every route. */
|
|
6
|
+
definition: DefinitionInput;
|
|
7
|
+
/** Initial values of the items, per resource id. Items without an `id` get one. */
|
|
8
|
+
items?: Record<string, Values[]>;
|
|
9
|
+
}
|
|
10
|
+
export interface MockBackend {
|
|
11
|
+
/** A `fetch`-compatible function that answers protocol requests. */
|
|
12
|
+
fetch: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
13
|
+
/** The current items of a resource. */
|
|
14
|
+
items: (resource: string) => Item[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* An in-memory backend that implements the protocol's resource routes: definition, list, show,
|
|
18
|
+
* create, update (with `If-Match`), delete and the built-in `delete` action. Values are checked
|
|
19
|
+
* with the portable rules. Used to test the conformance kit, and to develop frontends without a
|
|
20
|
+
* real backend.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createMockBackend({ definition, items }: MockBackendOptions): MockBackend;
|
|
23
|
+
export { createExampleDefinition } from './example';
|
|
24
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/mock/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAiB,MAAM,YAAY,CAAC;AACjE,OAAO,KAAK,EAAgB,IAAI,EAAsB,MAAM,gBAAgB,CAAC;AAI7E,KAAK,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEtC,MAAM,WAAW,kBAAkB;IAC/B,kGAAkG;IAClG,UAAU,EAAE,eAAe,CAAC;IAC5B,mFAAmF;IACnF,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,WAAW;IACxB,oEAAoE;IACpE,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,OAAO,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IAChF,uCAAuC;IACvC,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,EAAE,CAAC;CACvC;AAyGD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,UAAU,EAAE,KAAU,EAAE,EAAE,kBAAkB,GAAG,WAAW,CAyN7F;AAED,OAAO,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC"}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { PROBLEM_CONTENT_TYPE } from "../version.js";
|
|
2
|
+
import { isRecord } from "../values.js";
|
|
3
|
+
import { checkValues } from "../rules.js";
|
|
4
|
+
import { fromSearchParams } from "../query.js";
|
|
5
|
+
import { createExampleDefinition } from "./example.js";
|
|
6
|
+
//#region src/mock/index.ts
|
|
7
|
+
var RESERVED_KEYS = /* @__PURE__ */ new Set([
|
|
8
|
+
"id",
|
|
9
|
+
"version",
|
|
10
|
+
"abilities"
|
|
11
|
+
]);
|
|
12
|
+
var DEFAULT_MESSAGES = {
|
|
13
|
+
required: () => "This field is required.",
|
|
14
|
+
min: (failure) => `This value must be at least ${String(failure.value)}.`,
|
|
15
|
+
max: (failure) => `This value must be at most ${String(failure.value)}.`,
|
|
16
|
+
pattern: () => "This value has an invalid format.",
|
|
17
|
+
email: () => "This value must be an email address.",
|
|
18
|
+
url: () => "This value must be a URL.",
|
|
19
|
+
in: () => "This value is not one of the choices.",
|
|
20
|
+
accept: () => "This file type is not accepted.",
|
|
21
|
+
maxSize: () => "This file is too large.",
|
|
22
|
+
before: () => "This date is too late.",
|
|
23
|
+
after: () => "This date is too early."
|
|
24
|
+
};
|
|
25
|
+
function message(failure) {
|
|
26
|
+
return failure.message ?? DEFAULT_MESSAGES[failure.rule]?.(failure) ?? "This value is invalid.";
|
|
27
|
+
}
|
|
28
|
+
function json(status, body, headers = {}) {
|
|
29
|
+
return new Response(status === 204 ? null : JSON.stringify(body), {
|
|
30
|
+
status,
|
|
31
|
+
headers: status === 204 ? headers : {
|
|
32
|
+
"Content-Type": "application/json",
|
|
33
|
+
...headers
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function problem(status, title, extra = {}) {
|
|
38
|
+
const body = {
|
|
39
|
+
type: "about:blank",
|
|
40
|
+
title,
|
|
41
|
+
status,
|
|
42
|
+
...extra
|
|
43
|
+
};
|
|
44
|
+
return new Response(JSON.stringify(body), {
|
|
45
|
+
status,
|
|
46
|
+
headers: { "Content-Type": PROBLEM_CONTENT_TYPE }
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function etag(item) {
|
|
50
|
+
return item.version === null ? {} : { ETag: `"${item.version}"` };
|
|
51
|
+
}
|
|
52
|
+
function parseEtag(value) {
|
|
53
|
+
return value.replace(/^W\//, "").replace(/^"(.*)"$/, "$1");
|
|
54
|
+
}
|
|
55
|
+
function searchableText(value) {
|
|
56
|
+
if (typeof value === "string") return value;
|
|
57
|
+
if (isRecord(value)) return Object.values(value).map(searchableText).join(" ");
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
function compare(a, b) {
|
|
61
|
+
const left = isRecord(a) ? searchableText(a) : a;
|
|
62
|
+
const right = isRecord(b) ? searchableText(b) : b;
|
|
63
|
+
if (left === right) return 0;
|
|
64
|
+
if (left === void 0 || left === null) return 1;
|
|
65
|
+
if (right === void 0 || right === null) return -1;
|
|
66
|
+
return String(left).localeCompare(String(right), void 0, { numeric: true });
|
|
67
|
+
}
|
|
68
|
+
function applyQuery(items, query) {
|
|
69
|
+
let result = items;
|
|
70
|
+
Object.entries(query.filter ?? {}).forEach(([key, expected]) => {
|
|
71
|
+
const accepted = (Array.isArray(expected) ? expected : [expected]).map(String);
|
|
72
|
+
result = result.filter((item) => {
|
|
73
|
+
const value = item[key];
|
|
74
|
+
return (Array.isArray(value) ? value : [value]).some((candidate) => accepted.includes(String(candidate)));
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
if (query.q !== void 0 && query.q !== "") {
|
|
78
|
+
const terms = query.q.toLowerCase();
|
|
79
|
+
result = result.filter((item) => Object.entries(item).filter(([key]) => !RESERVED_KEYS.has(key)).some(([, value]) => searchableText(value).toLowerCase().includes(terms)));
|
|
80
|
+
}
|
|
81
|
+
const sort = query.sort ?? [];
|
|
82
|
+
if (sort.length > 0) result = [...result].sort((a, b) => {
|
|
83
|
+
for (const part of sort) {
|
|
84
|
+
const descending = part.startsWith("-");
|
|
85
|
+
const key = descending ? part.slice(1) : part;
|
|
86
|
+
const order = compare(a[key], b[key]);
|
|
87
|
+
if (order !== 0) return descending ? -order : order;
|
|
88
|
+
}
|
|
89
|
+
return 0;
|
|
90
|
+
});
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* An in-memory backend that implements the protocol's resource routes: definition, list, show,
|
|
95
|
+
* create, update (with `If-Match`), delete and the built-in `delete` action. Values are checked
|
|
96
|
+
* with the portable rules. Used to test the conformance kit, and to develop frontends without a
|
|
97
|
+
* real backend.
|
|
98
|
+
*/
|
|
99
|
+
function createMockBackend({ definition, items = {} }) {
|
|
100
|
+
const store = /* @__PURE__ */ new Map();
|
|
101
|
+
let nextId = 1;
|
|
102
|
+
const abilitiesFor = (resource) => Object.fromEntries([
|
|
103
|
+
["update", true],
|
|
104
|
+
["delete", true],
|
|
105
|
+
...(resource.actions ?? []).map((action) => [action.id, true])
|
|
106
|
+
]);
|
|
107
|
+
const makeItem = (resource, values) => {
|
|
108
|
+
const id = typeof values.id === "string" || typeof values.id === "number" ? String(values.id) : String(nextId++);
|
|
109
|
+
return {
|
|
110
|
+
...Object.fromEntries(Object.entries(values).filter(([key]) => !RESERVED_KEYS.has(key))),
|
|
111
|
+
id,
|
|
112
|
+
version: "1",
|
|
113
|
+
abilities: abilitiesFor(resource)
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
definition.resources.forEach((resource) => {
|
|
117
|
+
store.set(resource.id, (items[resource.id] ?? []).map((values) => makeItem(resource, values)));
|
|
118
|
+
});
|
|
119
|
+
const basePath = new URL(definition.api.baseUrl, "http://panneau.test").pathname.replace(/\/$/, "");
|
|
120
|
+
const validationProblem = (resource, values, mode) => {
|
|
121
|
+
const failures = checkValues(resource.fields, values, {
|
|
122
|
+
mode,
|
|
123
|
+
partial: mode === "update",
|
|
124
|
+
locales: definition.locales
|
|
125
|
+
});
|
|
126
|
+
const paths = Object.keys(failures);
|
|
127
|
+
if (paths.length === 0) return null;
|
|
128
|
+
return problem(422, "The given data was invalid.", {
|
|
129
|
+
type: "urn:panneau:problem:validation",
|
|
130
|
+
errors: Object.fromEntries(paths.map((path) => [path, (failures[path] ?? []).map(message)]))
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
const selectItems = (list, selection) => {
|
|
134
|
+
if (selection === void 0) return [];
|
|
135
|
+
if ("ids" in selection) {
|
|
136
|
+
const ids = selection.ids;
|
|
137
|
+
return list.filter((item) => ids.includes(item.id));
|
|
138
|
+
}
|
|
139
|
+
const except = selection.except ?? [];
|
|
140
|
+
const { filter, q } = selection.query;
|
|
141
|
+
const query = {};
|
|
142
|
+
if (filter !== void 0) query.filter = filter;
|
|
143
|
+
if (q !== void 0) query.q = q;
|
|
144
|
+
return applyQuery(list, query).filter((item) => !except.includes(item.id));
|
|
145
|
+
};
|
|
146
|
+
const handle = async (request) => {
|
|
147
|
+
const url = new URL(request.url, "http://panneau.test");
|
|
148
|
+
if (!url.pathname.startsWith(basePath)) return problem(404, "Not found.");
|
|
149
|
+
const segments = url.pathname.slice(basePath.length).split("/").filter((segment) => segment !== "").map(decodeURIComponent);
|
|
150
|
+
const method = request.method.toUpperCase();
|
|
151
|
+
const readBody = async () => {
|
|
152
|
+
const body = await request.json().catch(() => null);
|
|
153
|
+
return isRecord(body) ? body : {};
|
|
154
|
+
};
|
|
155
|
+
if (segments.length === 1 && segments[0] === "definition" && method === "GET") return json(200, definition);
|
|
156
|
+
if (segments[0] !== "resources" || segments[1] === void 0) return problem(404, "Not found.");
|
|
157
|
+
const resource = definition.resources.find((candidate) => candidate.id === segments[1]);
|
|
158
|
+
const list = resource !== void 0 ? store.get(resource.id) : void 0;
|
|
159
|
+
if (resource === void 0 || list === void 0) return problem(404, "Resource not found.");
|
|
160
|
+
if (segments.length === 2) {
|
|
161
|
+
if (method === "GET") {
|
|
162
|
+
const query = fromSearchParams(url.searchParams);
|
|
163
|
+
const perPage = query.perPage ?? resource.list?.perPage ?? 20;
|
|
164
|
+
const page = query.page ?? 1;
|
|
165
|
+
const sort = query.sort ?? resource.list?.defaultSort?.split(",");
|
|
166
|
+
const matching = applyQuery(list, {
|
|
167
|
+
...query,
|
|
168
|
+
...sort !== void 0 ? { sort } : {}
|
|
169
|
+
});
|
|
170
|
+
return json(200, {
|
|
171
|
+
data: matching.slice((page - 1) * perPage, page * perPage),
|
|
172
|
+
meta: {
|
|
173
|
+
page,
|
|
174
|
+
perPage,
|
|
175
|
+
total: matching.length,
|
|
176
|
+
lastPage: Math.max(1, Math.ceil(matching.length / perPage))
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
if (method === "POST") {
|
|
181
|
+
const values = await readBody();
|
|
182
|
+
const invalid = validationProblem(resource, values, "create");
|
|
183
|
+
if (invalid !== null) return invalid;
|
|
184
|
+
const item = makeItem(resource, {
|
|
185
|
+
...values,
|
|
186
|
+
id: void 0
|
|
187
|
+
});
|
|
188
|
+
list.push(item);
|
|
189
|
+
return json(201, { data: item }, etag(item));
|
|
190
|
+
}
|
|
191
|
+
return problem(405, "Method not allowed.");
|
|
192
|
+
}
|
|
193
|
+
if (segments[2] === "actions" && segments[3] !== void 0 && segments.length === 4) {
|
|
194
|
+
if (method !== "POST") return problem(405, "Method not allowed.");
|
|
195
|
+
if (segments[3] !== "delete") return problem(404, "Action not found.");
|
|
196
|
+
const body = await readBody();
|
|
197
|
+
const selected = selectItems(list, body.selection);
|
|
198
|
+
store.set(resource.id, list.filter((item) => !selected.includes(item)));
|
|
199
|
+
return json(200, {
|
|
200
|
+
message: `${selected.length} deleted.`,
|
|
201
|
+
summary: {
|
|
202
|
+
succeeded: selected.length,
|
|
203
|
+
failed: 0,
|
|
204
|
+
skipped: 0
|
|
205
|
+
},
|
|
206
|
+
refresh: "list"
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
if (segments.length !== 3 || segments[2] === void 0) return problem(404, "Not found.");
|
|
210
|
+
const id = segments[2];
|
|
211
|
+
const index = list.findIndex((item) => item.id === id);
|
|
212
|
+
const current = list[index];
|
|
213
|
+
if (current === void 0) return problem(404, "Item not found.");
|
|
214
|
+
if (method === "GET") return json(200, { data: current }, etag(current));
|
|
215
|
+
if (method === "PATCH") {
|
|
216
|
+
const ifMatch = request.headers.get("If-Match");
|
|
217
|
+
if (ifMatch !== null && current.version !== null && parseEtag(ifMatch) !== current.version) return problem(412, "The item was changed by someone else.", {
|
|
218
|
+
type: "urn:panneau:problem:version-conflict",
|
|
219
|
+
current
|
|
220
|
+
});
|
|
221
|
+
const values = await readBody();
|
|
222
|
+
const invalid = validationProblem(resource, values, "update");
|
|
223
|
+
if (invalid !== null) return invalid;
|
|
224
|
+
const updates = Object.fromEntries(Object.entries(values).filter(([key]) => !RESERVED_KEYS.has(key)));
|
|
225
|
+
const updated = {
|
|
226
|
+
...current,
|
|
227
|
+
...updates,
|
|
228
|
+
version: current.version === null ? null : String(Number(current.version) + 1)
|
|
229
|
+
};
|
|
230
|
+
list[index] = updated;
|
|
231
|
+
return json(200, { data: updated }, etag(updated));
|
|
232
|
+
}
|
|
233
|
+
if (method === "DELETE") {
|
|
234
|
+
list.splice(index, 1);
|
|
235
|
+
return json(204, null);
|
|
236
|
+
}
|
|
237
|
+
return problem(405, "Method not allowed.");
|
|
238
|
+
};
|
|
239
|
+
return {
|
|
240
|
+
fetch: (input, init) => {
|
|
241
|
+
if (input instanceof Request) return handle(input);
|
|
242
|
+
return handle(new Request(new URL(input, "http://panneau.test"), init));
|
|
243
|
+
},
|
|
244
|
+
items: (resource) => [...store.get(resource) ?? []]
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
//#endregion
|
|
248
|
+
export { createExampleDefinition, createMockBackend };
|
|
249
|
+
|
|
250
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/mock/index.ts"],"sourcesContent":["import { type ListQuery, fromSearchParams } from '../query';\nimport { type RuleFailure, checkValues } from '../rules';\nimport type { DefinitionInput, ResourceInput } from '../schemas';\nimport type { ActionResult, Item, Problem, Selection } from '../schemas/api';\nimport { isRecord } from '../values';\nimport { PROBLEM_CONTENT_TYPE } from '../version';\n\ntype Values = Record<string, unknown>;\n\nexport interface MockBackendOptions {\n /** The definition served by `GET /definition`. Its `api.baseUrl` is the prefix of every route. */\n definition: DefinitionInput;\n /** Initial values of the items, per resource id. Items without an `id` get one. */\n items?: Record<string, Values[]>;\n}\n\nexport interface MockBackend {\n /** A `fetch`-compatible function that answers protocol requests. */\n fetch: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;\n /** The current items of a resource. */\n items: (resource: string) => Item[];\n}\n\nconst RESERVED_KEYS = new Set(['id', 'version', 'abilities']);\n\nconst DEFAULT_MESSAGES: Record<string, (failure: RuleFailure) => string> = {\n required: () => 'This field is required.',\n min: (failure) => `This value must be at least ${String(failure.value)}.`,\n max: (failure) => `This value must be at most ${String(failure.value)}.`,\n pattern: () => 'This value has an invalid format.',\n email: () => 'This value must be an email address.',\n url: () => 'This value must be a URL.',\n in: () => 'This value is not one of the choices.',\n accept: () => 'This file type is not accepted.',\n maxSize: () => 'This file is too large.',\n before: () => 'This date is too late.',\n after: () => 'This date is too early.',\n};\n\nfunction message(failure: RuleFailure): string {\n return failure.message ?? DEFAULT_MESSAGES[failure.rule]?.(failure) ?? 'This value is invalid.';\n}\n\nfunction json(status: number, body: unknown, headers: Record<string, string> = {}): Response {\n return new Response(status === 204 ? null : JSON.stringify(body), {\n status,\n headers: status === 204 ? headers : { 'Content-Type': 'application/json', ...headers },\n });\n}\n\nfunction problem(status: number, title: string, extra: Partial<Problem> = {}): Response {\n const body: Problem = { type: 'about:blank', title, status, ...extra };\n return new Response(JSON.stringify(body), {\n status,\n headers: { 'Content-Type': PROBLEM_CONTENT_TYPE },\n });\n}\n\nfunction etag(item: Item): Record<string, string> {\n return item.version === null ? {} : { ETag: `\"${item.version}\"` };\n}\n\nfunction parseEtag(value: string): string {\n return value.replace(/^W\\//, '').replace(/^\"(.*)\"$/, '$1');\n}\n\nfunction searchableText(value: unknown): string {\n if (typeof value === 'string') {\n return value;\n }\n if (isRecord(value)) {\n return Object.values(value).map(searchableText).join(' ');\n }\n return '';\n}\n\nfunction compare(a: unknown, b: unknown): number {\n const left = isRecord(a) ? searchableText(a) : a;\n const right = isRecord(b) ? searchableText(b) : b;\n if (left === right) {\n return 0;\n }\n if (left === undefined || left === null) {\n return 1;\n }\n if (right === undefined || right === null) {\n return -1;\n }\n return String(left).localeCompare(String(right), undefined, { numeric: true });\n}\n\nfunction applyQuery(items: Item[], query: Pick<ListQuery, 'filter' | 'q' | 'sort'>): Item[] {\n let result = items;\n Object.entries(query.filter ?? {}).forEach(([key, expected]) => {\n const accepted = (Array.isArray(expected) ? expected : [expected]).map(String);\n result = result.filter((item) => {\n const value = item[key];\n const values = Array.isArray(value) ? value : [value];\n return values.some((candidate) => accepted.includes(String(candidate)));\n });\n });\n if (query.q !== undefined && query.q !== '') {\n const terms = query.q.toLowerCase();\n result = result.filter((item) =>\n Object.entries(item)\n .filter(([key]) => !RESERVED_KEYS.has(key))\n .some(([, value]) => searchableText(value).toLowerCase().includes(terms)),\n );\n }\n const sort = query.sort ?? [];\n if (sort.length > 0) {\n result = [...result].sort((a, b) => {\n for (const part of sort) {\n const descending = part.startsWith('-');\n const key = descending ? part.slice(1) : part;\n const order = compare(a[key], b[key]);\n if (order !== 0) {\n return descending ? -order : order;\n }\n }\n return 0;\n });\n }\n return result;\n}\n\n/**\n * An in-memory backend that implements the protocol's resource routes: definition, list, show,\n * create, update (with `If-Match`), delete and the built-in `delete` action. Values are checked\n * with the portable rules. Used to test the conformance kit, and to develop frontends without a\n * real backend.\n */\nexport function createMockBackend({ definition, items = {} }: MockBackendOptions): MockBackend {\n const store = new Map<string, Item[]>();\n let nextId = 1;\n\n const abilitiesFor = (resource: ResourceInput) =>\n Object.fromEntries([\n ['update', true],\n ['delete', true],\n ...(resource.actions ?? []).map((action) => [action.id, true]),\n ]);\n\n const makeItem = (resource: ResourceInput, values: Values): Item => {\n const id =\n typeof values.id === 'string' || typeof values.id === 'number'\n ? String(values.id)\n : String(nextId++);\n const rest = Object.fromEntries(\n Object.entries(values).filter(([key]) => !RESERVED_KEYS.has(key)),\n );\n return { ...rest, id, version: '1', abilities: abilitiesFor(resource) };\n };\n\n definition.resources.forEach((resource) => {\n store.set(\n resource.id,\n (items[resource.id] ?? []).map((values) => makeItem(resource, values)),\n );\n });\n\n const basePath = new URL(definition.api.baseUrl, 'http://panneau.test').pathname.replace(\n /\\/$/,\n '',\n );\n\n const validationProblem = (\n resource: ResourceInput,\n values: Values,\n mode: 'create' | 'update',\n ) => {\n const failures = checkValues(resource.fields, values, {\n mode,\n partial: mode === 'update',\n locales: definition.locales,\n });\n const paths = Object.keys(failures);\n if (paths.length === 0) {\n return null;\n }\n return problem(422, 'The given data was invalid.', {\n type: 'urn:panneau:problem:validation',\n errors: Object.fromEntries(\n paths.map((path) => [path, (failures[path] ?? []).map(message)]),\n ),\n });\n };\n\n const selectItems = (list: Item[], selection: Selection | undefined): Item[] => {\n if (selection === undefined) {\n return [];\n }\n if ('ids' in selection) {\n const ids = selection.ids as string[];\n return list.filter((item) => ids.includes(item.id));\n }\n const except = selection.except ?? [];\n const { filter, q } = selection.query;\n const query: Pick<ListQuery, 'filter' | 'q'> = {};\n if (filter !== undefined) {\n query.filter = filter as NonNullable<ListQuery['filter']>;\n }\n if (q !== undefined) {\n query.q = q;\n }\n return applyQuery(list, query).filter((item) => !except.includes(item.id));\n };\n\n const handle = async (request: Request): Promise<Response> => {\n const url = new URL(request.url, 'http://panneau.test');\n if (!url.pathname.startsWith(basePath)) {\n return problem(404, 'Not found.');\n }\n const segments = url.pathname\n .slice(basePath.length)\n .split('/')\n .filter((segment) => segment !== '')\n .map(decodeURIComponent);\n const method = request.method.toUpperCase();\n const readBody = async (): Promise<Values> => {\n const body: unknown = await request.json().catch(() => null);\n return isRecord(body) ? body : {};\n };\n\n if (segments.length === 1 && segments[0] === 'definition' && method === 'GET') {\n return json(200, definition);\n }\n if (segments[0] !== 'resources' || segments[1] === undefined) {\n return problem(404, 'Not found.');\n }\n\n const resource = definition.resources.find((candidate) => candidate.id === segments[1]);\n const list = resource !== undefined ? store.get(resource.id) : undefined;\n if (resource === undefined || list === undefined) {\n return problem(404, 'Resource not found.');\n }\n\n if (segments.length === 2) {\n if (method === 'GET') {\n const query = fromSearchParams(url.searchParams);\n const perPage = query.perPage ?? resource.list?.perPage ?? 20;\n const page = query.page ?? 1;\n const sort = query.sort ?? resource.list?.defaultSort?.split(',');\n const matching = applyQuery(list, {\n ...query,\n ...(sort !== undefined ? { sort } : {}),\n });\n return json(200, {\n data: matching.slice((page - 1) * perPage, page * perPage),\n meta: {\n page,\n perPage,\n total: matching.length,\n lastPage: Math.max(1, Math.ceil(matching.length / perPage)),\n },\n });\n }\n if (method === 'POST') {\n const values = await readBody();\n const invalid = validationProblem(resource, values, 'create');\n if (invalid !== null) {\n return invalid;\n }\n const item = makeItem(resource, { ...values, id: undefined });\n list.push(item);\n return json(201, { data: item }, etag(item));\n }\n return problem(405, 'Method not allowed.');\n }\n\n if (segments[2] === 'actions' && segments[3] !== undefined && segments.length === 4) {\n if (method !== 'POST') {\n return problem(405, 'Method not allowed.');\n }\n if (segments[3] !== 'delete') {\n return problem(404, 'Action not found.');\n }\n const body = await readBody();\n const selected = selectItems(list, body.selection as Selection | undefined);\n store.set(\n resource.id,\n list.filter((item) => !selected.includes(item)),\n );\n const result: ActionResult = {\n message: `${selected.length} deleted.`,\n summary: { succeeded: selected.length, failed: 0, skipped: 0 },\n refresh: 'list',\n };\n return json(200, result);\n }\n\n if (segments.length !== 3 || segments[2] === undefined) {\n return problem(404, 'Not found.');\n }\n const id = segments[2];\n const index = list.findIndex((item) => item.id === id);\n const current = list[index];\n if (current === undefined) {\n return problem(404, 'Item not found.');\n }\n\n if (method === 'GET') {\n return json(200, { data: current }, etag(current));\n }\n if (method === 'PATCH') {\n const ifMatch = request.headers.get('If-Match');\n if (\n ifMatch !== null &&\n current.version !== null &&\n parseEtag(ifMatch) !== current.version\n ) {\n return problem(412, 'The item was changed by someone else.', {\n type: 'urn:panneau:problem:version-conflict',\n current,\n });\n }\n const values = await readBody();\n const invalid = validationProblem(resource, values, 'update');\n if (invalid !== null) {\n return invalid;\n }\n const updates = Object.fromEntries(\n Object.entries(values).filter(([key]) => !RESERVED_KEYS.has(key)),\n );\n const updated: Item = {\n ...current,\n ...updates,\n version: current.version === null ? null : String(Number(current.version) + 1),\n };\n list[index] = updated;\n return json(200, { data: updated }, etag(updated));\n }\n if (method === 'DELETE') {\n list.splice(index, 1);\n return json(204, null);\n }\n return problem(405, 'Method not allowed.');\n };\n\n return {\n fetch: (input, init) => {\n if (input instanceof Request) {\n return handle(input);\n }\n // Relative URLs are accepted, as a browser would resolve them.\n return handle(new Request(new URL(input, 'http://panneau.test'), init));\n },\n items: (resource) => [...(store.get(resource) ?? [])],\n };\n}\n\nexport { createExampleDefinition } from './example';\n"],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type JsonObject = Record<string, unknown>;
|
|
2
|
+
/** Component schemas, generated from the protocol's Zod schemas. */
|
|
3
|
+
export declare function createComponentSchemas(): Record<string, JsonObject>;
|
|
4
|
+
/** The JSON Schema of the definition (`GET /definition`). */
|
|
5
|
+
export declare function createDefinitionJsonSchema(): JsonObject;
|
|
6
|
+
/** The JSON Schema of the boot config, which a server writes in the page of the app. */
|
|
7
|
+
export declare function createBootConfigJsonSchema(): JsonObject;
|
|
8
|
+
/** The OpenAPI 3.1 document of the Panneau HTTP API. */
|
|
9
|
+
export declare function createOpenApiDocument(): JsonObject;
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=openapi.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"AAOA,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AA0E1C,oEAAoE;AACpE,wBAAgB,sBAAsB,IAAI,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAWnE;AAED,6DAA6D;AAC7D,wBAAgB,0BAA0B,IAAI,UAAU,CAQvD;AAED,wFAAwF;AACxF,wBAAgB,0BAA0B,IAAI,UAAU,CAQvD;AAED,wDAAwD;AACxD,wBAAgB,qBAAqB,IAAI,UAAU,CAialD"}
|