@kollors/deep-json-server 1.0.0-rc.6 → 1.0.0-rc.8

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.
Files changed (82) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/MIGRATION.md +187 -25
  3. package/MIGRATION.ru.md +211 -0
  4. package/README.md +48 -33
  5. package/README.ru.md +50 -35
  6. package/dist/index.d.ts +1 -1
  7. package/dist/package.json +104 -0
  8. package/dist/src/auth/contract.d.ts +8 -0
  9. package/dist/src/auth/contract.js.map +1 -1
  10. package/dist/src/auth/public.d.ts +1 -1
  11. package/dist/src/auth/service.d.ts +7 -16
  12. package/dist/src/auth/service.js +40 -48
  13. package/dist/src/auth/service.js.map +1 -1
  14. package/dist/src/auth/store.d.ts +11 -5
  15. package/dist/src/auth/store.js +37 -19
  16. package/dist/src/auth/store.js.map +1 -1
  17. package/dist/src/cli/index.js +2 -2
  18. package/dist/src/cli/index.js.map +1 -1
  19. package/dist/src/core/constants.d.ts +1 -1
  20. package/dist/src/core/constants.js +2 -1
  21. package/dist/src/core/constants.js.map +1 -1
  22. package/dist/src/core/engine.d.ts +2 -1
  23. package/dist/src/core/engine.js +7 -3
  24. package/dist/src/core/engine.js.map +1 -1
  25. package/dist/src/core/model/infer.js +8 -5
  26. package/dist/src/core/model/infer.js.map +1 -1
  27. package/dist/src/core/model/load.js +1 -1
  28. package/dist/src/core/model/load.js.map +1 -1
  29. package/dist/src/core/mutations/write.d.ts +3 -3
  30. package/dist/src/core/mutations/write.js +10 -5
  31. package/dist/src/core/mutations/write.js.map +1 -1
  32. package/dist/src/core/paths.d.ts +4 -0
  33. package/dist/src/core/paths.js +7 -0
  34. package/dist/src/core/paths.js.map +1 -1
  35. package/dist/src/core/query/budget.d.ts +24 -0
  36. package/dist/src/core/query/budget.js +69 -0
  37. package/dist/src/core/query/budget.js.map +1 -0
  38. package/dist/src/core/query/execute.d.ts +2 -1
  39. package/dist/src/core/query/execute.js +5 -2
  40. package/dist/src/core/query/execute.js.map +1 -1
  41. package/dist/src/core/query/filter.d.ts +2 -1
  42. package/dist/src/core/query/filter.js +27 -9
  43. package/dist/src/core/query/filter.js.map +1 -1
  44. package/dist/src/core/records.d.ts +2 -0
  45. package/dist/src/core/records.js +15 -4
  46. package/dist/src/core/records.js.map +1 -1
  47. package/dist/src/files/contract.js +1 -0
  48. package/dist/src/files/contract.js.map +1 -1
  49. package/dist/src/files/disk-store.js +7 -5
  50. package/dist/src/files/disk-store.js.map +1 -1
  51. package/dist/src/graphql/preflight.d.ts +11 -2
  52. package/dist/src/graphql/preflight.js +48 -2
  53. package/dist/src/graphql/preflight.js.map +1 -1
  54. package/dist/src/graphql/resolvers.d.ts +3 -0
  55. package/dist/src/graphql/resolvers.js +47 -8
  56. package/dist/src/graphql/resolvers.js.map +1 -1
  57. package/dist/src/graphql/routes.d.ts +1 -1
  58. package/dist/src/graphql/routes.js +16 -0
  59. package/dist/src/graphql/routes.js.map +1 -1
  60. package/dist/src/graphql/schema.js +6 -4
  61. package/dist/src/graphql/schema.js.map +1 -1
  62. package/dist/src/openapi/document.js +8 -2
  63. package/dist/src/openapi/document.js.map +1 -1
  64. package/dist/src/rest/options.d.ts +2 -1
  65. package/dist/src/rest/options.js +5 -3
  66. package/dist/src/rest/options.js.map +1 -1
  67. package/dist/src/rest/projection.d.ts +4 -3
  68. package/dist/src/rest/projection.js +31 -12
  69. package/dist/src/rest/projection.js.map +1 -1
  70. package/dist/src/rest/routes.js +13 -7
  71. package/dist/src/rest/routes.js.map +1 -1
  72. package/dist/src/server/bootstrap.js +15 -7
  73. package/dist/src/server/bootstrap.js.map +1 -1
  74. package/dist/src/server/input-paths.d.ts +14 -1
  75. package/dist/src/server/input-paths.js +18 -2
  76. package/dist/src/server/input-paths.js.map +1 -1
  77. package/dist/src/server/storage-lock.d.ts +7 -1
  78. package/dist/src/server/storage-lock.js +103 -99
  79. package/dist/src/server/storage-lock.js.map +1 -1
  80. package/examples/database.json +2 -14
  81. package/examples/schema.json +0 -15
  82. package/package.json +3 -1
