@web-ts-toolkit/access-router 0.2.0 → 0.4.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 +7 -358
- package/advanced.d.mts +941 -0
- package/advanced.d.ts +941 -0
- package/advanced.js +1025 -0
- package/advanced.mjs +941 -0
- package/index.d.mts +179 -1435
- package/index.d.ts +179 -1435
- package/index.js +3232 -1965
- package/index.mjs +3167 -1866
- package/package.json +9 -3
- package/parsers-CsyGHYQR.d.mts +1418 -0
- package/parsers-CsyGHYQR.d.ts +1418 -0
package/README.md
CHANGED
|
@@ -8,370 +8,19 @@ Access-policy Express routers and in-memory data services for Mongoose-backed AP
|
|
|
8
8
|
pnpm add @web-ts-toolkit/access-router express mongoose
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Documentation
|
|
12
|
+
|
|
13
|
+
- Full package documentation lives in `website/docs/packages/access-router/`.
|
|
14
|
+
- Browse the docs site from `website` to read the full guide alongside the rest of the workspace packages.
|
|
15
|
+
|
|
16
|
+
## Minimal Example
|
|
12
17
|
|
|
13
18
|
```ts
|
|
14
19
|
import acl from '@web-ts-toolkit/access-router';
|
|
15
20
|
|
|
16
|
-
acl.set('globalPermissions', (req) => {
|
|
17
|
-
return req.headers.user === 'admin' ? ['isAdmin'] : [];
|
|
18
|
-
});
|
|
19
|
-
|
|
20
21
|
const router = acl.createDataRouter('fruit', {
|
|
21
22
|
basePath: '/fruit',
|
|
23
|
+
idField: 'id',
|
|
22
24
|
data: [{ id: 'apple', name: 'Apple', public: true }],
|
|
23
|
-
identifier: 'id',
|
|
24
|
-
routeGuard: {
|
|
25
|
-
list: true,
|
|
26
|
-
read: true,
|
|
27
|
-
},
|
|
28
|
-
permissionSchema: {
|
|
29
|
-
id: true,
|
|
30
|
-
name: 'isAdmin',
|
|
31
|
-
public: true,
|
|
32
|
-
},
|
|
33
|
-
});
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## List Responses
|
|
37
|
-
|
|
38
|
-
List endpoints now return a stable envelope:
|
|
39
|
-
|
|
40
|
-
```json
|
|
41
|
-
{
|
|
42
|
-
"data": [],
|
|
43
|
-
"meta": {
|
|
44
|
-
"returnedCount": 0,
|
|
45
|
-
"skip": 0,
|
|
46
|
-
"limit": 25,
|
|
47
|
-
"page": 1,
|
|
48
|
-
"pageSize": 25,
|
|
49
|
-
"hasPreviousPage": false
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
When `include_count=true` is enabled, `meta` also includes total pagination information:
|
|
55
|
-
|
|
56
|
-
```json
|
|
57
|
-
{
|
|
58
|
-
"data": [],
|
|
59
|
-
"meta": {
|
|
60
|
-
"returnedCount": 0,
|
|
61
|
-
"totalCount": 100,
|
|
62
|
-
"skip": 25,
|
|
63
|
-
"limit": 25,
|
|
64
|
-
"page": 2,
|
|
65
|
-
"pageSize": 25,
|
|
66
|
-
"totalPages": 4,
|
|
67
|
-
"hasNextPage": true,
|
|
68
|
-
"hasPreviousPage": true
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
Notes:
|
|
74
|
-
|
|
75
|
-
- `returnedCount` is the number of rows in this response.
|
|
76
|
-
- `totalCount` is only included when `include_count=true`.
|
|
77
|
-
- `include_extra_headers=true` can still add the total count header, but it does not change the response body shape.
|
|
78
|
-
|
|
79
|
-
When `include_extra_headers=true` is enabled, the response can also include these headers:
|
|
80
|
-
|
|
81
|
-
- `wtt-returned-count`
|
|
82
|
-
- `wtt-page`
|
|
83
|
-
- `wtt-page-size`
|
|
84
|
-
- `wtt-has-previous-page`
|
|
85
|
-
|
|
86
|
-
And when `include_count=true` is also enabled:
|
|
87
|
-
|
|
88
|
-
- `wtt-total-count`
|
|
89
|
-
- `wtt-total-pages`
|
|
90
|
-
- `wtt-has-next-page`
|
|
91
|
-
|
|
92
|
-
## Request Validation
|
|
93
|
-
|
|
94
|
-
Public router endpoints now validate request path params, known query params, and top-level request body shapes before calling the service layer.
|
|
95
|
-
|
|
96
|
-
Examples:
|
|
97
|
-
|
|
98
|
-
- `GET /users/:id?try_list=false` validates `id` and `try_list`
|
|
99
|
-
- `GET /pets?include_count=true&limit=10` validates boolean and pagination query params
|
|
100
|
-
- `POST /users/__mutation` requires a top-level `data` field
|
|
101
|
-
- `POST /users/__query/:id` validates advanced `select`, `populate`, `include`, and `tasks` shapes
|
|
102
|
-
|
|
103
|
-
Invalid requests return `400 application/problem+json` with structured `errors` entries using `parameter` or `pointer`:
|
|
104
|
-
|
|
105
|
-
```json
|
|
106
|
-
{
|
|
107
|
-
"title": "Bad Request",
|
|
108
|
-
"detail": "Bad Request",
|
|
109
|
-
"status": 400,
|
|
110
|
-
"errors": [
|
|
111
|
-
{
|
|
112
|
-
"parameter": "include_count",
|
|
113
|
-
"detail": "Invalid option: expected one of \"true\"|\"false\""
|
|
114
|
-
}
|
|
115
|
-
]
|
|
116
|
-
}
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
## User-Defined Request Schemas
|
|
120
|
-
|
|
121
|
-
Model and data routers can add route-specific Zod validation through the `requestSchemas` option.
|
|
122
|
-
|
|
123
|
-
Use this when you want stricter application-level request validation on top of the built-in router boundary validation.
|
|
124
|
-
|
|
125
|
-
Recommended shape:
|
|
126
|
-
|
|
127
|
-
- whole-body schemas: `requestSchemas.<route>` or `requestSchemas.<route>.default`
|
|
128
|
-
- nested advanced mutation payloads: `requestSchemas.<route>.data`
|
|
129
|
-
|
|
130
|
-
Model router examples:
|
|
131
|
-
|
|
132
|
-
- `requestSchemas.create`
|
|
133
|
-
- `requestSchemas.update`
|
|
134
|
-
- `requestSchemas.upsert`
|
|
135
|
-
- `requestSchemas.count`
|
|
136
|
-
- `requestSchemas.distinct`
|
|
137
|
-
- `requestSchemas.advancedList`
|
|
138
|
-
- `requestSchemas.advancedReadFilter`
|
|
139
|
-
- `requestSchemas.advancedRead`
|
|
140
|
-
- `requestSchemas.advancedCreate.default`
|
|
141
|
-
- `requestSchemas.advancedCreate.data`
|
|
142
|
-
- `requestSchemas.advancedUpdate.default`
|
|
143
|
-
- `requestSchemas.advancedUpdate.data`
|
|
144
|
-
- `requestSchemas.advancedUpsert.default`
|
|
145
|
-
- `requestSchemas.advancedUpsert.data`
|
|
146
|
-
- `requestSchemas.subList`
|
|
147
|
-
- `requestSchemas.subRead`
|
|
148
|
-
- `requestSchemas.subCreate`
|
|
149
|
-
- `requestSchemas.subUpdate`
|
|
150
|
-
- `requestSchemas.subBulkUpdate`
|
|
151
|
-
|
|
152
|
-
Data router examples:
|
|
153
|
-
|
|
154
|
-
- `requestSchemas.advancedList`
|
|
155
|
-
- `requestSchemas.advancedReadFilter`
|
|
156
|
-
- `requestSchemas.advancedRead`
|
|
157
|
-
|
|
158
|
-
Example:
|
|
159
|
-
|
|
160
|
-
```ts
|
|
161
|
-
import { z } from 'zod';
|
|
162
|
-
import acl from '@web-ts-toolkit/access-router';
|
|
163
|
-
|
|
164
|
-
const router = acl.createRouter('User', {
|
|
165
|
-
basePath: '/users',
|
|
166
|
-
identifier: 'name',
|
|
167
|
-
requestSchemas: {
|
|
168
|
-
create: z.object({
|
|
169
|
-
name: z.string().min(3),
|
|
170
|
-
role: z.string(),
|
|
171
|
-
}),
|
|
172
|
-
advancedCreate: {
|
|
173
|
-
data: z.object({
|
|
174
|
-
name: z.string().min(3),
|
|
175
|
-
role: z.literal('user'),
|
|
176
|
-
}),
|
|
177
|
-
},
|
|
178
|
-
advancedUpdate: {
|
|
179
|
-
data: z.object({
|
|
180
|
-
role: z.enum(['manager', 'staff']),
|
|
181
|
-
}),
|
|
182
|
-
},
|
|
183
|
-
},
|
|
184
|
-
});
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
Validation order:
|
|
188
|
-
|
|
189
|
-
- built-in route/query/body-shape validation runs first
|
|
190
|
-
- user-defined `requestSchemas` validation runs second
|
|
191
|
-
- write-operation model `validate` hooks still run afterward in the service layer
|
|
192
|
-
|
|
193
|
-
## Custom Route Validation
|
|
194
|
-
|
|
195
|
-
The package also exports the same validation helpers used by the built-in public routers:
|
|
196
|
-
|
|
197
|
-
- `parsePathParam`
|
|
198
|
-
- `parseQuery`
|
|
199
|
-
- `parseBody`
|
|
200
|
-
- `requestSchemas`
|
|
201
|
-
- advanced body schemas such as `listBodySchema`, `readByIdBodySchema`, `advancedCreateBodySchema`, and `advancedUpdateBodySchema`
|
|
202
|
-
|
|
203
|
-
Example:
|
|
204
|
-
|
|
205
|
-
```ts
|
|
206
|
-
import acl, {
|
|
207
|
-
parseBody,
|
|
208
|
-
parsePathParam,
|
|
209
|
-
parseQuery,
|
|
210
|
-
requestSchemas,
|
|
211
|
-
readByIdBodySchema,
|
|
212
|
-
} from '@web-ts-toolkit/access-router';
|
|
213
|
-
|
|
214
|
-
const router = acl.createRouter('User', {
|
|
215
|
-
basePath: '/users',
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
router.router.post('/custom/:id', async (req) => {
|
|
219
|
-
const id = parsePathParam(req.params.id, 'id');
|
|
220
|
-
const { include_permissions } = parseQuery(requestSchemas.readQuery, req.query);
|
|
221
|
-
const body = parseBody(readByIdBodySchema, req.body);
|
|
222
|
-
|
|
223
|
-
return {
|
|
224
|
-
id,
|
|
225
|
-
includePermissions: include_permissions === 'true',
|
|
226
|
-
body,
|
|
227
|
-
};
|
|
228
|
-
});
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
These helpers throw the same `BadRequestError` shape as the built-in router endpoints, so custom routes can stay consistent with the package defaults.
|
|
232
|
-
|
|
233
|
-
## Hook Signatures
|
|
234
|
-
|
|
235
|
-
The most common model hooks are called with `this` bound to the current Express request.
|
|
236
|
-
|
|
237
|
-
### `baseFilter`
|
|
238
|
-
|
|
239
|
-
```ts
|
|
240
|
-
(this: express.Request, permissions: Permissions) =>
|
|
241
|
-
| Filter
|
|
242
|
-
| true
|
|
243
|
-
| null
|
|
244
|
-
| undefined
|
|
245
|
-
| Promise<Filter | true | null | undefined>
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
- Return a `Filter` to restrict access.
|
|
249
|
-
- Return `true`, `null`, or `undefined` for no extra base filter.
|
|
250
|
-
- Return `false` to deny access.
|
|
251
|
-
|
|
252
|
-
### `decorate`
|
|
253
|
-
|
|
254
|
-
```ts
|
|
255
|
-
(this: express.Request, value: unknown, permissions: Permissions, context: MiddlewareContext) =>
|
|
256
|
-
unknown | Promise<unknown>;
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
- Runs after a document has been loaded and trimmed.
|
|
260
|
-
- Can also be an array of middleware functions.
|
|
261
|
-
|
|
262
|
-
### `overrideFilter`
|
|
263
|
-
|
|
264
|
-
```ts
|
|
265
|
-
(this: express.Request, filter: Filter, permissions: Permissions) => Filter | Promise<Filter>;
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
- Runs before the base filter is applied.
|
|
269
|
-
- Use it to rewrite or augment the caller-provided filter.
|
|
270
|
-
|
|
271
|
-
### `validate`
|
|
272
|
-
|
|
273
|
-
```ts
|
|
274
|
-
(this: express.Request, allowedData: unknown, permissions: Permissions, context: MiddlewareContext) => boolean | unknown[] | Promise<boolean | unknown[]>
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
- Return `true` to allow the write.
|
|
278
|
-
- Return `false` to reject it.
|
|
279
|
-
- Return an array to provide validation errors.
|
|
280
|
-
|
|
281
|
-
### `prepare`
|
|
282
|
-
|
|
283
|
-
```ts
|
|
284
|
-
(this: express.Request, value: unknown, permissions: Permissions, context: MiddlewareContext) =>
|
|
285
|
-
unknown | Promise<unknown>;
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
- Runs before create/update data is assigned to the document.
|
|
289
|
-
- Can also be an array of middleware functions.
|
|
290
|
-
|
|
291
|
-
### `transform`
|
|
292
|
-
|
|
293
|
-
```ts
|
|
294
|
-
(this: express.Request, value: unknown, permissions: Permissions, context: MiddlewareContext) =>
|
|
295
|
-
unknown | Promise<unknown>;
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
- Runs during update flows before the document is saved.
|
|
299
|
-
- Can also be an array of middleware functions.
|
|
300
|
-
|
|
301
|
-
### `finalize`
|
|
302
|
-
|
|
303
|
-
```ts
|
|
304
|
-
(this: express.Request, value: unknown, permissions: Permissions, context: MiddlewareContext) =>
|
|
305
|
-
unknown | Promise<unknown>;
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
- Runs after create/update persistence work and before response decoration.
|
|
309
|
-
- Can also be an array of middleware functions.
|
|
310
|
-
|
|
311
|
-
### `docPermissions`
|
|
312
|
-
|
|
313
|
-
```ts
|
|
314
|
-
(this: express.Request, doc: unknown, permissions: Permissions, context: MiddlewareContext) =>
|
|
315
|
-
Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
- Returns the document-level permission object written to the configured permission field.
|
|
319
|
-
|
|
320
|
-
### Example
|
|
321
|
-
|
|
322
|
-
```ts
|
|
323
|
-
acl.createModelRouter('Post', {
|
|
324
|
-
baseFilter: {
|
|
325
|
-
read(this, permissions) {
|
|
326
|
-
if (permissions.has('isAdmin')) return true;
|
|
327
|
-
return { published: true };
|
|
328
|
-
},
|
|
329
|
-
},
|
|
330
|
-
validate: {
|
|
331
|
-
create(this, data) {
|
|
332
|
-
if (!data || typeof data !== 'object' || !('title' in data)) {
|
|
333
|
-
return ['title is required'];
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
return true;
|
|
337
|
-
},
|
|
338
|
-
},
|
|
339
|
-
prepare: {
|
|
340
|
-
create(this, data) {
|
|
341
|
-
if (typeof data === 'object' && data) {
|
|
342
|
-
return { ...data, createdAt: new Date() };
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
return data;
|
|
346
|
-
},
|
|
347
|
-
},
|
|
348
|
-
transform: {
|
|
349
|
-
update(this, doc) {
|
|
350
|
-
return doc;
|
|
351
|
-
},
|
|
352
|
-
},
|
|
353
|
-
finalize: {
|
|
354
|
-
update(this, doc) {
|
|
355
|
-
return doc;
|
|
356
|
-
},
|
|
357
|
-
},
|
|
358
|
-
decorate: {
|
|
359
|
-
read(this, doc) {
|
|
360
|
-
const record = typeof doc === 'object' && doc ? doc : {};
|
|
361
|
-
return { ...record, summary: '...' };
|
|
362
|
-
},
|
|
363
|
-
},
|
|
364
|
-
overrideFilter: {
|
|
365
|
-
read(this, filter) {
|
|
366
|
-
return filter ?? {};
|
|
367
|
-
},
|
|
368
|
-
},
|
|
369
|
-
docPermissions: {
|
|
370
|
-
read(this, doc, permissions) {
|
|
371
|
-
return {
|
|
372
|
-
canArchive: permissions.has('isAdmin'),
|
|
373
|
-
};
|
|
374
|
-
},
|
|
375
|
-
},
|
|
376
25
|
});
|
|
377
26
|
```
|