@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.
- package/CHANGELOG.md +20 -0
- package/MIGRATION.md +187 -25
- package/MIGRATION.ru.md +211 -0
- package/README.md +48 -33
- package/README.ru.md +50 -35
- package/dist/index.d.ts +1 -1
- package/dist/package.json +104 -0
- package/dist/src/auth/contract.d.ts +8 -0
- package/dist/src/auth/contract.js.map +1 -1
- package/dist/src/auth/public.d.ts +1 -1
- package/dist/src/auth/service.d.ts +7 -16
- package/dist/src/auth/service.js +40 -48
- package/dist/src/auth/service.js.map +1 -1
- package/dist/src/auth/store.d.ts +11 -5
- package/dist/src/auth/store.js +37 -19
- package/dist/src/auth/store.js.map +1 -1
- package/dist/src/cli/index.js +2 -2
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/core/constants.d.ts +1 -1
- package/dist/src/core/constants.js +2 -1
- package/dist/src/core/constants.js.map +1 -1
- package/dist/src/core/engine.d.ts +2 -1
- package/dist/src/core/engine.js +7 -3
- package/dist/src/core/engine.js.map +1 -1
- package/dist/src/core/model/infer.js +8 -5
- package/dist/src/core/model/infer.js.map +1 -1
- package/dist/src/core/model/load.js +1 -1
- package/dist/src/core/model/load.js.map +1 -1
- package/dist/src/core/mutations/write.d.ts +3 -3
- package/dist/src/core/mutations/write.js +10 -5
- package/dist/src/core/mutations/write.js.map +1 -1
- package/dist/src/core/paths.d.ts +4 -0
- package/dist/src/core/paths.js +7 -0
- package/dist/src/core/paths.js.map +1 -1
- package/dist/src/core/query/budget.d.ts +24 -0
- package/dist/src/core/query/budget.js +69 -0
- package/dist/src/core/query/budget.js.map +1 -0
- package/dist/src/core/query/execute.d.ts +2 -1
- package/dist/src/core/query/execute.js +5 -2
- package/dist/src/core/query/execute.js.map +1 -1
- package/dist/src/core/query/filter.d.ts +2 -1
- package/dist/src/core/query/filter.js +27 -9
- package/dist/src/core/query/filter.js.map +1 -1
- package/dist/src/core/records.d.ts +2 -0
- package/dist/src/core/records.js +15 -4
- package/dist/src/core/records.js.map +1 -1
- package/dist/src/files/contract.js +1 -0
- package/dist/src/files/contract.js.map +1 -1
- package/dist/src/files/disk-store.js +7 -5
- package/dist/src/files/disk-store.js.map +1 -1
- package/dist/src/graphql/preflight.d.ts +11 -2
- package/dist/src/graphql/preflight.js +48 -2
- package/dist/src/graphql/preflight.js.map +1 -1
- package/dist/src/graphql/resolvers.d.ts +3 -0
- package/dist/src/graphql/resolvers.js +47 -8
- package/dist/src/graphql/resolvers.js.map +1 -1
- package/dist/src/graphql/routes.d.ts +1 -1
- package/dist/src/graphql/routes.js +16 -0
- package/dist/src/graphql/routes.js.map +1 -1
- package/dist/src/graphql/schema.js +6 -4
- package/dist/src/graphql/schema.js.map +1 -1
- package/dist/src/openapi/document.js +8 -2
- package/dist/src/openapi/document.js.map +1 -1
- package/dist/src/rest/options.d.ts +2 -1
- package/dist/src/rest/options.js +5 -3
- package/dist/src/rest/options.js.map +1 -1
- package/dist/src/rest/projection.d.ts +4 -3
- package/dist/src/rest/projection.js +31 -12
- package/dist/src/rest/projection.js.map +1 -1
- package/dist/src/rest/routes.js +13 -7
- package/dist/src/rest/routes.js.map +1 -1
- package/dist/src/server/bootstrap.js +15 -7
- package/dist/src/server/bootstrap.js.map +1 -1
- package/dist/src/server/input-paths.d.ts +14 -1
- package/dist/src/server/input-paths.js +18 -2
- package/dist/src/server/input-paths.js.map +1 -1
- package/dist/src/server/storage-lock.d.ts +7 -1
- package/dist/src/server/storage-lock.js +103 -99
- package/dist/src/server/storage-lock.js.map +1 -1
- package/examples/database.json +2 -14
- package/examples/schema.json +0 -15
- 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.
|
|
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,
|
|
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`.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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 `
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|