package/README.md CHANGED
@@ -14,7 +14,7 @@ A JSON mock server with REST, GraphQL, related records, file uploads and schema
14
14
  npm install @kollors/deep-json-server@rc
15
15
  ```
16
16
 
17
- To install this release candidate, use `@1.0.0-rc.6`.
17
+ To install this release candidate, use `@1.0.0-rc.8`.
18
18
 
19
19
  ## Quick start
20
20
 
@@ -177,7 +177,20 @@ Examples: [database](examples/database.json), [model schema](examples/schema.jso
177
177
  }
178
178
  ```
179
179
 
180
- Model definitions belong in `models`. The schema root accepts `models`, `api`, `timestamps` and `softDelete`. Root `api` selects database routes. When omitted, it defaults to REST and also GraphQL if the server config has a `graphql` section. A model can override the global timestamp and soft-deletion settings. A model's optional `api` array can narrow access to `['rest']`, `['graphql']`, both, or `[]` to hide it from both. A model cannot enable an API absent from root `api`. OpenAPI includes models with REST enabled. GraphQL generation needs at least one GraphQL model; REST OpenAPI generation needs at least one REST model. On a GraphQL-only server, OpenAPI can still describe configured auth and file routes. Related models must allow the same API. Model names must be valid identifiers; type and operation collisions cause errors. `and`, `or` and `not` are reserved filter names.
180
+ Model definitions belong in `models`. The schema root accepts `models`, `api`, `timestamps` and `softDelete`. Root `api` selects database routes. When omitted, its default depends on how the schema is used:
181
+
182
+ | Context | Default root `api` |
183
+ |---|---|
184
+ | Server without a `graphql` section | `['rest']` |
185
+ | Server with a `graphql` section | `['rest', 'graphql']` |
186
+ | Standalone `generateOpenapi()` | `['rest']` |
187
+ | Standalone `generateGraphql()` | `['graphql']` |
188
+
189
+ Explicit `['rest']` enables database REST routes; `['graphql']` enables GraphQL operations; `['rest', 'graphql']` enables both. OpenAPI describes REST routes when `openapi` is configured. Root `api: []` is invalid; `[]` is allowed only at the model level.
190
+
191
+ A model without its own `api` inherits the root value. A model can override it with `['rest']`, `['graphql']`, both, or `[]` to hide it from both. A model cannot enable an API absent from root `api`. OpenAPI includes models with REST enabled. GraphQL generation needs at least one GraphQL model; REST OpenAPI generation needs at least one REST model. On a GraphQL-only server, OpenAPI can still describe configured auth and file routes. Related models must allow the same API. Model names must be valid identifiers; type and operation collisions cause errors. `and`, `or` and `not` are reserved filter names.
192
+
193
+ Thus an explicit root `"api": ["rest", "graphql"]` enables both APIs for models that inherit the root setting. The array order does not matter.
181
194
 
182
195
  For example, with root `api: ['rest', 'graphql']`, a model set to `api: ['rest']` has REST routes and OpenAPI paths but no GraphQL operations; a model set to `api: ['graphql']` has GraphQL operations but no REST routes or OpenAPI paths.
