@aaroncadillac/crudify-mongo 1.8.2 → 3.0.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 CHANGED
@@ -2,282 +2,334 @@
2
2
 
3
3
  [![License: MPL-2.0](https://img.shields.io/badge/License-MPL--2.0-blue.svg)](https://opensource.org/licenses/MPL-2.0)
4
4
  ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/aaroncadillac/crudify-mongo/test.yml)
5
+ ![Node](https://img.shields.io/node/v/%40aaroncadillac%2Fcrudify-mongo)
6
+ ![Recommended: Node 22 LTS](https://img.shields.io/badge/recommended-Node%2022%20LTS-339933?logo=node.js&logoColor=white)
5
7
  ![NPM Version](https://img.shields.io/npm/v/%40aaroncadillac%2Fcrudify-mongo)
6
8
 
9
+ A CRUD generator for [Fastify](https://fastify.dev) and [Mongoose](https://mongoosejs.com) that is **safe to expose by default**: client queries are sanitized, results are bounded, errors never leak internals, and every read and write can be pinned to a server-side scope. It is small (two files, no runtime dependencies) and extensible through hooks and custom routes.
7
10
 
8
- An automatic CRUD generator for [fastify](https://fastify.dev) and MongoDB, using [mongoose](https://mongoosejs.com),a nd adding basic features like pagination, filtering, and sorting.
11
+ > Upgrading from 1.x? Read the [migration guide](CHANGELOG.md#migration-guide-1x--20).
9
12
 
10
- ## Table of contents
13
+ - [Install](#install)
14
+ - [Quick start](#quick-start)
15
+ - [Secure by default](#secure-by-default)
16
+ - [Authentication and authorization](#authentication-and-authorization)
17
+ - [Query language](#query-language)
18
+ - [Options](#options)
19
+ - [Errors](#errors)
20
+ - [Scope and authorize](#scope-and-authorize)
21
+ - [Hooks and request order](#hooks-and-request-order)
22
+ - [Custom routes, actions and overrides](#custom-routes-actions-and-overrides)
23
+ - [Recipes](#recipes)
24
+ - [The sanitizer on its own](#the-sanitizer-on-its-own)
25
+ - [JSON Schema and Swagger](#json-schema-and-swagger)
26
+ - [Performance](#performance)
11
27
 
12
- - [Features](#features)
13
- - [Automatic CRUD generation](#automatic-crud-generation)
14
- - [Custom routes](#custom-routes)
15
- - [Getting started](#getting-started)
16
- - [Installation](#installation)
17
- - [Usage](#usage)
18
- - [Aknowledgements](#aknowledgements)
19
- - [License](#license)
28
+ ## Install
20
29
 
21
- ## Pre-requisites
30
+ ```bash
31
+ yarn add @aaroncadillac/crudify-mongo
32
+ ```
22
33
 
23
- - [Fastify](https://fastify.dev) v4 or higher
24
- - [Mongoose](https://mongoosejs.com) v8 or higher
25
- - [mongoose-paginate-v2](https://www.npmjs.com/package/mongoose-paginate-v2) v1.8 or higher
26
-
27
- ## Features
34
+ Requires Node >= 20 (Node 22, the current LTS, is recommended), Fastify 4 or 5, Mongoose 8 and, for the list route, [mongoose-paginate-v2](https://www.npmjs.com/package/mongoose-paginate-v2) >= 1.8. They are peer dependencies: Crudify imports none of them.
28
35
 
29
- ### Automatic CRUD generation
36
+ ## Quick start
30
37
 
31
- You can automatically enjoy the following CRUD operations:
38
+ ```js
39
+ import Fastify from 'fastify'
40
+ import mongoose from 'mongoose'
41
+ import paginate from 'mongoose-paginate-v2'
42
+ import { Crudify } from '@aaroncadillac/crudify-mongo'
32
43
 
33
- | Route | Description | Additional built-in benefits |How to use|
34
- |-------|-------------|----|---|
35
- | `GET /` | Get all documents | `Pagination` `Filtering` `Sorting` |query parameters: `filters` and `options` for more details see [mongoose-paginate-v2 documentation](https://github.com/aravindnc/mongoose-paginate-v2#readme) |
36
- | `GET /:id` | Get a document by id | `Not-Found validation`|Activated by default|
37
- | `POST /` | Create a document | `Schema validation`|Activated by default|
38
- | `PUT /:id` | Update a document by id | `Not-Found validation` `Schema validation`|Activated by default|
39
- | `DELETE /:id` | Delete a document by id | `Not-Found validation` |Activated by default|
44
+ const userSchema = new mongoose.Schema({
45
+ name: { type: String, required: true },
46
+ email: { type: String, required: true }
47
+ }, { timestamps: true })
48
+ userSchema.plugin(paginate) // required by the list route
40
49
 
41
- ### Custom routes
50
+ const User = mongoose.model('Users', userSchema)
42
51
 
43
- You can add custom routes to the CRUD, to increase the functionality of your API, you only need to pass the route configuration to the plugin.
52
+ await mongoose.connect(process.env.MONGODB_URL)
44
53
 
45
- ```js
46
- const routes = ( fastify, opts, done ) => {
47
- fastify.register(Crudify, {
48
- url: '/users',
49
- Model: UserModel,
50
- additionalRoutes: [
51
- {
52
- method: 'GET',
53
- url: '/:id/light-details',
54
- handler: LightDetailsUserHandler
55
- },
56
- ...
57
- ]
58
- })
54
+ const app = Fastify({ logger: true })
55
+ app.register(Crudify, { url: '/users', Model: User, scope: false }) // see "Scope and authorize"
56
+ await app.listen({ port: 3000 })
59
57
  ```
60
58
 
61
- ### Swagger auto-documentation (WIP)
59
+ | Route | Does | Built in |
60
+ |---|---|---|
61
+ | `GET /users` | paginated list | sanitized `filters` / `options`, limit cap |
62
+ | `GET /users/:id` | one document | 404 when missing or out of scope |
63
+ | `POST /users` | create one document, or an array (bulk) | schema validation, save hooks run |
64
+ | `PUT /users/:id` | update fields | only your writable fields, validators run |
65
+ | `DELETE /users/:id` | delete | 404 when missing or out of scope |
62
66
 
63
- > If you need to integrate Swagger auto-documentation, is necessary to export and process mongoose model to define schemas into `openapi.json`, this module is under development.
67
+ The URLs and response shapes are the same as in 1.x. A runnable example with authentication, two tenants and a custom action lives in [`example/`](example/).
64
68
 
65
- You can add Swagger auto-documentation to your API, using the [`@fastify/swagger`](https://github.com/fastify/fastify-swagger) and [`@fastify/swagger-ui`](https://github.com/fastify/fastify-swagger-ui) plugins.
69
+ ## Secure by default
66
70
 
67
- Once you have those plugins installed, you can use the following code to add Swagger auto-documentation to your API:
71
+ Without any configuration:
68
72
 
69
- ```js
70
- // server.js
73
+ - `filters` accept a short allowlist of operators with bounded depth and size. `$where`, `$expr`, `$function`, `$regex`, `$text` and friends are refused.
74
+ - `"/text/"` searches are **escaped literals**, never a client-supplied regex, so they cannot be used for ReDoS.
75
+ - `options` only honour `page`, `limit` and `sort`. `limit` defaults to 50 and is capped at 100; `populate`, `select`, `lean` and `pagination: false` from the client are ignored.
76
+ - Writes drop `_id`, `__v` and the timestamps, accept no update operators besides `$set` / `$unset`, and refuse operators nested inside values.
77
+ - Fields with `select: false` in your schema cannot be filtered or sorted on (it would be a search oracle).
78
+ - Bulk creation goes through `Model.create`, so `pre('save')` / `post('save')` hooks always run, and an invalid item inserts nothing.
79
+ - Errors are mapped to a fixed shape. Duplicate keys become a generic `409`; unexpected errors become `500 Internal error` (the detail goes to your logs, never to the client).
80
+ - Invalid ids answer `404` without touching the database.
71
81
 
72
- import 'dotenv/config';
73
- import Fastify from 'fastify'
74
- import { dbConnection } from './db-connection.js';
75
- import { routes } from './routes.js';
76
- import * as Swagger from '@fastify/swagger'
77
- import * as SwaggerUI from '@fastify/swagger-ui'
82
+ What it does **not** do on its own is decide *who* may see *which* documents. That is what `authorize` and `scope` are for.
78
83
 
79
- const fastify = Fastify({
80
- logger: true
81
- })
84
+ ## Authentication and authorization
82
85
 
83
- fastify.register(await dbConnection)
84
- process.env.NODE_ENV == 'development' && fastify.register(Swagger) && await fastify.register(SwaggerUI, {
85
- routePrefix: '/docs',
86
- uiConfig: {
87
- docExpansion: 'list',
88
- deepLinking: true
89
- },
90
- uiHooks: {
91
- onRequest: function (request, reply, next) { next() },
92
- preHandler: function (request, reply, next) { next() }
93
- },
94
- staticCSP: true,
95
- transformStaticCSP: (header) => header,
96
- transformSpecification: (swaggerObject, request, reply) => { return swaggerObject },
97
- transformSpecificationClone: true
98
- })
86
+ Crudify does **not** authenticate users and does not include any permission system. It stays out of that on purpose and gives you two seams so any tool can plug in: [`@fastify/auth`](https://github.com/fastify/fastify-auth), JWT plugins, sessions, Casbin, your own checks.
99
87
 
100
- fastify.register(routes)
88
+ 1. Authenticate with a Fastify hook or `routeOptions.preHandler` (this is where `req.user` gets set).
89
+ 2. `authorize(req, action)` answers *may this user do this action?* and throws (or returns `false`) to deny.
90
+ 3. `scope(req, action)` answers *which documents is this user limited to?* and returns a Mongo filter.
101
91
 
102
- fastify.listen({ port: process.env.PORT || 3000, host: process.env.HOST || 'localhost' }, function (err, address) {
103
- if (err) {
104
- fastify.log.error(err)
105
- process.exit(1)
106
- }
107
- })
108
- ```
92
+ [`example/app.js`](example/app.js) is a complete, tested setup with `@fastify/auth`, two accounts and roles. A Casbin recipe is under [Recipes](#recipes).
109
93
 
110
- As you can see in the code above, you only need to import the `@fastify/swagger` and `@fastify/swagger-ui` plugins, and then use the `fastify.register` method to register the plugins., you even can customize your swagger documentation path and enable documentation only in development environment (recommended).
94
+ ## Query language
111
95
 
112
- ### Body and Response schema auto generation
96
+ Query params are JSON strings:
113
97
 
114
- To help you to use Swagger `crudify-mongo 1.5.0+` supports `body` and `response` schema auto generation, this feature is enable by default and you only need to install [`mongoose-schema-jsonschema`](https://github.com/DScheglov/mongoose-schema-jsonschema) to use it, this dependency helps you to use your mongoose model schema as a json schema compatible with fastify specification based on [AJV schema validation](https://ajv.js.org).
98
+ ```
99
+ GET /users?filters={"age":{"$gte":18},"name":"/ana/"}&options={"page":2,"limit":20,"sort":"-createdAt name"}
100
+ ```
115
101
 
116
- Here an example
102
+ | Piece | Allowed |
103
+ |---|---|
104
+ | Comparison | `$eq $ne $gt $gte $lt $lte` with plain values |
105
+ | Lists | `$in $nin` (and `$all`, opt-in) with arrays of plain values |
106
+ | Other | `$exists` (boolean); `$size` (opt-in) |
107
+ | Logical | `$or $and` (and `$nor`, opt-in), non-empty arrays |
108
+ | Text | `"/text/"` as a value: case-insensitive *contains* on an escaped literal (`query.search: 'prefix'` makes it `^text`, which can use an index) |
109
+ | Sort | a string (`"-createdAt name"`) or an object (`{"name": 1}`), field names validated |
110
+ | Never | `$where $expr $function $accumulator $regex $text $jsonSchema $elemMatch` |
111
+
112
+ The default limits (all configurable through `query`) are depth 4, 100 keys, 1000 array items, 20 branches per `$or`/`$and`, 64 characters of search text and 5 sort fields. Anything else is a `400` with `code: "invalid_query"`.
113
+
114
+ ## Options
115
+
116
+ `fastify.register(Crudify, options)`:
117
+
118
+ | Option | Default | Meaning |
119
+ |---|---|---|
120
+ | `url` | required | Base path, e.g. `/users`. May contain params: `/orders/:orderId/lines` |
121
+ | `Model` | required | Mongoose model (with `paginate` if `list` is enabled) |
122
+ | `methods` | all five | Subset of `list get create update delete` |
123
+ | `scope` | none (warns) | `(req, action) => filter`, or `false` for "no restriction" |
124
+ | `authorize` | none | `(req, action) => void`; throw or return `false` to deny |
125
+ | `hooks` | `{}` | `beforeCreate afterCreate beforeUpdate afterUpdate beforeDelete afterDelete afterList` |
126
+ | `serialize` | none | `(doc, req) => object`, applied to every document sent |
127
+ | `mapError` | built in | `(err, req) => ({ status, body }) \| undefined`; `false` to use the app's own error handler |
128
+ | `query` | see below | Sanitizer settings for `filters` / `options` |
129
+ | `fields` | none | `writable` (allowlist), `readonly` (denylist), `hidden` (not filterable) |
130
+ | `bulk` | `{ max: 100, insertMany: false }` | Bulk create limits; `false` disables arrays |
131
+ | `lean` | `false` | Return plain objects (faster; skips getters, virtuals and `toJSON` transforms) |
132
+ | `select` | none | Fixed projection for reads and updates |
133
+ | `sanitizer` | `true` | `false` disables it (1.x behaviour, logs a warning) or pass `{ filter, options, update, create }` to replace pieces |
134
+ | `JSONSchema` | none | JSON Schema of the model: validates bodies and serializes responses |
135
+ | `schema` | none | Fastify schema per verb: `{ list, get, create, update, delete }` |
136
+ | `routeOptions` | `{}` | Fastify route options for every route (`preHandler`, `config`, `bodyLimit`...) |
137
+ | `handlers` | none | Replace one built-in verb: `{ create: (req, reply, ctx) => ... }` |
138
+ | `routes` | none | `(api, ctx) => void`: add your own routes |
139
+ | `actions` | none | Non-CRUD operations on one document, see below |
140
+ | `strict` | `false` | Throw at startup if `scope` is missing or the sanitizer is disabled |
141
+
142
+ `query`: `operators`, `maxDepth`, `maxKeys`, `maxArray`, `maxBranches`, `search` (`'contains' | 'prefix' | false`), `maxSearchLength`, `filterable` (allowlist of root fields), `defaultLimit` (50), `maxLimit` (100), `maxSort`, `populate` (paths a client may populate).
143
+
144
+ `createdAt`, `updatedAt` (including custom names from `timestamps`), `_id`, `__v` and the root keys of your scope are always read-only for clients. Registering the plugin with invalid options throws immediately.
145
+
146
+ ## Errors
147
+
148
+ Every error uses one body, the same `{ status, error, message }` shape the 404 always had, plus an optional `code`:
149
+
150
+ | Case | Status | Notes |
151
+ |---|---|---|
152
+ | Not found, out of scope, invalid id | 404 | `ID: <id> not found` |
153
+ | Invalid query, body or operators | 400 | `code: "invalid_query"` / `"invalid_body"` |
154
+ | Mongoose validation | 400 | `Fields: [a, b] are required and cannot be empty`, or `Invalid fields: [...]` with `details.fields`; custom messages stay private |
155
+ | Cast error | 400 | `Invalid value for "<path>"` |
156
+ | `authorize` / `scope` deny | 403 | `Forbidden` (or your `CrudifyError`) |
157
+ | Bulk over `bulk.max` | 413 | |
158
+ | Duplicate key (E11000) | 409 | `code: "duplicate_key"`; **never** the collection, index or value |
159
+ | Version conflict | 409 | `code: "version_conflict"` |
160
+ | Anything else | 500 | `Internal error`; the real error is logged |
161
+
162
+ To show a message to the client from your own code, throw a `CrudifyError`:
117
163
 
118
164
  ```js
119
- // models/user.js
120
-
121
- import mongoose from 'mongoose';
122
- import paginate from 'mongoose-paginate-v2'; // Required for pagination
123
- import extendMongoose from 'mongoose-schema-jsonschema'; // Required for body and response schema auto generation
124
-
125
- extendMongoose(mongoose);
126
-
127
- const Schema = mongoose.Schema;
165
+ import { CrudifyError } from '@aaroncadillac/crudify-mongo'
166
+ throw new CrudifyError(409, 'This order is already paid', { code: 'order_paid' })
167
+ ```
128
168
 
129
- const userSchema = new Schema({
130
- name: {
131
- type: String,
132
- required: true,
133
- },
134
- age: {
135
- type: Number,
136
- required: true,
137
- },
138
- email: {
139
- type: String,
140
- required: true,
141
- },
142
- }, {
143
- timestamps: true,
144
- });
169
+ `toErrorResponse(err)` is exported if you want the same mapping in your own `setErrorHandler`.
145
170
 
146
- userSchema.plugin(paginate);
171
+ ## Scope and authorize
147
172
 
148
- const UserSchema = userSchema.jsonSchema();
149
- const UserModel = mongoose.model('Users', userSchema);
173
+ `scope` is a filter ANDed with every read and write, so a document outside it is simply "not found":
150
174
 
151
- export {
152
- UserModel,
153
- UserSchema
154
- }
175
+ ```js
176
+ app.register(Crudify, {
177
+ url: '/orders',
178
+ Model: Order,
179
+ scope: (req) => ({ accountId: req.user.accountId })
180
+ })
155
181
  ```
156
182
 
157
- Then you need to add on the `JSONSchema` property of your fastify routes
183
+ - **list / get / update / delete:** the scope is combined with the client filter and the id. If a key collides (the client also filters on `accountId`), both conditions are kept with `$and`, so the client can never override the scope.
184
+ - **create:** root keys with an equality value are *forced* into the new document (the client value is ignored). With `{ $in: [...] }` the client must send a value from the list (`400` if missing, `403` if not allowed).
185
+ - **update:** root keys of the scope can't be rewritten, so a document can never be moved out of its scope. If your scope uses `$or`, list the involved fields in `fields.readonly`.
186
+ - `scope` must return a plain object. Anything else is a `403` (fail closed). Return `{}` to say "no restriction" explicitly, or pass `scope: false`.
187
+ - `authorize` runs first, before any query. Use it for role checks that don't depend on the document; use `scope` for anything about *which* documents (it is atomic: no read-then-check race).
188
+
189
+ ## Hooks and request order
158
190
 
159
191
  ```js
160
- // routes.js
161
- import { Crudify } from '@aaroncadillac/crudify-mongo';
162
- import { UserModel, UserSchema } from './models/user.js';
163
-
164
- const routes = ( fastify, opts, done ) => {
165
- fastify.register(Crudify, {
166
- url: '/users',
167
- Model: UserModel
168
- JSONSchema: UserSchema
169
- })
170
- }
192
+ app.register(Crudify, {
193
+ url: '/orders',
194
+ Model: Order,
195
+ hooks: {
196
+ beforeCreate: async (data, req) => ({ ...data, createdBy: req.user.id }), // return the (new) data
197
+ afterCreate: async (docOrDocs, req) => {},
198
+ beforeUpdate: async (changes, req) => changes,
199
+ afterUpdate: async (doc, req) => {},
200
+ beforeDelete: async (doc, req) => { if (doc.paid) throw new CrudifyError(409, 'Paid orders cannot be deleted') },
201
+ afterDelete: async (doc, req) => {},
202
+ afterList: async (result, req) => result // the paginate result; return it or a new one
203
+ }
204
+ })
171
205
  ```
172
206
 
173
- ## Getting started
174
-
175
- ### Installation
207
+ Order for every request: Fastify hooks and schema validation, `routeOptions.preHandler`, cheap structural checks (id format, body shape, bulk size), `authorize`, `scope`, sanitizing, `before*`, the Mongo operation, `after*`, `serialize`. Any hook can stop the request by throwing a `CrudifyError`. Declaring `beforeDelete` costs one extra query (it needs the document); without it, delete is a single `findOneAndDelete`. An error thrown from an `after*` hook happens after the write: use `onResponse` for non-critical side effects.
176
208
 
177
- ```bash
178
- yarn add @aaroncadillac/crudify-mongo
179
- ```
209
+ `req.crudify` is `{ action, filter, doc }` for the current request, readable from your own Fastify hooks.
180
210
 
181
- ### Usage
211
+ ## Custom routes, actions and overrides
182
212
 
183
- First, you need to create a mongoose model, and use the `mongoose-paginate-v2` plugin to enable pagination.
213
+ Everything below shares the same `authorize`, `scope` and error handling as the built-in verbs, and receives a context `ctx`:
184
214
 
185
215
  ```js
186
- // models/user.js
216
+ app.register(Crudify, {
217
+ url: '/orders',
218
+ Model: Order,
219
+ scope: (req) => ({ accountId: req.user.accountId }),
220
+ authorize: (req, action) => { if (action === 'close' && req.user.role !== 'admin') throw new CrudifyError(403, 'Admins only') },
221
+
222
+ // 1. an action on one document: POST /orders/:id/close (the document arrives loaded, in scope)
223
+ actions: {
224
+ close: {
225
+ handler: async (req, reply, ctx) => {
226
+ const order = req.crudify.doc
227
+ order.status = 'closed'
228
+ await order.save()
229
+ return ctx.serialize(order, req)
230
+ }
231
+ }
232
+ },
187
233
 
188
- import mongoose from 'mongoose';
189
- import paginate from 'mongoose-paginate-v2'; // Required for pagination
234
+ // 2. free-form routes
235
+ routes: (api) => {
236
+ api.get('/stats', { action: 'list' }, async (req, reply, ctx) => ({
237
+ open: await ctx.Model.countDocuments(await ctx.where(req, 'list', { status: 'open' }))
238
+ }))
239
+ api.get('/:id/light', { load: true }, async (req) => ({ id: req.crudify.doc._id }))
240
+ },
190
241
 
191
- const Schema = mongoose.Schema;
242
+ // 3. replace one built-in verb, keeping authorize + scope
243
+ handlers: {
244
+ list: async (req, reply, ctx) => ({ total: (await ctx.list(req)).totalDocs })
245
+ }
246
+ })
247
+ ```
192
248
 
193
- const userSchema = new Schema({
194
- name: {
195
- type: String,
196
- required: true,
197
- },
198
- age: {
199
- type: Number,
200
- required: true,
201
- },
202
- email: {
203
- type: String,
204
- required: true,
205
- },
206
- }, {
207
- timestamps: true,
208
- });
249
+ `actions.<name>` options: `handler`, `method` (`POST`), `path` (`/:id/<name>`), `load` (`true`), `action` (the name given to `authorize`/`scope`, default the key), plus any Fastify route option such as `schema`.
209
250
 
210
- userSchema.plugin(paginate);
251
+ `api.get|post|put|patch|delete(path, [options], handler)`; options are Fastify route options plus `action` (what `authorize`/`scope` see; defaults to `get` for GET, `delete` for DELETE, `update` otherwise) and `load` (load the document into `req.crudify.doc`).
211
252
 
212
- const UserModel = mongoose.model('Users', userSchema);
253
+ `ctx` has `Model`, `url`, `scope(req, action)`, `where(req, action, base)` (a filter with the scope applied), `load(req, action, id)` (a real Mongoose document, or 404), `list(req)` (sanitized + scoped paginate), `sanitize.{filter, options, update, create, query}`, `serialize`, `notFound` and `checkId`.
213
254
 
214
- export {
215
- UserModel
216
- }
217
- ```
255
+ > `additionalRoutes` (a function, or an array of `{ method, url, handler }`) still works but is deprecated and will be removed in 3.0. Use `routes` or `actions`.
218
256
 
219
- Then, I suggest to separate DB connection into a module, and connect to the database, this will be an async fastify plugin.
257
+ ## Recipes
258
+
259
+ **A tenant resource in five lines.** Multi-tenancy is your domain, so keep the factory in your app:
220
260
 
221
261
  ```js
222
- // db-connection.js
262
+ const owned = (url, Model, extra = {}) => ({ url, Model, scope: (req) => ({ accountId: req.user.accountId }), ...extra })
263
+ app.register(Crudify, owned('/tables', Table))
264
+ app.register(Crudify, owned('/menus', Menu, { methods: ['list', 'get'] }))
265
+ ```
223
266
 
224
- import mongoose from 'mongoose';
267
+ **Sub-resources.** The parent id is just part of the URL and the scope:
225
268
 
226
- const dbConnection = async ( fastify ) => {
227
- try {
228
- await mongoose.connect(`${process.env.MONGODB_URL}/${process.env.MONGO_DATABASE}${process.env.MONGO_OPTIONS && `?${process.env.MONGO_OPTIONS}`}`);
229
- fastify.log.info('Connected to database');
230
- }
231
- catch (error) {
232
- fastify.log.error('Error connecting to database');
269
+ ```js
270
+ app.register(Crudify, {
271
+ url: '/orders/:orderId/lines',
272
+ Model: Line,
273
+ scope: async (req) => {
274
+ if (!(await Order.exists({ _id: req.params.orderId, accountId: req.user.accountId }))) throw new CrudifyError(404, 'Not found')
275
+ return { orderId: req.params.orderId } // forced on create, ANDed on the rest
233
276
  }
234
- }
277
+ })
278
+ ```
235
279
 
236
- export {
237
- dbConnection
238
- }
280
+ **Casbin** (or any policy engine), with no extra dependency in Crudify:
281
+
282
+ ```js
283
+ const ACTION = { list: 'read', get: 'read', create: 'create', update: 'update', delete: 'delete' }
284
+
285
+ app.register(Crudify, {
286
+ url: '/orders',
287
+ Model: Order,
288
+ authorize: async (req, action) => {
289
+ const allowed = await enforcer.enforce(req.user.id, req.user.accountId, 'orders', ACTION[action] ?? action)
290
+ if (!allowed) throw new CrudifyError(403, 'Forbidden')
291
+ },
292
+ scope: (req) => ({ accountId: req.user.accountId }) // the policy domain doubles as the data scope
293
+ })
239
294
  ```
240
295
 
241
- Then, you can create the server file, and register the CRUD plugin.
296
+ **Hide fields from responses:**
242
297
 
243
298
  ```js
244
- // server.js
299
+ serialize: (doc) => { const { passwordHash, ...rest } = doc.toObject({ flattenObjectIds: true }); return rest }
300
+ ```
245
301
 
246
- import fastify from 'fastify';
247
- import Crudify from 'crudify-mongo';
248
- import { dbConnection } from './db-connection';
249
- import { UserModel } from './models/user';
302
+ ## The sanitizer on its own
250
303
 
251
- const fastify = Fastify({
252
- logger: true
253
- })
304
+ The sanitizer is a separate module you can use in your own handlers:
305
+
306
+ ```js
307
+ import { sanitizeFilter, sanitizeOptions, sanitizeUpdate, createSanitizer, parseJson } from '@aaroncadillac/crudify-mongo/sanitizer'
254
308
 
255
- const routes = ( fastify, opts, done ) => {
256
- fastify.register(Crudify, {
257
- url: '/users',
258
- Model: UserModel
259
- })
260
- }
309
+ const filter = sanitizeFilter(parseJson(req.query.filters, 'filters'), { maxDepth: 2 })
310
+ const { filter: f, options, update, create } = createSanitizer({ maxLimit: 25, writable: ['name'] }) // compile once, reuse
311
+ ```
261
312
 
262
- fastify.register(await dbConnection)
263
- fastify.register(routes)
313
+ Pass `sanitizer: { filter: myFn }` to Crudify to replace a piece, or `sanitizer: false` to turn it off (not recommended).
264
314
 
315
+ ## JSON Schema and Swagger
265
316
 
266
- fastify.listen({ port: process.env.PORT || 3000, host: process.env.HOST || 'localhost' }, function (err, address) {
267
- if (err) {
268
- fastify.log.error(err)
269
- process.exit(1)
270
- }
271
- })
317
+ Give Crudify the JSON Schema of your model (for example from [`mongoose-schema-jsonschema`](https://github.com/DScheglov/mongoose-schema-jsonschema)) and it validates bodies and serializes responses with it:
318
+
319
+ ```js
320
+ app.register(Crudify, { url: '/users', Model: User, JSONSchema: userJsonSchema, scope: false })
272
321
  ```
273
322
 
274
- Simple as that, you have a CRUD for the `User` model and could be accessed through the `/users` route.
323
+ The list response is typed as the paginate result, `POST` accepts one object or an array, read-only fields are removed from the body schema (so a key forced by your scope is never "required" from the client), and `x-ref` keys are stripped. Responses drop `required`, so `select` and `serialize` can hide fields freely. Use `schema: { list: {...}, ... }` to override any verb, and `@fastify/swagger` picks everything up.
324
+
325
+ ## Performance
275
326
 
276
- ## Aknowledgements
327
+ Everything that can be computed is computed once at registration: allowlist `Set`s, the sanitizer, route schemas, error handler and the context. Per request there is a single `JSON.parse` for each query param, one pass of the sanitizer that builds its output directly, one query (paginate does two) and no `try/catch` per route. `node bench/run.js` (no extra dependencies) measures it; on an Apple-silicon laptop with a stubbed model, a typical filter sanitizes in about 0.8 µs and Crudify adds roughly 0.5-0.8 µs per request over a bare Fastify route. `lean: true` skips Mongoose document hydration on reads when you don't need getters, virtuals or `toJSON` transforms.
277
328
 
278
- This plugin is inspired by [fastify-mongoose-crud](https://github.com/paranoiasystem/fastify-autocrud)
329
+ ## Acknowledgements
330
+
331
+ Inspired by [fastify-mongoose-crud](https://github.com/paranoiasystem/fastify-autocrud).
279
332
 
280
333
  ## License
281
334
 
282
335
  [Mozilla Public License 2.0](/LICENSE)
283
-