@typion/rest 0.0.0-stage → 0.1.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.
- package/LICENSE +21 -0
- package/README.md +28 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +354 -0
- package/dist/index.js.map +1 -0
- package/package.json +31 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Simone Ergotino
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @typion/rest
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The REST transport for [Typion](../../typion/docs/README.md): every collection a resource, with its access rules applied to each
|
|
4
|
+
call and the read config in the query string. Install it and list it in the handler's `transports`.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { rest } from '@typion/rest'
|
|
8
|
+
import { routeHandlers } from 'typion/next'
|
|
9
|
+
import { config } from '@/typion.config.ts'
|
|
10
|
+
|
|
11
|
+
export const { GET, POST, PATCH, PUT, DELETE, OPTIONS } = await routeHandlers(config, {
|
|
12
|
+
transports: [rest()],
|
|
13
|
+
})
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```http
|
|
17
|
+
GET /api/posts?columns=slug,title&where[status][eq]=published&sort=-createdAt&limit=10
|
|
18
|
+
GET /api/posts/:id?with=author&locale=en
|
|
19
|
+
POST /api/posts/find the whole read config as JSON (also /find-one, /page, /count)
|
|
20
|
+
POST /api/posts { "data": { … } }
|
|
21
|
+
PATCH /api/posts/:id { "data": { … } }
|
|
22
|
+
DELETE /api/posts/:id
|
|
23
|
+
POST /api/posts/:id/publish the routes the features' operations declare
|
|
24
|
+
POST /api/posts/:id/link/tags { "ids": [ … ] }, the links of a many-to-many (also /unlink)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Each route becomes a descriptor that the core checks before it runs, as the other transports' do: an unknown key,
|
|
28
|
+
column, relation or operator is `400`, and bulk writes (`/update-many`, `/delete-many`) run only with the handler's
|
|
29
|
+
`exposeBulk: true`. The routes in full are in the guide, [Pages, routes and the browser](../../typion/docs/http.md).
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { BadRequestError } from "typion";
|
|
2
|
+
//#region src/query.ts
|
|
3
|
+
function isPlainObject(value) {
|
|
4
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
5
|
+
const proto = Object.getPrototypeOf(value);
|
|
6
|
+
return proto === Object.prototype || proto === null;
|
|
7
|
+
}
|
|
8
|
+
const LIST_PARAMS = /* @__PURE__ */ new Set([
|
|
9
|
+
"q",
|
|
10
|
+
"columns",
|
|
11
|
+
"with",
|
|
12
|
+
"sort",
|
|
13
|
+
"limit",
|
|
14
|
+
"offset",
|
|
15
|
+
"page",
|
|
16
|
+
"where"
|
|
17
|
+
]);
|
|
18
|
+
const ONE_PARAMS = /* @__PURE__ */ new Set([
|
|
19
|
+
"q",
|
|
20
|
+
"columns",
|
|
21
|
+
"with"
|
|
22
|
+
]);
|
|
23
|
+
const LOGICAL = {
|
|
24
|
+
and: "AND",
|
|
25
|
+
AND: "AND",
|
|
26
|
+
or: "OR",
|
|
27
|
+
OR: "OR",
|
|
28
|
+
not: "NOT",
|
|
29
|
+
NOT: "NOT"
|
|
30
|
+
};
|
|
31
|
+
const LIST_OPERATORS = /* @__PURE__ */ new Set([
|
|
32
|
+
"in",
|
|
33
|
+
"notIn",
|
|
34
|
+
"arrayContains",
|
|
35
|
+
"arrayContained",
|
|
36
|
+
"arrayOverlaps"
|
|
37
|
+
]);
|
|
38
|
+
const FLAG_OPERATORS = /* @__PURE__ */ new Set(["isNull", "isNotNull"]);
|
|
39
|
+
function bad(message) {
|
|
40
|
+
throw new BadRequestError(message);
|
|
41
|
+
}
|
|
42
|
+
function bool(name, raw) {
|
|
43
|
+
if (raw === "true" || raw === "") return true;
|
|
44
|
+
if (raw === "false") return false;
|
|
45
|
+
return bad(`${name} must be true or false`);
|
|
46
|
+
}
|
|
47
|
+
function int(name, raw) {
|
|
48
|
+
if (!/^\d+$/.test(raw)) bad(`${name} must be a non-negative integer`);
|
|
49
|
+
return Number(raw);
|
|
50
|
+
}
|
|
51
|
+
function json(name, raw) {
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(raw);
|
|
54
|
+
} catch {
|
|
55
|
+
return bad(`${name} must be JSON`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function selection(raw, name) {
|
|
59
|
+
const out = {};
|
|
60
|
+
for (const entry of raw.split(",")) {
|
|
61
|
+
const path = entry.trim();
|
|
62
|
+
if (path === "") continue;
|
|
63
|
+
const [head, ...rest] = path.split(".");
|
|
64
|
+
if (head === void 0 || head === "") bad(`${name} has an empty entry`);
|
|
65
|
+
if (rest.length === 0) {
|
|
66
|
+
out[head] ??= true;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const nested = isPlainObject(out[head]) ? out[head] : {};
|
|
70
|
+
const inner = isPlainObject(nested.with) ? nested.with : {};
|
|
71
|
+
Object.assign(inner, selection(rest.join("."), name));
|
|
72
|
+
nested.with = inner;
|
|
73
|
+
out[head] = nested;
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
function sort(raw) {
|
|
78
|
+
const out = {};
|
|
79
|
+
for (const entry of raw.split(",")) {
|
|
80
|
+
const field = entry.trim();
|
|
81
|
+
if (field === "") continue;
|
|
82
|
+
out[field.startsWith("-") ? field.slice(1) : field] = field.startsWith("-") ? "desc" : "asc";
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
function pathOf(key) {
|
|
87
|
+
const segments = [];
|
|
88
|
+
const brackets = key.slice(5);
|
|
89
|
+
const re = /\[([^\]]*)\]/g;
|
|
90
|
+
let consumed = 0;
|
|
91
|
+
let match = re.exec(brackets);
|
|
92
|
+
while (match !== null) {
|
|
93
|
+
if (match.index !== consumed) bad(`Malformed filter parameter '${key}'`);
|
|
94
|
+
segments.push(match[1] ?? "");
|
|
95
|
+
consumed = re.lastIndex;
|
|
96
|
+
match = re.exec(brackets);
|
|
97
|
+
}
|
|
98
|
+
if (consumed !== brackets.length || segments.length === 0) bad(`Malformed filter parameter '${key}'`);
|
|
99
|
+
return segments;
|
|
100
|
+
}
|
|
101
|
+
function filterValue(operator, raw) {
|
|
102
|
+
if (operator !== void 0 && LIST_OPERATORS.has(operator)) return raw.split(",").map((item) => item.trim());
|
|
103
|
+
if (operator !== void 0 && FLAG_OPERATORS.has(operator)) return bool(operator, raw);
|
|
104
|
+
return raw;
|
|
105
|
+
}
|
|
106
|
+
function assign(where, key, raw) {
|
|
107
|
+
const segments = pathOf(key);
|
|
108
|
+
let node = where;
|
|
109
|
+
for (const [index, segment] of segments.entries()) {
|
|
110
|
+
const name = LOGICAL[segment] ?? segment;
|
|
111
|
+
const last = index === segments.length - 1;
|
|
112
|
+
if (Array.isArray(node)) {
|
|
113
|
+
if (!/^\d+$/.test(segment)) bad(`'${key}': ${name} is not an index of the list before it`);
|
|
114
|
+
const at = Number(segment);
|
|
115
|
+
if (last) {
|
|
116
|
+
node[at] = filterValue(void 0, raw);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
node[at] = normalize(node[at], segments[index + 1]);
|
|
120
|
+
node = node[at];
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (last) {
|
|
124
|
+
node[name] = filterValue(name, raw);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
node[name] = normalize(node[name], segments[index + 1]);
|
|
128
|
+
node = node[name];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function normalize(current, next) {
|
|
132
|
+
if (next !== void 0 && /^\d+$/.test(next)) return Array.isArray(current) ? current : [];
|
|
133
|
+
return isPlainObject(current) ? current : {};
|
|
134
|
+
}
|
|
135
|
+
function optionValue(key, type, raw) {
|
|
136
|
+
if (type === "boolean") return bool(key, raw);
|
|
137
|
+
if (type === "number") {
|
|
138
|
+
const value = Number(raw);
|
|
139
|
+
if (raw.trim() === "" || !Number.isFinite(value)) bad(`${key} must be a number`);
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
142
|
+
return raw;
|
|
143
|
+
}
|
|
144
|
+
function readOptions(params, config, plugins) {
|
|
145
|
+
for (const [key, type] of plugins) {
|
|
146
|
+
const raw = params.get(key);
|
|
147
|
+
if (raw !== null) config[key] = optionValue(key, type, raw);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function optionParams(url, options) {
|
|
151
|
+
const config = {};
|
|
152
|
+
for (const [key, type] of options) {
|
|
153
|
+
const raw = url.searchParams.get(key);
|
|
154
|
+
if (raw !== null) config[key] = optionValue(key, type, raw);
|
|
155
|
+
}
|
|
156
|
+
return Object.keys(config).length === 0 ? void 0 : config;
|
|
157
|
+
}
|
|
158
|
+
function selections(params, config) {
|
|
159
|
+
const columns = params.get("columns");
|
|
160
|
+
if (columns !== null) config.columns = selection(columns, "columns");
|
|
161
|
+
const withParam = params.get("with");
|
|
162
|
+
if (withParam !== null) config.with = selection(withParam, "with");
|
|
163
|
+
}
|
|
164
|
+
function base(params, allowed, what) {
|
|
165
|
+
for (const key of params.keys()) {
|
|
166
|
+
const name = key.startsWith("where[") ? "where" : key;
|
|
167
|
+
if (!allowed.has(name)) bad(`Unknown query parameter '${key}' (${what}: ${[...allowed].join(", ")})`);
|
|
168
|
+
}
|
|
169
|
+
const q = params.get("q");
|
|
170
|
+
if (q === null) return {};
|
|
171
|
+
const parsed = json("q", q);
|
|
172
|
+
if (!isPlainObject(parsed)) bad("q must be a JSON object");
|
|
173
|
+
return { ...parsed };
|
|
174
|
+
}
|
|
175
|
+
function parseReadQuery(url, maxLimit, plugins) {
|
|
176
|
+
const params = url.searchParams;
|
|
177
|
+
const config = base(params, /* @__PURE__ */ new Set([...LIST_PARAMS, ...plugins.keys()]), "a list read takes");
|
|
178
|
+
selections(params, config);
|
|
179
|
+
readOptions(params, config, plugins);
|
|
180
|
+
const sortParam = params.get("sort");
|
|
181
|
+
if (sortParam !== null) config.orderBy = sort(sortParam);
|
|
182
|
+
const limit = params.get("limit");
|
|
183
|
+
if (limit !== null) config.limit = int("limit", limit);
|
|
184
|
+
const offset = params.get("offset");
|
|
185
|
+
if (offset !== null) config.offset = int("offset", offset);
|
|
186
|
+
const page = params.get("page");
|
|
187
|
+
if (page !== null) {
|
|
188
|
+
const number = int("page", page);
|
|
189
|
+
if (number < 1) bad("page starts at 1");
|
|
190
|
+
config.offset = (number - 1) * (typeof config.limit === "number" ? config.limit : maxLimit);
|
|
191
|
+
}
|
|
192
|
+
const whereParam = params.get("where");
|
|
193
|
+
const where = isPlainObject(config.where) ? { ...config.where } : {};
|
|
194
|
+
if (whereParam !== null) {
|
|
195
|
+
const parsed = json("where", whereParam);
|
|
196
|
+
if (!isPlainObject(parsed)) bad("where must be a JSON object");
|
|
197
|
+
Object.assign(where, parsed);
|
|
198
|
+
}
|
|
199
|
+
for (const [key, value] of params.entries()) if (key.startsWith("where[")) assign(where, key, value);
|
|
200
|
+
if (Object.keys(where).length > 0) config.where = where;
|
|
201
|
+
return config;
|
|
202
|
+
}
|
|
203
|
+
function parseOneQuery(url, plugins) {
|
|
204
|
+
const params = url.searchParams;
|
|
205
|
+
const config = base(params, /* @__PURE__ */ new Set([...ONE_PARAMS, ...plugins.keys()]), "reading one row takes");
|
|
206
|
+
selections(params, config);
|
|
207
|
+
readOptions(params, config, plugins);
|
|
208
|
+
return config;
|
|
209
|
+
}
|
|
210
|
+
//#endregion
|
|
211
|
+
//#region src/index.ts
|
|
212
|
+
async function objectBody(call) {
|
|
213
|
+
const value = await call.body();
|
|
214
|
+
if (value === void 0) return {};
|
|
215
|
+
if (!isPlainObject(value)) throw new BadRequestError("The request body must be a JSON object");
|
|
216
|
+
return value;
|
|
217
|
+
}
|
|
218
|
+
function matchPath(path, segments) {
|
|
219
|
+
const pattern = path.split("/").filter(Boolean);
|
|
220
|
+
if (pattern.length !== segments.length) return null;
|
|
221
|
+
const params = {};
|
|
222
|
+
return pattern.every((part, i) => {
|
|
223
|
+
const segment = segments[i];
|
|
224
|
+
if (part.startsWith(":")) {
|
|
225
|
+
params[part.slice(1)] = segment;
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
return part === segment;
|
|
229
|
+
}) ? params : null;
|
|
230
|
+
}
|
|
231
|
+
function operationQuery(url, op) {
|
|
232
|
+
const out = {};
|
|
233
|
+
for (const [key, raw] of url.searchParams) {
|
|
234
|
+
const type = op.options[key];
|
|
235
|
+
if (type !== void 0) out[key] = optionValue(key, type, raw);
|
|
236
|
+
else if (op.args.includes(key)) out[key] = raw;
|
|
237
|
+
else throw new BadRequestError(`Unknown query parameter '${key}'`);
|
|
238
|
+
}
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
const POSTS = {
|
|
242
|
+
find: "find",
|
|
243
|
+
"find-one": "findOne",
|
|
244
|
+
page: "page",
|
|
245
|
+
count: "count",
|
|
246
|
+
"update-many": "updateMany",
|
|
247
|
+
"delete-many": "deleteMany"
|
|
248
|
+
};
|
|
249
|
+
function collectionRoute(call, entity) {
|
|
250
|
+
const { method, segments, url, schema } = call;
|
|
251
|
+
const [, id, action, relation] = segments;
|
|
252
|
+
switch (segments.length) {
|
|
253
|
+
case 1:
|
|
254
|
+
if (method === "GET") return {
|
|
255
|
+
entity,
|
|
256
|
+
op: "find",
|
|
257
|
+
input: async () => parseReadQuery(url, schema.maxLimit, schema.readOptions)
|
|
258
|
+
};
|
|
259
|
+
if (method === "POST") return {
|
|
260
|
+
entity,
|
|
261
|
+
op: "create",
|
|
262
|
+
input: call.body,
|
|
263
|
+
status: 201
|
|
264
|
+
};
|
|
265
|
+
return null;
|
|
266
|
+
case 2: {
|
|
267
|
+
const name = id;
|
|
268
|
+
if (method === "POST") {
|
|
269
|
+
const op = POSTS[name];
|
|
270
|
+
return op === void 0 ? null : {
|
|
271
|
+
entity,
|
|
272
|
+
op,
|
|
273
|
+
input: call.body
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
if (method === "GET") return {
|
|
277
|
+
entity,
|
|
278
|
+
op: "findById",
|
|
279
|
+
input: async () => ({
|
|
280
|
+
id: name,
|
|
281
|
+
config: parseOneQuery(url, schema.readOptions)
|
|
282
|
+
})
|
|
283
|
+
};
|
|
284
|
+
if (method === "PATCH") return {
|
|
285
|
+
entity,
|
|
286
|
+
op: "update",
|
|
287
|
+
input: async () => ({
|
|
288
|
+
...await objectBody(call),
|
|
289
|
+
id: name
|
|
290
|
+
})
|
|
291
|
+
};
|
|
292
|
+
if (method === "DELETE") return {
|
|
293
|
+
entity,
|
|
294
|
+
op: "delete",
|
|
295
|
+
input: async () => ({
|
|
296
|
+
id: name,
|
|
297
|
+
options: optionParams(url, schema.deleteOptions)
|
|
298
|
+
})
|
|
299
|
+
};
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
case 4:
|
|
303
|
+
if (method === "POST" && (action === "link" || action === "unlink")) return {
|
|
304
|
+
entity,
|
|
305
|
+
op: action,
|
|
306
|
+
input: async () => ({
|
|
307
|
+
...await objectBody(call),
|
|
308
|
+
id,
|
|
309
|
+
relation
|
|
310
|
+
})
|
|
311
|
+
};
|
|
312
|
+
return null;
|
|
313
|
+
default: return null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function operationRoute(call, entity) {
|
|
317
|
+
for (const op of entity.operations) {
|
|
318
|
+
if (op.route === void 0 || op.route.method !== call.method) continue;
|
|
319
|
+
const params = matchPath(op.route.path, call.segments.slice(1));
|
|
320
|
+
if (params === null) continue;
|
|
321
|
+
return {
|
|
322
|
+
entity: entity.name,
|
|
323
|
+
op: op.name,
|
|
324
|
+
input: async () => ({
|
|
325
|
+
...call.method === "GET" ? operationQuery(call.url, op) : await objectBody(call),
|
|
326
|
+
...params
|
|
327
|
+
})
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
function rest() {
|
|
333
|
+
return {
|
|
334
|
+
name: "rest",
|
|
335
|
+
async handle(call) {
|
|
336
|
+
const name = call.segments[0];
|
|
337
|
+
const entity = name === void 0 ? null : call.schema.entity(name);
|
|
338
|
+
if (entity === null) return void 0;
|
|
339
|
+
const route = collectionRoute(call, entity.name) ?? operationRoute(call, entity);
|
|
340
|
+
if (route === null) return void 0;
|
|
341
|
+
const input = await route.input();
|
|
342
|
+
const data = await call.run(call.descriptor({
|
|
343
|
+
entity: route.entity,
|
|
344
|
+
op: route.op,
|
|
345
|
+
input
|
|
346
|
+
}));
|
|
347
|
+
return call.respond(route.status ?? 200, { data });
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
//#endregion
|
|
352
|
+
export { rest };
|
|
353
|
+
|
|
354
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/query.ts","../src/index.ts"],"sourcesContent":["import { BadRequestError, type OptionType } from 'typion'\n\ntype AnyRow = Record<string, unknown>\n\nexport function isPlainObject(value: unknown): value is AnyRow {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return false\n const proto = Object.getPrototypeOf(value)\n return proto === Object.prototype || proto === null\n}\n\n// The query string of a REST read. `q` carries the whole wire config as JSON for anything this cannot say;\n// everything else is the readable form: `?columns=slug,title&where[title][ilike]=%ciao%&sort=-createdAt`.\n\nconst LIST_PARAMS = new Set(['q', 'columns', 'with', 'sort', 'limit', 'offset', 'page', 'where'])\n\nconst ONE_PARAMS = new Set(['q', 'columns', 'with'])\n\nconst LOGICAL: Record<string, string> = {\n and: 'AND',\n AND: 'AND',\n or: 'OR',\n OR: 'OR',\n not: 'NOT',\n NOT: 'NOT',\n}\n\nconst LIST_OPERATORS = new Set(['in', 'notIn', 'arrayContains', 'arrayContained', 'arrayOverlaps'])\nconst FLAG_OPERATORS = new Set(['isNull', 'isNotNull'])\n\nfunction bad(message: string): never {\n throw new BadRequestError(message)\n}\n\nfunction bool(name: string, raw: string): boolean {\n if (raw === 'true' || raw === '') return true\n if (raw === 'false') return false\n return bad(`${name} must be true or false`)\n}\n\nfunction int(name: string, raw: string): number {\n if (!/^\\d+$/.test(raw)) bad(`${name} must be a non-negative integer`)\n return Number(raw)\n}\n\nfunction json(name: string, raw: string): unknown {\n try {\n return JSON.parse(raw)\n } catch {\n return bad(`${name} must be JSON`)\n }\n}\n\n// `columns=slug,title` and `with=author,comments.author`; a relation is included whole, and a narrower\n// selection inside it is what `q` (or POST /:collection/find) is for.\nfunction selection(raw: string, name: string): AnyRow {\n const out: AnyRow = {}\n for (const entry of raw.split(',')) {\n const path = entry.trim()\n if (path === '') continue\n const [head, ...rest] = path.split('.')\n if (head === undefined || head === '') bad(`${name} has an empty entry`)\n if (rest.length === 0) {\n out[head] ??= true\n continue\n }\n const nested = isPlainObject(out[head]) ? (out[head] as AnyRow) : {}\n const inner = isPlainObject(nested.with) ? (nested.with as AnyRow) : {}\n Object.assign(inner, selection(rest.join('.'), name))\n nested.with = inner\n out[head] = nested\n }\n return out\n}\n\nfunction sort(raw: string): AnyRow {\n const out: AnyRow = {}\n for (const entry of raw.split(',')) {\n const field = entry.trim()\n if (field === '') continue\n out[field.startsWith('-') ? field.slice(1) : field] = field.startsWith('-') ? 'desc' : 'asc'\n }\n return out\n}\n\nfunction pathOf(key: string): string[] {\n const segments: string[] = []\n const brackets = key.slice('where'.length)\n const re = /\\[([^\\]]*)\\]/g\n let consumed = 0\n let match = re.exec(brackets)\n while (match !== null) {\n if (match.index !== consumed) bad(`Malformed filter parameter '${key}'`)\n segments.push(match[1] ?? '')\n consumed = re.lastIndex\n match = re.exec(brackets)\n }\n if (consumed !== brackets.length || segments.length === 0) {\n bad(`Malformed filter parameter '${key}'`)\n }\n return segments\n}\n\nfunction filterValue(operator: string | undefined, raw: string): unknown {\n if (operator !== undefined && LIST_OPERATORS.has(operator)) {\n return raw.split(',').map((item) => item.trim())\n }\n if (operator !== undefined && FLAG_OPERATORS.has(operator)) return bool(operator, raw)\n return raw\n}\n\n// `where[title][ilike]=%x%`, `where[or][0][status][eq]=published`, `where[author][role][eq]=admin`.\nfunction assign(where: AnyRow, key: string, raw: string): void {\n const segments = pathOf(key)\n let node: AnyRow | unknown[] = where\n for (const [index, segment] of segments.entries()) {\n const name = LOGICAL[segment] ?? segment\n const last = index === segments.length - 1\n if (Array.isArray(node)) {\n if (!/^\\d+$/.test(segment)) bad(`'${key}': ${name} is not an index of the list before it`)\n const at = Number(segment)\n if (last) {\n node[at] = filterValue(undefined, raw)\n return\n }\n node[at] = normalize(node[at], segments[index + 1])\n node = node[at] as AnyRow | unknown[]\n continue\n }\n if (last) {\n node[name] = filterValue(name, raw)\n return\n }\n node[name] = normalize(node[name], segments[index + 1])\n node = node[name] as AnyRow | unknown[]\n }\n}\n\nfunction normalize(current: unknown, next: string | undefined): AnyRow | unknown[] {\n const wantsList = next !== undefined && /^\\d+$/.test(next)\n if (wantsList) return Array.isArray(current) ? current : []\n return isPlainObject(current) ? (current as AnyRow) : {}\n}\n\n// The installed plugins' read options (`locale`, `fallback`, `withDeleted`, `draft`), typed so the strings\n// can be coerced.\nexport type QueryOptions = ReadonlyMap<string, OptionType>\n\nexport function optionValue(key: string, type: OptionType, raw: string): unknown {\n if (type === 'boolean') return bool(key, raw)\n if (type === 'number') {\n const value = Number(raw)\n if (raw.trim() === '' || !Number.isFinite(value)) bad(`${key} must be a number`)\n return value\n }\n return raw\n}\n\nfunction readOptions(params: URLSearchParams, config: AnyRow, plugins: QueryOptions): void {\n for (const [key, type] of plugins) {\n const raw = params.get(key)\n if (raw !== null) config[key] = optionValue(key, type, raw)\n }\n}\n\n// The plugins' options of a request whose other parameters are fixed (`DELETE …?hard=true`).\nexport function optionParams(url: URL, options: QueryOptions): AnyRow | undefined {\n const config: AnyRow = {}\n for (const [key, type] of options) {\n const raw = url.searchParams.get(key)\n if (raw !== null) config[key] = optionValue(key, type, raw)\n }\n return Object.keys(config).length === 0 ? undefined : config\n}\n\nfunction selections(params: URLSearchParams, config: AnyRow): void {\n const columns = params.get('columns')\n if (columns !== null) config.columns = selection(columns, 'columns')\n const withParam = params.get('with')\n if (withParam !== null) config.with = selection(withParam, 'with')\n}\n\nfunction base(params: URLSearchParams, allowed: Set<string>, what: string): AnyRow {\n for (const key of params.keys()) {\n const name = key.startsWith('where[') ? 'where' : key\n if (!allowed.has(name)) {\n bad(`Unknown query parameter '${key}' (${what}: ${[...allowed].join(', ')})`)\n }\n }\n const q = params.get('q')\n if (q === null) return {}\n const parsed = json('q', q)\n if (!isPlainObject(parsed)) bad('q must be a JSON object')\n return { ...parsed }\n}\n\nexport function parseReadQuery(url: URL, maxLimit: number, plugins: QueryOptions): unknown {\n const params = url.searchParams\n const config = base(params, new Set([...LIST_PARAMS, ...plugins.keys()]), 'a list read takes')\n selections(params, config)\n readOptions(params, config, plugins)\n\n const sortParam = params.get('sort')\n if (sortParam !== null) config.orderBy = sort(sortParam)\n const limit = params.get('limit')\n if (limit !== null) config.limit = int('limit', limit)\n const offset = params.get('offset')\n if (offset !== null) config.offset = int('offset', offset)\n const page = params.get('page')\n if (page !== null) {\n const number = int('page', page)\n if (number < 1) bad('page starts at 1')\n config.offset = (number - 1) * (typeof config.limit === 'number' ? config.limit : maxLimit)\n }\n\n const whereParam = params.get('where')\n const where: AnyRow = isPlainObject(config.where) ? { ...(config.where as AnyRow) } : {}\n if (whereParam !== null) {\n const parsed = json('where', whereParam)\n if (!isPlainObject(parsed)) bad('where must be a JSON object')\n Object.assign(where, parsed)\n }\n for (const [key, value] of params.entries()) {\n if (key.startsWith('where[')) assign(where, key, value)\n }\n if (Object.keys(where).length > 0) config.where = where\n return config\n}\n\nexport function parseOneQuery(url: URL, plugins: QueryOptions): unknown {\n const params = url.searchParams\n const config = base(params, new Set([...ONE_PARAMS, ...plugins.keys()]), 'reading one row takes')\n selections(params, config)\n readOptions(params, config, plugins)\n return config\n}\n","import { BadRequestError, type Op } from 'typion'\nimport type { Transport, TransportCall, TransportEntity, TransportOperation } from 'typion/http'\nimport { isPlainObject, optionParams, optionValue, parseOneQuery, parseReadQuery } from './query.js'\n\ntype AnyRow = Record<string, unknown>\n\ntype Route = {\n readonly entity: string\n readonly op: Op\n readonly input: () => Promise<unknown>\n readonly status?: number\n}\n\nasync function objectBody(call: TransportCall): Promise<AnyRow> {\n const value = await call.body()\n if (value === undefined) return {}\n if (!isPlainObject(value)) throw new BadRequestError('The request body must be a JSON object')\n return value\n}\n\nfunction matchPath(path: string, segments: readonly string[]): Record<string, string> | null {\n const pattern = path.split('/').filter(Boolean)\n if (pattern.length !== segments.length) return null\n const params: Record<string, string> = {}\n const ok = pattern.every((part, i) => {\n const segment = segments[i] as string\n if (part.startsWith(':')) {\n params[part.slice(1)] = segment\n return true\n }\n return part === segment\n })\n return ok ? params : null\n}\n\n// On a GET the query string carries what the path does not: the other arguments as they are, the options in\n// their wire types.\nfunction operationQuery(url: URL, op: TransportOperation): AnyRow {\n const out: AnyRow = {}\n for (const [key, raw] of url.searchParams) {\n const type = op.options[key]\n if (type !== undefined) out[key] = optionValue(key, type, raw)\n else if (op.args.includes(key)) out[key] = raw\n else throw new BadRequestError(`Unknown query parameter '${key}'`)\n }\n return out\n}\n\n// The bulk routes are matched like the others; the handler's `exposeBulk` decides whether they run, for every\n// transport at once.\nconst POSTS: Record<string, Op> = {\n find: 'find',\n 'find-one': 'findOne',\n page: 'page',\n count: 'count',\n 'update-many': 'updateMany',\n 'delete-many': 'deleteMany',\n}\n\n// The routes every collection has: its reads, writes, and the links of the many-to-many it owns.\nfunction collectionRoute(call: TransportCall, entity: string): Route | null {\n const { method, segments, url, schema } = call\n const [, id, action, relation] = segments\n switch (segments.length) {\n case 1:\n if (method === 'GET') {\n return {\n entity,\n op: 'find',\n input: async () => parseReadQuery(url, schema.maxLimit, schema.readOptions),\n }\n }\n if (method === 'POST') return { entity, op: 'create', input: call.body, status: 201 }\n return null\n case 2: {\n const name = id as string\n if (method === 'POST') {\n const op = POSTS[name]\n return op === undefined ? null : { entity, op, input: call.body }\n }\n if (method === 'GET') {\n return {\n entity,\n op: 'findById',\n input: async () => ({ id: name, config: parseOneQuery(url, schema.readOptions) }),\n }\n }\n if (method === 'PATCH') {\n return {\n entity,\n op: 'update',\n input: async () => ({ ...(await objectBody(call)), id: name }),\n }\n }\n if (method === 'DELETE') {\n return {\n entity,\n op: 'delete',\n input: async () => ({ id: name, options: optionParams(url, schema.deleteOptions) }),\n }\n }\n return null\n }\n case 4:\n if (method === 'POST' && (action === 'link' || action === 'unlink')) {\n return {\n entity,\n op: action,\n input: async () => ({ ...(await objectBody(call)), id, relation }),\n }\n }\n return null\n default:\n return null\n }\n}\n\n// A route an operation of a feature declares under the collection (`POST /posts/:id/publish`).\nfunction operationRoute(call: TransportCall, entity: TransportEntity): Route | null {\n for (const op of entity.operations) {\n if (op.route === undefined || op.route.method !== call.method) continue\n const params = matchPath(op.route.path, call.segments.slice(1))\n if (params === null) continue\n return {\n entity: entity.name,\n op: op.name,\n input: async () => ({\n ...(call.method === 'GET' ? operationQuery(call.url, op) : await objectBody(call)),\n ...params,\n }),\n }\n }\n return null\n}\n\n// A resource per collection, with the rules of each: `GET /posts?columns=slug,title&where[status][eq]=published`,\n// `PATCH /posts/:id`, `POST /posts/:id/publish`. Every route becomes a descriptor that the core checks before it\n// runs, as the other transports' do.\nexport function rest(): Transport {\n return {\n name: 'rest',\n async handle(call) {\n const name = call.segments[0]\n const entity = name === undefined ? null : call.schema.entity(name)\n if (entity === null) return undefined\n const route = collectionRoute(call, entity.name) ?? operationRoute(call, entity)\n if (route === null) return undefined\n const input = await route.input()\n const data = await call.run(call.descriptor({ entity: route.entity, op: route.op, input }))\n return call.respond(route.status ?? 200, { data })\n },\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typion/rest",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "REST routes for Typion: a resource per collection, with the rules of each",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/the-escape-studio/typion.git",
|
|
10
|
+
"directory": "packages/transports/rest"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/the-escape-studio/typion/tree/main/packages/transports/rest#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/the-escape-studio/typion/issues"
|
|
15
|
+
},
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"typion": "^0.1.1"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
}
|
|
6
34
|
}
|