183
196
 
@@ -219,7 +232,7 @@ String and numeric constraints on `string[]`/`number[]` apply to every element.
219
232
 
220
233
  Each `enum` value must match the field type and its constraints. Use `nullable: true` to allow `null` for the field; do not include `null` in `enum`. Invalid values are rejected when the schema is loaded.
221
234
 
222
- Each model requires exactly one primary key of type `string` or `number`, declared at the top level. The name is arbitrary: `id`, `username`, `code`. If `generated` is omitted, the client supplies the value on creation. Generated fields must be declared at the top level, are excluded from input types and cannot have `default`. Replacing a record preserves generated values and read-only fields, including nested objects. To protect fields inside an array, mark the entire array or its containing object as `readOnly`. Objects containing only server-managed fields are output-only.
235
+ Each model requires exactly one primary key of type `string` or `number`, declared at the top level. The name is arbitrary: `id`, `username`, `code`. If `generated` is omitted, the client supplies the value on creation. Generated fields must be declared at the top level, are excluded from input types and cannot have `default`. Replacing a record preserves generated values and read-only fields, including nested objects. Setting a nullable parent to `null` is rejected if it would remove stored read-only descendants. To protect fields inside an array, mark the entire array or its containing object as `readOnly`. Objects containing only server-managed fields are output-only.
223
236
 
224
237
  For example, a `LocalUser` with primary key `username` and `password: {"type":"string","required":true,"writeOnly":true}` has `localUser(username: ...)` and `/localUsers/{username}`. A `writeOnly` field accepts input and is excluded from responses, `scope`, filters and ordering.
225
238
 
@@ -244,7 +257,9 @@ On the `current` side, `target` defaults to the related model's primary key. `so
244
257
 
245
258
  `source` is always a path in the current model, and `target` is always a path in the model named by `type`. For an inverse relation, the paths are swapped: `Country.users` has `source: "id"` and `target: "countryId"`. Specify a path when the stored key has a custom name. If multiple relations connect the same models, disambiguate the inverse declarations, for example `Country.birthUsers = {"type":"User[]","keyOn":"related","target":"birthCountryId"}` and `Country.residenceUsers = {"type":"User[]","keyOn":"related","target":"residenceCountryId"}`. Missing, inconsistent, or ambiguous pairs fail when the schema loads.
246
259
 
247
- Relation keys remain in the database even when they are absent from `fields`. A `source` field pointing to a target primary key can be omitted: the server infers an array of keys for a list relation or a scalar key for a single relation. An inferred key is internal: REST and GraphQL cannot read, select, filter, sort or write it directly, and OpenAPI does not describe it. The relation itself remains available. Declare the key in `fields`, for example `"countryId": { "type": "string" }`, to expose it in the APIs. An ambiguous mapping also requires an explicit declaration.
260
+ Relation keys remain in the database even when they are absent from `fields`. On the `current` side, a `source` field can be omitted when the target key type is known: the server infers an array of keys for a list relation or a scalar key for a single relation. This applies to primary and non-primary target fields. An inferred key is internal: REST and GraphQL cannot read, select, filter, sort or write it directly, and OpenAPI does not describe it. The relation itself remains available. Declare the key in `fields`, for example `"countryId": { "type": "string" }`, to expose it in the APIs. An ambiguous mapping also requires an explicit declaration.
261
+
262
+ For example, `publishers: { "type": "Publisher[]", "keyOn": "current", "target": "code" }` infers `publisherCodes: { "type": "string[]" }` when `Publisher.code` is a string, even if the primary key is `Publisher.id`. Explicitly declared storage key types are preserved.
248
263
 
249
264
  Reverse example: `User.movies = {"type":"Movie[]","keyOn":"related","target":"actors.userId"}` pairs with `Movie.actors.user = {"type":"User","keyOn":"current","source":"actors.userId"}`. A movie is returned once even if several actors match. A single relation that matches multiple records causes an error.
250
265
 
