@aaroncadillac/crudify-mongo 1.8.2 → 3.1.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/CHANGELOG.md +133 -0
- package/README.md +295 -198
- package/crudify.js +314 -183
- package/package.json +47 -8
- package/sanitizer.js +264 -0
- package/.github/workflows/publish.yml +0 -34
- package/.github/workflows/test.yml +0 -21
- package/example/db-connection.js +0 -15
- package/example/models/posts.js +0 -30
- package/example/models/users.js +0 -37
- package/example/routes.js +0 -15
- package/example/server.js +0 -20
- package/jest-mongodb-config.js +0 -11
- package/jest.config.js +0 -7
- package/test/crudify.test.js +0 -73
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to [Semantic Versioning](https://semver.org/).
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
### ⚠ BREAKING
|
|
8
|
+
|
|
9
|
+
- **`additionalRoutes` was removed** (deprecated since the 2.x line, both the function and the array form). Passing it now throws a `TypeError` at registration pointing to the replacement. Migrate to `routes(api, ctx)` or `actions`. Because this is a breaking change on top of the published 3.0.0, the next release that includes it must be a **major** version.
|
|
10
|
+
|
|
11
|
+
## [3.1.0] - 2026-10-22
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`createScope: 'strict' | 'after-hooks'`** (default `'strict'`, exactly the 3.0.0 behaviour). With `'after-hooks'`, `beforeCreate` can derive a scope key the client does not send (for example an order's `restaurantId` from its `tableIds`): the key is no longer demanded before the hook, the **final document** is validated afterwards (`400 <field> is required` when missing, `403` when outside the scope's `{ $in }` list) and the equality keys are forced last. Bulk requests are validated entirely before anything is inserted. Invalid values throw at registration. See "Deriving a scope key" in the README.
|
|
16
|
+
|
|
17
|
+
### Security
|
|
18
|
+
|
|
19
|
+
- The document that reaches Mongo is always checked against the scope on create, in both modes. In `'after-hooks'` a hook can derive a key but cannot move a document out of its scope, nor overwrite a forced key.
|
|
20
|
+
|
|
21
|
+
### Notes
|
|
22
|
+
|
|
23
|
+
- In `'after-hooks'` mode `beforeCreate` receives the document without the forced equality keys (they are applied after the hook); in `'strict'` mode it still receives them, as in 3.0.0.
|
|
24
|
+
- Publishing: the release workflow bumps the version from the commit message, so use `[minor]` for this release.
|
|
25
|
+
|
|
26
|
+
## [3.0.0] - 2026-10-21
|
|
27
|
+
|
|
28
|
+
Crudify is now **secure by default** and extensible. The plugin that used to hand raw client input to Mongo (and made every real application re-implement it) now sanitizes queries and bodies, bounds results, hides internals from error responses and offers `scope` / `authorize` / hooks / custom routes as first-class seams. It stays small: two files, no runtime dependencies. It does **not** authenticate or authorize by itself; it gives you the points where your tool of choice (`@fastify/auth`, Casbin, JWT...) plugs in.
|
|
29
|
+
|
|
30
|
+
### ⚠ BREAKING
|
|
31
|
+
|
|
32
|
+
- **Paginated by default.** `GET /` returns 50 documents per page and never more than 100. `pagination: false` (which used to be the default) and any client `populate`, `select`, `lean`, `collation`, `offset`... are ignored. Raise the cap per resource with `query.maxLimit`.
|
|
33
|
+
- **`filters` are sanitized.** Only a short allowlist of operators, bounded depth and size. `"/text/"` is now an *escaped literal* (case-insensitive "contains"), no longer a client-controlled regular expression. `$where`, `$expr`, `$regex`, `$text`, `$function`... are rejected with `400`.
|
|
34
|
+
- **Updates (`PUT`) are filtered.** `_id`, `__v`, `createdAt` / `updatedAt` (and custom timestamp names) and your `fields.readonly` / scope keys are dropped silently; only plain fields, dotted paths, `$set` and `$unset` are accepted (the others answer `400`); an update with nothing valid left is a `400`. Creates drop the same fields.
|
|
35
|
+
- **Error responses changed.** One shape for every error: `{ status, error, message, code? }` (the 404 body is unchanged). Validation and cast errors are `400` everywhere (a cast error or a non-`required` validation error on `PUT` was a `500`), duplicate keys are a generic `409`, and every unexpected error is `500 Internal error`: `err.message` is no longer sent to clients.
|
|
36
|
+
- **`POST` with an array** goes through `Model.create` (so `pre/post('save')` hooks run), validates every item before inserting any, and is limited to `bulk.max` (default 100, `413` above it). The old `insertMany` behaviour is available with `bulk: { insertMany: true }`.
|
|
37
|
+
- **`opts.schema`** is now per verb: `{ list, get, create, update, delete }`, and it is merged over the generated schemas instead of replacing them.
|
|
38
|
+
- **`exports` map.** The package exposes `.` and `./sanitizer`; deep imports such as `.../crudify.js` no longer resolve. `engines.node` is `>=20`; `fastify` (^4 or ^5), `mongoose` (^8) and `mongoose-paginate-v2` are peer dependencies.
|
|
39
|
+
- **The plugin is async** (no `next` callback). It still registers with `fastify.register(Crudify, options)`.
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- **Sanitizer module**, on by default, usable on its own via `@aaroncadillac/crudify-mongo/sanitizer`: `createSanitizer`, `sanitizeFilter`, `sanitizeOptions`, `sanitizeUpdate`, `sanitizeCreate`, `parseJson`, `escapeRegex`, `CrudifyError`. Configure it with `query` and `fields`, replace pieces with `sanitizer: { filter, options, update, create }`, or disable it with `sanitizer: false`.
|
|
44
|
+
- **`scope(req, action)`**: a server-side filter ANDed with every read and write (no IDOR), forced into new documents, protected from updates. Fails closed.
|
|
45
|
+
- **`authorize(req, action)`**: runs before any I/O; deny by throwing or returning `false`.
|
|
46
|
+
- **Hooks**: `beforeCreate`, `afterCreate`, `beforeUpdate`, `afterUpdate`, `beforeDelete`, `afterDelete`, `afterList`. **`serialize`** and **`mapError`**.
|
|
47
|
+
- **`methods`** to register only some verbs; **`fields`** (`writable`, `readonly`, `hidden`); **`bulk`**; **`lean`**; **`select`**; **`routeOptions`**.
|
|
48
|
+
- **Custom endpoints**: `routes(api, ctx)`, `actions`, and per-verb `handlers`, all sharing scope, authorize and error handling through a context `ctx` (`where`, `load`, `list`, `sanitize`, `serialize`...). `req.crudify = { action, filter, doc }`.
|
|
49
|
+
- **`CrudifyError`** and the pure helpers `toErrorResponse`, `composeFilter`, `isDuplicateKeyError`, `duplicateKeyFields`.
|
|
50
|
+
- **`strict: true`** fails at startup if `scope` is missing or the sanitizer is off.
|
|
51
|
+
- Fastify **4 and 5** support, validated by the test suite against both.
|
|
52
|
+
- `example/` with `@fastify/auth`, two tenants, roles and a custom action; `bench/run.js`; this changelog.
|
|
53
|
+
|
|
54
|
+
### Changed
|
|
55
|
+
|
|
56
|
+
- Registration validates its options and throws immediately on mistakes (unknown verbs, missing `paginate`, non-function hooks, forbidden operators...).
|
|
57
|
+
- Everything computable is computed once at registration (see the benchmark in the README).
|
|
58
|
+
- Route options are built explicitly, so `Model`, `url` and `JSONSchema` no longer leak into Fastify's route options.
|
|
59
|
+
|
|
60
|
+
### Deprecated
|
|
61
|
+
|
|
62
|
+
- `additionalRoutes` (both the function form and the array form the old README described). Use `routes` or `actions`. It still works and logs a warning; it will be removed in 4.0.
|
|
63
|
+
|
|
64
|
+
### Fixed
|
|
65
|
+
|
|
66
|
+
- With `JSONSchema`, the list response schema is now the paginate shape (it described a bare array) and `POST` accepts the bulk array (it only allowed an object); generated schemas no longer drop the ones you supply, and `required` is not enforced on responses.
|
|
67
|
+
- `POST` with an array no longer bypasses `pre/post('save')` hooks.
|
|
68
|
+
- `jest-mongodb-config` had a typo (`moduile.exports`) in an ESM package, so it was silently ignored; it is now `jest-mongodb-config.cjs`, wired through `MONGO_MEMORY_SERVER_FILE`, and uses a Mongo version with arm64 binaries.
|
|
69
|
+
- `example/` and the README now run as written (wrong export / import names, a model registered under the wrong name, a connection string that interpolated `undefined`).
|
|
70
|
+
|
|
71
|
+
### Security
|
|
72
|
+
|
|
73
|
+
- Raw client filters, options and bodies no longer reach Mongo: no operator injection, no ReDoS through client regex, no unbounded dumps, no `populate` / `select` of arbitrary relations, no mass assignment of `_id`, timestamps or ownership fields.
|
|
74
|
+
- Documents can be pinned to a scope on every verb, which closes cross-tenant reads, edits and deletes by id.
|
|
75
|
+
- Duplicate-key and internal errors no longer reveal collections, indexes, values or stack details (an email-enumeration oracle in 1.x).
|
|
76
|
+
- Fields marked `select: false` cannot be used as a search or sort oracle.
|
|
77
|
+
|
|
78
|
+
### Test coverage map
|
|
79
|
+
|
|
80
|
+
Each audited problem has a test; the suites live in `test/unit` (model stubbed, no database) and `test/integration` (real in-memory `mongod`), and every `crudify` suite runs against Fastify 4 **and** 5.
|
|
81
|
+
|
|
82
|
+
| ID | Problem in 1.8.2 | Where it is tested |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| C1 | raw operators in `filters` | `unit/sanitizer` · `unit/crudify` "reads" · `integration` "bounded queries" |
|
|
85
|
+
| C2 | client regex, ReDoS | `unit/sanitizer` (escaped literal, timing) · `integration` "a catastrophic regex" |
|
|
86
|
+
| C3 | raw `options`, `pagination:false` default | `unit/sanitizer` sanitizeOptions · `unit/crudify` "caps the limit" · `integration` "defaults to 50" |
|
|
87
|
+
| C4 | no tenant: `findById` / update / delete by `_id` only | `unit/crudify` "scope on every verb" · `integration` "tenant isolation" |
|
|
88
|
+
| C5 | mass assignment in `PUT` | `unit/sanitizer` sanitizeUpdate · `integration` "ignores _id, timestamps..." |
|
|
89
|
+
| C6 | bulk `insertMany` skips save hooks | `unit/crudify` "bulk create" · `integration` "bulk create runs the save hooks" |
|
|
90
|
+
| C7 | every error is `500 err.message` (leaks E11000) | `unit/errors` · `integration` "real duplicate key" |
|
|
91
|
+
| C8 | inconsistent validation errors | `unit/errors` · `unit/crudify` "same body shape" · `integration` "validation and cast errors" |
|
|
92
|
+
| C9 | no hooks / authorization points | `unit/crudify` "hooks, authorize and serialize" |
|
|
93
|
+
| C10 | cannot disable verbs | `unit/crudify` and `integration` "methods" |
|
|
94
|
+
| C11 | `additionalRoutes` README / code mismatch | `unit/crudify` and `integration` "additionalRoutes" · `integration/example` |
|
|
95
|
+
| C12 | options clobbered / leaked into routes | `unit/crudify` "does not leak plugin options" · "per-verb Fastify schema" |
|
|
96
|
+
| C13 | no validation without an undeclared dependency | `unit/crudify` and `integration` "JSONSchema" (hand-written schemas are enough) |
|
|
97
|
+
| C14 | unbounded bulk | `unit/crudify` and `integration` "bounds the bulk size" |
|
|
98
|
+
| C15 | plugin composition (async, prefix, parent hooks) | `unit/crudify` "registers under a prefix", "plugin metadata" |
|
|
99
|
+
| C16 | happy-path-only tests, Fastify 5 untested | the whole suite; Fastify 4 + 5 matrix (`test/helpers/fastifies.js`) |
|
|
100
|
+
| C17 | broken README / example | `integration/example` (the example app runs, with `@fastify/auth`) |
|
|
101
|
+
| C18 | wrong list response schema | `unit/crudify` and `integration` "JSONSchema" |
|
|
102
|
+
| C19 | schema rejected the bulk array | `unit/crudify` and `integration` "JSONSchema" |
|
|
103
|
+
| C20 | `select:false` fields usable as a search oracle | `unit/sanitizer` hidden · `integration` "select:false" · `integration/example` |
|
|
104
|
+
| C21 | broken jest-mongodb config | `jest.config.js` + `jest-mongodb-config.cjs` (the integration project runs on it) |
|
|
105
|
+
| C22 | example model name / connection string | `integration/example` |
|
|
106
|
+
| C23 | publishes without running tests | **pending**: `.github/workflows` was not changed (see TODO.md) |
|
|
107
|
+
| C24 | Mongoose mutates shared option objects | `unit/crudify` "does not reuse option objects" |
|
|
108
|
+
| C25 | `fastify-plugin` not needed (a plain plugin inherits parent hooks / decorators) | `unit/crudify` "inherits hooks of the parent", "prefix" |
|
|
109
|
+
|
|
110
|
+
Needs of the first real consumer (a multi-tenant restaurant API) are covered too: tenant isolation, authorize by verb, operator allowlist, limits, writable / readonly fields, tenant forcing on create, `methods`, consistent errors, bulk, custom routes and actions with shared scope (`integration` "custom routes and actions share scope + authorize").
|
|
111
|
+
|
|
112
|
+
## Migration guide: 1.x → 3.0
|
|
113
|
+
|
|
114
|
+
| In 1.x | In 3.0 | If you need the old behaviour |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| `GET /` returned everything | 50 per page, max 100 | `query: { defaultLimit, maxLimit }`; there is no "all rows" mode, page through the results |
|
|
117
|
+
| Any Mongo operator in `filters` | Allowlisted operators | `query: { operators: [...DEFAULT_OPERATORS, '$size', '$all', '$nor'] }`, or `sanitizer: false` (not recommended) |
|
|
118
|
+
| `"/text/"` = client regex | escaped literal ("contains") | `query: { search: 'prefix' }` for an indexable prefix; nothing to restore the raw regex |
|
|
119
|
+
| `options: { populate, select, lean }` honoured | ignored | `query: { populate: ['owner'] }` allowlists populate paths; `lean` and `select` are plugin options |
|
|
120
|
+
| `PUT` body went straight to Mongo | filtered, flat `$set` / `$unset` only | `fields: { writable: [...] }` to be stricter; a `beforeUpdate` hook to reshape |
|
|
121
|
+
| Errors `{ error: err.message }` with status 500 | `{ status, error, message, code? }` | `mapError: (err) => ({ status, body })` |
|
|
122
|
+
| `POST` validation errors were `400` with a raw message; on `PUT` only `required` errors were `400`, the rest `500` | `400` with one shape everywhere | |
|
|
123
|
+
| `additionalRoutes` | deprecated | `routes(api, ctx)` or `actions` |
|
|
124
|
+
| `opts.schema` replaced the generated schema | per verb, merged | `schema: { update: { body } }` |
|
|
125
|
+
| No scope | `scope` / `authorize` (warns when `scope` is missing) | `scope: false` to say "no restriction"; `strict: true` to forbid it |
|
|
126
|
+
| Bulk with `insertMany` | `Model.create` (hooks run) | `bulk: { insertMany: true }` |
|
|
127
|
+
| `import '@aaroncadillac/crudify-mongo/crudify.js'` | not exported | `import { Crudify } from '@aaroncadillac/crudify-mongo'` |
|
|
128
|
+
|
|
129
|
+
Clients keep working if they only used the documented routes, the `docs` / `totalDocs` pagination fields and `{ status, error, message }` for 404s. Review any client that relied on the `error` field containing a human message (it is now the HTTP status text; the message is in `message`) or on a list returning more than 100 rows.
|
|
130
|
+
|
|
131
|
+
## [1.8.2]
|
|
132
|
+
|
|
133
|
+
Last 1.x release. See the git history.
|