@@ -266,37 +281,31 @@ Cascading deletion runs as one operation, including cyclic relations. A validati
266
281
  "countries": [
267
282
  {
268
283
  "id": "1",
269
- "isArchived": false,
270
284
  "name": "Ardenia"
271
285
  },
272
286
  {
273
287
  "id": "2",
274
- "isArchived": false,
275
288
  "name": "Veloria"
276
289
  }
277
290
  ],
278
291
  "genres": [
279
292
  {
280
293
  "id": "1",
281
- "isArchived": false,
282
294
  "name": "Crime",
283
295
  "parentIds": []
284
296
  },
285
297
  {
286
298
  "id": "2",
287
- "isArchived": false,
288
299
  "name": "Gangster",
289
300
  "parentIds": ["1"]
290
301
  },
291
302
  {
292
303
  "id": "3",
293
- "isArchived": false,
294
304
  "name": "Drama",
295
305
  "parentIds": []
296
306
  },
297
307
  {
298
308
  "id": "4",
299
- "isArchived": false,
300
309
  "name": "Comedy",
301
310
  "parentIds": []
302
311
  }
@@ -318,7 +327,6 @@ Cascading deletion runs as one operation, including cyclic relations. A validati
318
327
  "coverSrc": "https://example.com/covers/shadows-of-ardenia.jpg",
319
328
  "description": "An heiress in a port city uncovers a plot involving two rival families.",
320
329
  "id": "1",
321
- "isArchived": false,
322
330
  "publisherIds": ["2"],
323
331
  "title": "Shadows of Ardenia"
324
332
  },
@@ -327,7 +335,6 @@ Cascading deletion runs as one operation, including cyclic relations. A validati
327
335
  "coverSrc": "https://example.com/covers/northern-star.jpg",
328
336
  "description": "A night clerk at an old hotel gets drawn into the search for a missing painting.",
329
337
  "id": "2",
330
- "isArchived": false,
331
338
  "publisherIds": ["1"],
332
339
  "title": "Midnight at the Northern Star"
333
340
  }
@@ -335,12 +342,10 @@ Cascading deletion runs as one operation, including cyclic relations. A validati
335
342
  "publishers": [
336
343
  {
337
344
  "id": "1",
338
- "isArchived": false,
339
345
  "name": "Northlight Studio"
340
346
  },
341
347
  {
342
348
  "id": "2",
343
- "isArchived": false,
344
349
  "name": "Aurora Pictures"
345
350
  }
346
351
  ],
@@ -349,15 +354,13 @@ Cascading deletion runs as one operation, including cyclic relations. A validati
349
354
  "bornAt": "1988-03-14",
350
355
  "countryId": "1",
351
356
  "fullName": "Mira Volkova",
352
- "id": "1",
353
- "isArchived": false
357
+ "id": "1"
354
358
  },
355
359
  {
356
360
  "bornAt": "1991-11-02",
357
361
  "countryId": "2",
358
362
  "fullName": "Leon Vetrov",
359
- "id": "2",
360
- "isArchived": false
363
+ "id": "2"
361
364
  }
362
365
  ]
363
366
  }
@@ -471,6 +474,8 @@ Omitting `scope` returns scalar fields, as with `[{"*":true}]`. An empty selecti
471
474
 
472
475
  Arguments are available only on lists. A list can instead use `{ "union": [scope, ...] }`: each part is a normal list scope, parts run in array order, and the first record for each primary key is kept. For embedded object arrays without primary keys, each source element is kept once, even if several parts select it. Distinct elements with equal contents remain separate. Single-record queries and mutation responses can set arguments on their embedded lists. Parameters are validated even on empty data; an invalid response selection rolls back record changes. Invalid scopes return `400`. The JSON length limit is 10,000 characters; selection depth is limited to 32 levels.
473
476
 
477
+ REST and GraphQL also enforce a shared budget for each request: at most 1,000 selection nodes, 10,000 expanded records, 100,000 accounted execution steps, and a conservative JSON size estimate of 8 MiB. Nested lists, `union` parts and GraphQL aliases consume the same budget. Exceeding a limit returns REST `400` or GraphQL `INVALID_QUERY`; reduce selected fields, nesting or page sizes. REST mutations roll back if their response projection exceeds the budget.
478
+
474
479
  ### Nested writes
475
480
 
476
481
  Explicitly declared storage keys such as `genreIds: ["1"]` only set a relation. Relation fields also accept records to create or update:
@@ -495,20 +500,20 @@ Content-Type: application/json
495
500
 
496
501
  | Relation value | Behavior |
497
502
  |---|---|
498
- | An object containing only a primary key | Link an existing record without changing it |
503
+ | An object containing only a primary key | Link an existing record without updating its fields or audit values |
499
504
  | An object with a primary key and other fields | PATCH updates supplied fields; PUT replaces the related record |
500
505
  | An object without a primary key | Create a related record with defaults and a generated key |
501
506
 
502
- The key name and type follow the target model. An object containing only a key still counts as an update: in PUT it must include the model's required fields. Replacement preserves primary keys, generated values and `readOnly` fields. In POST, nested objects with existing keys receive partial updates. A missing target is an error; creating a nested record without a key requires an autogenerated primary key.
507
+ The key name and type follow the target model. An object containing only that key is a reference in POST, PUT and PATCH; it does not require the target's other mandatory fields. Adding other fields makes it an update: PUT requires all mandatory fields and removes omitted optional fields or resets them to defaults; POST/PATCH update supplied fields. Replacement preserves primary keys, generated values and `readOnly` fields. A missing target is an error; creating a nested record without a key requires an autogenerated primary key. Declared storage keys, for example `countryId`, can also change links directly.
503
508
 
504
- A supplied list replaces the relation's membership. PATCH preserves omitted relations; PUT clears omitted writable links. `[]` clears a list and `null` clears a nullable single relation. Removing a link does not delete the related record. Required single relations must remain linked; required list relations may be empty.
509
+ A supplied list replaces the relation's membership. PATCH preserves omitted relations. PUT replaces the current record's writable fields, including its own relation keys, but preserves omitted `keyOn: "related"` links whose keys live in other records. `[]` clears a list and `null` clears a nullable single relation. Removing a link does not delete the related record. Required single relations must remain linked; required list relations may be empty.
505
510
 
506
511
  When the storage key is declared in `fields`, use either the relation field or its key in an object, for example `genres` or `genreIds`. Supplying both fields returns `400 INVALID_INPUT`, even when their key sets match:
507
512
 
508
513
  ```json
509
514
  {
510
515
  "genreIds": ["1", "2"],
511
- "genres": ["1", "2"]
516
+ "genres": [{ "id": "1" }, { "id": "2" }]
512
517
  }
513
518
  ```
514
519
 
@@ -516,7 +521,7 @@ The server rejects both fields together and rolls back the operation. Relation f
516
521
 
517
522
  All nested changes belong to the main record's transaction. A validation error, missing record or invalid response selection rolls back the entire operation. Updating a shared record affects every record linked to it.
518
523
 
519
- GraphQL accepts typed objects in relation fields. To change only the links in a replace mutation, use a declared storage key such as `genreIds`. For example:
524
+ GraphQL accepts typed objects in relation fields. To change only links, including in a replace mutation, pass objects containing only primary keys or use a declared storage key such as `genreIds`. Required fields for nested creation or replacement with additional fields are checked at runtime. For example:
520
525
 
521
526
  ```graphql
522
527
  mutation {
@@ -568,9 +573,13 @@ query {
568
573
 
569
574
  The query `user(id: ...)` returns one record or `null` if it is missing. Mutations are `userCreate(data: ...)`, `userReplace(id: ..., data: ...)`, `userUpdate(id: ..., data: ...)`, `userDelete(id: ...)`. For models containing only generated fields, the create mutation takes no `data` argument. Writes and validation follow the same rules as REST. Relations selected in a mutation result determine the response contents.
570
575
 
576
+ When the primary key is named `data`, Update/Replace mutations accept its value as `key` and the input object as `data`, for example `entryUpdate(key: "one", data: { name: "Updated" })`. Find/Delete keep the primary-key argument named `data`.
577
+
571
578
  String primary keys use GraphQL `ID`; ordinary strings use `String`, numbers use `Float`, and pagination parameters use `Int`. Schema enums preserve valid string labels; other values receive `VALUE_0`, `VALUE_1`, etc. String lengths, formats and other model constraints are validated by the server during request execution. Introspection is available for exploring the schema. Selected list arguments are validated before executing mutations.
572
579
 
573
- Errors include `extensions.code`: `INVALID_INPUT`, `INVALID_QUERY`, `NOT_FOUND`, `CONFLICT`, `UNAUTHENTICATED`, `FORBIDDEN` or `INTERNAL_ERROR`. GraphQL syntax and type errors appear in the standard `errors` array. Query depth is limited to 32 levels.
580
+ Errors include `extensions.code`: `INVALID_INPUT`, `INVALID_QUERY`, `NOT_FOUND`, `CONFLICT`, `UNAUTHENTICATED`, `FORBIDDEN` or `INTERNAL_ERROR`. GraphQL syntax and type errors appear in the standard `errors` array. Query depth is limited to 32 levels. The request budgets described for REST also apply to GraphQL. Selection limits and list arguments are checked before mutations run. Errors while resolving a mutation result, including runtime budget exhaustion, do not undo a mutation that has already committed; query the record to check its state before retrying.
581
+
582
+ A GraphQL operation containing several mutation fields is not one transaction: fields execute in order, and earlier committed changes remain if a later result fails. Runtime budget exhaustion returns `data: null` and stops subsequent mutation fields, but `data: null` does not mean writes were rolled back. Recover the saved state with a smaller query. Automatically retrying a create mutation with a generated key can create a second record.
574
583
 
575
584
  ## OpenAPI and schema exports
576
585
 
@@ -612,7 +621,7 @@ if (!password) throw new Error('Set DJS_PASSWORD');
612
621
  await writeFile(
613
622
  './auth.json',
614
623
  JSON.stringify(
615
- [{ id: '1', username: 'admin', passwordHash: await hashPassword(password), isAdmin: true }],
624
+ [{ id: '1', username: 'admin', passwordHash: await hashPassword(password), isAdmin: true, sessions: [] }],
616
625
  null,
617
626
  2,
618
627
  ),
@@ -630,7 +639,7 @@ export default {
630
639
  };
631
640
  ```
632
641
 
633
- Start with `npx deep-json-server server.config.js`. Each initial user needs a unique string `id`, a unique `username` and a `passwordHash` created by the helper. `isAdmin` defaults to `false`. Passwords use salted scrypt hashes.
642
+ Start with `npx deep-json-server server.config.js`. Each initial user needs a unique string `id`, a unique `username` and a `passwordHash` created by the helper. `isAdmin` defaults to `false`, and `sessions` defaults to `[]`, so existing user files remain valid. Passwords use salted scrypt hashes.
634
643
 
635
644
  With `storage: 'memory'`, pass an array in `auth.source`:
636
645
 
@@ -645,13 +654,19 @@ export default {
645
654
  database: { source: { items: [] } },
646
655
  auth: {
647
656
  source: [
648
- { id: '1', username: 'admin', passwordHash: await hashPassword(password), isAdmin: true },
657
+ { id: '1', username: 'admin', passwordHash: await hashPassword(password), isAdmin: true, sessions: [] },
649
658
  ],
650
659
  },
651
660
  };
652
661
  ```
653
662
 
654
- Auth users are stored separately from the database. In file mode, changes are saved to `auth.source`; in memory mode, they disappear on restart. The supplied user array is not modified. Restart the server after editing the file manually.
663
+ Auth users and their sessions are stored together in `auth.source`, separately from the database. Each entry in a user's `sessions` array contains `{ tokenHash, expiresAt }`: `tokenHash` is the SHA-256 hash of the access token as 64 lowercase hexadecimal characters, and `expiresAt` is an absolute Unix timestamp in milliseconds. Raw tokens are never stored, and auth responses expose neither password hashes nor session records. Token hashes must be unique across all users.
664
+
665
+ With `storage: 'file'`, login, logout and user changes are saved to the same JSON file before a successful response. Valid sessions survive server restarts: the client can keep using its existing token. A password change and removal of that user's sessions are saved together. A failed write leaves the previous users and sessions unchanged. Restart the server after editing the file manually; if you replace a password hash manually, also clear that user's `sessions`.
666
+
667
+ With `storage: 'memory'`, the supplied user array is not modified. Every startup loads the initial users and sessions again. Sessions created at runtime disappear on restart; unexpired sessions supplied in the initial data are restored, including any that were logged out during the previous run. The client still needs the corresponding raw token; it cannot recover it from `tokenHash`.
668
+
669
+ `auth.expiresIn` sets the lifetime of new sessions and defaults to 3600 seconds. Existing sessions keep their saved expiration time, including across restarts. Expired sessions are ignored on startup, rejected on every request and removed from all users on the next successful auth update, such as login or logout. There is no automatic extension of the expiration time.
655
670
 
656
671
  | REST request | JSON body | Response |
657
672
  |---|---|---|
@@ -662,13 +677,13 @@ Auth users are stored separately from the database. In file mode, changes are sa
662
677
  | `PATCH /auth/users/:id/password` | `{ "currentPassword": "…", "newPassword": "…" }` | `{ success: true }` |
663
678
  | `PATCH /auth/users/:id/admin` | `{ "isAdmin": true }` | `{ id, username, isAdmin }` |
664
679
 
665
- Registration and login are public. Send `Authorization: Bearer <accessToken>` for the other methods. Registration creates an ordinary user with a generated `id`; requests cannot include `id`, `passwordHash` or `isAdmin`. Usernames are case-sensitive and unique; duplicates return `409`. A username must contain a non-whitespace character and be at most 256 characters long. Passwords must contain 1–1024 characters. Values are not trimmed. Registration does not create a session: log in afterwards.
680
+ Registration and login are public. Send `Authorization: Bearer <accessToken>` for the other methods. Registration creates an ordinary user with a generated `id`; requests cannot include `id`, `passwordHash`, `isAdmin` or `sessions`. Usernames are case-sensitive and unique; duplicates return `409`. A username must contain a non-whitespace character and be at most 256 characters long. Passwords must contain 1–1024 characters. Values are not trimmed. Registration does not create a session: log in afterwards.
666
681
 
667
682
  Users can change only their own password by supplying `currentPassword` and `newPassword`. Administrators follow the same rule for their own password. An administrator can change an ordinary user's password with just `newPassword`. Changing another administrator's password returns `403`. A successful password change ends all sessions of the target user, including the current session when changing your own password; log in again. An incorrect current password returns `401` without changing sessions.
668
683
 
669
684
  Only administrators can change `isAdmin`. They can grant or remove another user's admin status. An administrator can remove their own status only if another administrator remains; otherwise the request returns `409`. This check accounts for concurrent requests. Existing tokens use the new permissions as soon as the change is saved, including for GraphQL mutations. An administrator may demote another administrator and then change their password as an ordinary user.
670
685
 
671
- Invalid or expired tokens return `401`, insufficient permissions return `403`, and an absent user for an otherwise permitted operation returns `404`. Invalid request bodies return `400`. Login, registration and password changes may return `429` when too many password computations are running; login also limits active sessions. Sessions are kept in memory and disappear on restart. Logout revokes only the supplied token.
686
+ Invalid or expired tokens return `401`, insufficient permissions return `403`, and an absent user for an otherwise permitted operation returns `404`. Invalid request bodies return `400`. Login, registration and password changes may return `429` when too many password computations are running; login also returns `429` when there are already 10,000 active sessions across all users, including sessions loaded from the source. Logout revokes only the supplied token.
672
687
 
673
688
  Every model record has a virtual `actions` object when auth is enabled. It is calculated for the current user and is never stored. In REST, request it explicitly through `scope`:
674
689
 
@@ -760,7 +775,7 @@ Cascade deletion follows `onDelete` and each affected model's `softDelete`. Rest
760
775
 
761
776
  Restoration metadata is stored with the records and survives restarts; include it when backing up the database. The internal `djsDeletion` field is reserved and is not exposed by either API.
762
777
 
763
- With auth, any authenticated user can create records. Updating, deleting and restoring require ownership through `createdById` or `isAdmin: true`. Only administrators can change unowned records. Administrator edits preserve the original owner. The rules cover nested writes, changes to relation storage keys, cascades and restoration. Linking an existing record without changing it does not require owning it. Each mutation is atomic: denied changes leave all affected records unchanged.
778
+ With auth, any authenticated user can create records. Updating, deleting and restoring require ownership through `createdById` or `isAdmin: true`. Only administrators can change unowned records. Administrator edits preserve the original owner. The rules cover nested writes, changes to relation storage keys, cascades and restoration. Linking a record through a primary-key-only object or a declared storage key does not require owning the unchanged target. A nested object with additional fields requires permission to update it. Changing inverse links still requires permission for every record whose stored key changes. Each mutation is atomic: denied changes leave all affected records unchanged.
764
779
 
765
780
  REST returns 401 for an invalid or missing token and 403 for insufficient permissions. GraphQL applies the same rules to mutations and returns `UNAUTHENTICATED` or `FORBIDDEN`; obtain the token through REST login and send `Authorization: Bearer <token>`. GraphQL reads remain public, as do OPTIONS requests and every file operation.
766
781
 
@@ -839,7 +854,7 @@ Content-Type: application/json
839
854
 
840
855
  In disk mode, the binary is stored at `<files.source>/<directory>/<name>`. Metadata defaults to `<files.source>/.files.json`; set `files.metadata` for another location. Directories and the metadata file are created when needed.
841
856
 
842
- Use one server process per disk database and file store. A second server using the same database file fails at startup; closing the first server releases its lock, and a lock left by a crashed process is recovered on the next start. Stop the server before editing stored files or metadata manually. Storage paths cannot contain symbolic links. Uploads and renames cannot overwrite the database, its lock, counters, schema, auth users, loaded configuration or metadata file.
857
+ Use one server process per writable disk source. A second server sharing the database, auth users, file storage directory or metadata fails at startup. Locks are single JSON files next to their sources: `database.json` uses `database-lock.json`, with the last extension removed from the configured source name. A lock records its type, process ID and owner token. Closing the server removes the lock files; after a crash the operating system releases the locks and the next server reuses the abandoned files. Disk locking uses the native `fs-native-extensions` dependency; memory storage and schema generation do not load it. Symbolic-link sources lock both the original entry and its target. Stop the server before editing stored files or metadata manually. Paths inside the file storage directory cannot contain symbolic links. Uploads and renames cannot overwrite the database, its lock, counters, schema, auth users, loaded configuration or metadata file.
843
858
 
844
859
  Send the file as a binary request body. In a browser, use `xhr.send(file)` and track progress through `XMLHttpRequest.upload.onprogress`. The default maximum size is 100 MiB and can be changed through `server.maxFileSize`. Missing or unsafe headers and paths return `400`, an exceeded limit returns `413`, and a missing, malformed, or Fastify-unsupported `Content-Type` returns `400` or `415`, depending on which validation stage rejects it.
845
860
 
@@ -892,6 +907,6 @@ npm run verify
892
907
 
893
908
  `npm run verify` checks types, code style, test coverage and installation from the package archive.
894
909
 
895
- To publish a prerelease, update the version in `package.json`, `package-lock.json` and `src/core/constants.ts`, then push the commit to `main`. GitHub Actions creates its `v<version>` tag and publishes through trusted publishing to the `alpha`, `beta` or `rc` channel. Stable versions publish to `latest` from an explicitly pushed version tag.
910
+ To publish a prerelease, update the version in `package.json` and `package-lock.json` (the runtime and CLI read it from package metadata), then push the commit to `main`. GitHub Actions creates its `v<version>` tag and publishes through trusted publishing to the `alpha`, `beta` or `rc` channel. Stable versions publish to `latest` from an explicitly pushed version tag.
896
911
 
897
912
  License: MIT.