@kollors/deep-json-server 1.0.0-rc.5 → 1.0.0-rc.7
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 +14 -0
- package/MIGRATION.md +191 -21
- package/MIGRATION.ru.md +211 -0
- package/README.md +40 -30
- package/README.ru.md +42 -32
- package/dist/src/core/constants.d.ts +1 -1
- package/dist/src/core/constants.js +1 -1
- package/dist/src/core/engine.js +4 -0
- package/dist/src/core/engine.js.map +1 -1
- package/dist/src/core/model/infer.js +2 -2
- package/dist/src/core/model/infer.js.map +1 -1
- package/dist/src/core/model/load.js +36 -6
- package/dist/src/core/model/load.js.map +1 -1
- package/dist/src/core/model/tree.js +2 -0
- package/dist/src/core/model/tree.js.map +1 -1
- package/dist/src/core/model/types.d.ts +1 -0
- package/dist/src/core/mutations/write.d.ts +3 -3
- package/dist/src/core/mutations/write.js +13 -14
- package/dist/src/core/mutations/write.js.map +1 -1
- package/dist/src/graphql/schema.js +1 -1
- 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/examples/database.json +2 -14
- package/examples/schema.json +14 -21
- package/package.json +2 -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.7`.
|
|
18
18
|
|
|
19
19
|
## Quick start
|
|
20
20
|
|
|
@@ -151,7 +151,7 @@ Examples: [database](examples/database.json), [model schema](examples/schema.jso
|
|
|
151
151
|
},
|
|
152
152
|
"users": {
|
|
153
153
|
"type": "User[]",
|
|
154
|
-
"
|
|
154
|
+
"keyOn": "related"
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
},
|
|
@@ -169,7 +169,7 @@ Examples: [database](examples/database.json), [model schema](examples/schema.jso
|
|
|
169
169
|
},
|
|
170
170
|
"country": {
|
|
171
171
|
"type": "Country",
|
|
172
|
-
"
|
|
172
|
+
"keyOn": "current"
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
175
|
}
|
|
@@ -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
|
|
|
@@ -213,7 +226,7 @@ The `type` property accepts `string`, `number`, `boolean`, `object`, or a model
|
|
|
213
226
|
| `minLength`, `maxLength`, `pattern` | String constraints |
|
|
214
227
|
| `format` | `date`, `date-time`, `email`, `uri`, `uuid` |
|
|
215
228
|
| `minimum`, `maximum` | Inclusive numeric bounds |
|
|
216
|
-
| `source`, `target`, `onDelete` | Relation metadata |
|
|
229
|
+
| `keyOn`, `source`, `target`, `onDelete` | Relation metadata |
|
|
217
230
|
|
|
218
231
|
String and numeric constraints on `string[]`/`number[]` apply to every element. `required` and `nullable` apply to the entire array; `default` and `example` contain a complete array. Elements must match the array's type and be non-null. `required` requires a stored array field to be present but does not require any elements.
|
|
219
232
|
|
|
@@ -231,26 +244,35 @@ Objects used in GraphQL must have at least one field visible in responses; REST
|
|
|
231
244
|
{
|
|
232
245
|
"actors.genres": {
|
|
233
246
|
"type": "Genre[]",
|
|
247
|
+
"keyOn": "current",
|
|
234
248
|
"source": "actors.genreIds",
|
|
235
249
|
"required": true
|
|
236
250
|
}
|
|
237
251
|
}
|
|
238
252
|
```
|
|
239
253
|
|
|
240
|
-
`Genre` returns an object; `Genre[]` returns a list.
|
|
254
|
+
`Genre` returns an object; `Genre[]` returns a list. Every explicit relation needs a matching declaration on the other model. Both declarations require `keyOn`: `current` marks the side that stores the key, and `related` marks its inverse. The pair must contain one of each. For this example, add `Genre.movies = {"type":"Movie[]","keyOn":"related","target":"actors.genreIds"}`.
|
|
255
|
+
|
|
256
|
+
On the `current` side, `target` defaults to the related model's primary key. `source` defaults to the relation field name plus that target key: `User.country` → `User.countryId`; a list uses the singular field name and a plural key: `Genre.parents` → `Genre.parentIds`. Thus `Genre.parents = {"type":"Genre[]","keyOn":"current"}` and `Genre.children = {"type":"Genre[]","keyOn":"related"}` share `Genre.parentIds`. Nested relations keep their parent path: `Movie.actors.genres` → `Movie.actors.genreIds`. A custom primary key also changes the suffix: `Movie.publishers` targeting `Publisher.ref` → `Movie.publisherRefs`.
|
|
241
257
|
|
|
242
|
-
|
|
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.
|
|
243
259
|
|
|
244
|
-
|
|
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.
|
|
263
|
+
|
|
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.
|
|
245
265
|
|
|
246
266
|
Every directly supplied, explicitly declared relation key must point to an existing record. `required: true` on a single relation requires a target. On a list relation backed by its own array of source keys, it requires that array to be present, but `[]` is valid. A list computed without its own source-key array, including a reverse relation, is always available and may be empty even with `required: true`. Missing single relations return `null`.
|
|
247
267
|
|
|
268
|
+
Omitting a `related` relation from `PUT` leaves its existing links unchanged, because those keys live in other records. Supply the relation explicitly to replace its links; for example, `"users": []` removes every user from that country's inverse list.
|
|
269
|
+
|
|
248
270
|
`onDelete` describes what happens **when a target record is deleted**:
|
|
249
271
|
|
|
250
272
|
- `restrict` (default): refuse deletion while a retained record refers to the target.
|
|
251
273
|
- `cascade`: delete the referring record. For `User.country`, deleting the country deletes its users. For `Movie.actors.user`, deleting the user removes matching actor elements and retains the movie.
|
|
252
274
|
|
|
253
|
-
Cascading deletion runs as one operation, including cyclic relations. A validation failure cancels the entire operation. `onDelete`
|
|
275
|
+
Cascading deletion runs as one operation, including cyclic relations. A validation failure cancels the entire operation. A `related` inverse without `onDelete` does not restrict deletion merely because the other side stores a key; set `onDelete` on that inverse to define an additional rule.
|
|
254
276
|
|
|
255
277
|
## Example database
|
|
256
278
|
|
|
@@ -259,37 +281,31 @@ Cascading deletion runs as one operation, including cyclic relations. A validati
|
|
|
259
281
|
"countries": [
|
|
260
282
|
{
|
|
261
283
|
"id": "1",
|
|
262
|
-
"isArchived": false,
|
|
263
284
|
"name": "Ardenia"
|
|
264
285
|
},
|
|
265
286
|
{
|
|
266
287
|
"id": "2",
|
|
267
|
-
"isArchived": false,
|
|
268
288
|
"name": "Veloria"
|
|
269
289
|
}
|
|
270
290
|
],
|
|
271
291
|
"genres": [
|
|
272
292
|
{
|
|
273
293
|
"id": "1",
|
|
274
|
-
"isArchived": false,
|
|
275
294
|
"name": "Crime",
|
|
276
295
|
"parentIds": []
|
|
277
296
|
},
|
|
278
297
|
{
|
|
279
298
|
"id": "2",
|
|
280
|
-
"isArchived": false,
|
|
281
299
|
"name": "Gangster",
|
|
282
300
|
"parentIds": ["1"]
|
|
283
301
|
},
|
|
284
302
|
{
|
|
285
303
|
"id": "3",
|
|
286
|
-
"isArchived": false,
|
|
287
304
|
"name": "Drama",
|
|
288
305
|
"parentIds": []
|
|
289
306
|
},
|
|
290
307
|
{
|
|
291
308
|
"id": "4",
|
|
292
|
-
"isArchived": false,
|
|
293
309
|
"name": "Comedy",
|
|
294
310
|
"parentIds": []
|
|
295
311
|
}
|
|
@@ -311,7 +327,6 @@ Cascading deletion runs as one operation, including cyclic relations. A validati
|
|
|
311
327
|
"coverSrc": "https://example.com/covers/shadows-of-ardenia.jpg",
|
|
312
328
|
"description": "An heiress in a port city uncovers a plot involving two rival families.",
|
|
313
329
|
"id": "1",
|
|
314
|
-
"isArchived": false,
|
|
315
330
|
"publisherIds": ["2"],
|
|
316
331
|
"title": "Shadows of Ardenia"
|
|
317
332
|
},
|
|
@@ -320,7 +335,6 @@ Cascading deletion runs as one operation, including cyclic relations. A validati
|
|
|
320
335
|
"coverSrc": "https://example.com/covers/northern-star.jpg",
|
|
321
336
|
"description": "A night clerk at an old hotel gets drawn into the search for a missing painting.",
|
|
322
337
|
"id": "2",
|
|
323
|
-
"isArchived": false,
|
|
324
338
|
"publisherIds": ["1"],
|
|
325
339
|
"title": "Midnight at the Northern Star"
|
|
326
340
|
}
|
|
@@ -328,12 +342,10 @@ Cascading deletion runs as one operation, including cyclic relations. A validati
|
|
|
328
342
|
"publishers": [
|
|
329
343
|
{
|
|
330
344
|
"id": "1",
|
|
331
|
-
"isArchived": false,
|
|
332
345
|
"name": "Northlight Studio"
|
|
333
346
|
},
|
|
334
347
|
{
|
|
335
348
|
"id": "2",
|
|
336
|
-
"isArchived": false,
|
|
337
349
|
"name": "Aurora Pictures"
|
|
338
350
|
}
|
|
339
351
|
],
|
|
@@ -342,15 +354,13 @@ Cascading deletion runs as one operation, including cyclic relations. A validati
|
|
|
342
354
|
"bornAt": "1988-03-14",
|
|
343
355
|
"countryId": "1",
|
|
344
356
|
"fullName": "Mira Volkova",
|
|
345
|
-
"id": "1"
|
|
346
|
-
"isArchived": false
|
|
357
|
+
"id": "1"
|
|
347
358
|
},
|
|
348
359
|
{
|
|
349
360
|
"bornAt": "1991-11-02",
|
|
350
361
|
"countryId": "2",
|
|
351
362
|
"fullName": "Leon Vetrov",
|
|
352
|
-
"id": "2"
|
|
353
|
-
"isArchived": false
|
|
363
|
+
"id": "2"
|
|
354
364
|
}
|
|
355
365
|
]
|
|
356
366
|
}
|
|
@@ -488,20 +498,20 @@ Content-Type: application/json
|
|
|
488
498
|
|
|
489
499
|
| Relation value | Behavior |
|
|
490
500
|
|---|---|
|
|
491
|
-
| An object containing only a primary key | Link an existing record without
|
|
501
|
+
| An object containing only a primary key | Link an existing record without updating its fields or audit values |
|
|
492
502
|
| An object with a primary key and other fields | PATCH updates supplied fields; PUT replaces the related record |
|
|
493
503
|
| An object without a primary key | Create a related record with defaults and a generated key |
|
|
494
504
|
|
|
495
|
-
The key name and type follow the target model. An object containing only
|
|
505
|
+
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.
|
|
496
506
|
|
|
497
|
-
A supplied list replaces the relation's membership. PATCH preserves omitted relations
|
|
507
|
+
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.
|
|
498
508
|
|
|
499
509
|
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:
|
|
500
510
|
|
|
501
511
|
```json
|
|
502
512
|
{
|
|
503
513
|
"genreIds": ["1", "2"],
|
|
504
|
-
"genres": ["1", "2"]
|
|
514
|
+
"genres": [{ "id": "1" }, { "id": "2" }]
|
|
505
515
|
}
|
|
506
516
|
```
|
|
507
517
|
|
|
@@ -509,7 +519,7 @@ The server rejects both fields together and rolls back the operation. Relation f
|
|
|
509
519
|
|
|
510
520
|
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.
|
|
511
521
|
|
|
512
|
-
GraphQL accepts typed objects in relation fields. To change only
|
|
522
|
+
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:
|
|
513
523
|
|
|
514
524
|
```graphql
|
|
515
525
|
mutation {
|
|
@@ -753,7 +763,7 @@ Cascade deletion follows `onDelete` and each affected model's `softDelete`. Rest
|
|
|
753
763
|
|
|
754
764
|
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.
|
|
755
765
|
|
|
756
|
-
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
|
|
766
|
+
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.
|
|
757
767
|
|
|
758
768
|
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.
|
|
759
769
|
|
|
@@ -832,7 +842,7 @@ Content-Type: application/json
|
|
|
832
842
|
|
|
833
843
|
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.
|
|
834
844
|
|
|
835
|
-
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.
|
|
845
|
+
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. 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.
|
|
836
846
|
|
|
837
847
|
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.
|
|
838
848
|
|
package/README.ru.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md)
|
|
4
4
|
|
|
5
|
-
[История выпусков](CHANGELOG.md) · [Переход с 0.9.0](MIGRATION.md)
|
|
5
|
+
[История выпусков](CHANGELOG.md) · [Переход с 0.9.0](MIGRATION.ru.md)
|
|
6
6
|
|
|
7
7
|
JSON-сервер для создания тестового API с REST, GraphQL, связями между записями, загрузкой файлов и экспортом схем. Поддерживает вход пользователей, права владельца и администратора, даты записей и мягкое удаление. Требуется Node.js 22 или новее.
|
|
8
8
|
|
|
9
|
-
**Изменения в 1.0.0, нарушающие совместимость.** При переходе с 0.9.0 воспользуйтесь [инструкцией по миграции](MIGRATION.md). `*` в REST `scope` выбирает только скалярные поля, которые не хранят ключи связей. Массивы, объекты, связи и явно объявленные ключи связей нужно указывать в выборе полей. При включённой аутентификации права на запись доступны через виртуальное поле `actions`.
|
|
9
|
+
**Изменения в 1.0.0, нарушающие совместимость.** При переходе с 0.9.0 воспользуйтесь [инструкцией по миграции](MIGRATION.ru.md). `*` в REST `scope` выбирает только скалярные поля, которые не хранят ключи связей. Массивы, объекты, связи и явно объявленные ключи связей нужно указывать в выборе полей. При включённой аутентификации права на запись доступны через виртуальное поле `actions`.
|
|
10
10
|
|
|
11
11
|
## Установка
|
|
12
12
|
|
|
@@ -14,7 +14,7 @@ JSON-сервер для создания тестового API с REST, GraphQ
|
|
|
14
14
|
npm install @kollors/deep-json-server@rc
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
Для установки этого релиз-кандидата укажите `@1.0.0-rc.
|
|
17
|
+
Для установки этого релиз-кандидата укажите `@1.0.0-rc.7`.
|
|
18
18
|
|
|
19
19
|
## Быстрый старт
|
|
20
20
|
|
|
@@ -151,7 +151,7 @@ export default {
|
|
|
151
151
|
},
|
|
152
152
|
"users": {
|
|
153
153
|
"type": "User[]",
|
|
154
|
-
"
|
|
154
|
+
"keyOn": "related"
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
},
|
|
@@ -169,7 +169,7 @@ export default {
|
|
|
169
169
|
},
|
|
170
170
|
"country": {
|
|
171
171
|
"type": "Country",
|
|
172
|
-
"
|
|
172
|
+
"keyOn": "current"
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
175
|
}
|
|
@@ -177,7 +177,20 @@ export default {
|
|
|
177
177
|
}
|
|
178
178
|
```
|
|
179
179
|
|
|
180
|
-
Модели задаются в `models`. Корень схемы принимает `models`, `api`, `timestamps` и `softDelete`. Корневой `api` выбирает маршруты базы. Если
|
|
180
|
+
Модели задаются в `models`. Корень схемы принимает `models`, `api`, `timestamps` и `softDelete`. Корневой `api` выбирает маршруты базы. Если он не задан, значение по умолчанию зависит от способа использования схемы:
|
|
181
|
+
|
|
182
|
+
| Контекст | Корневой `api` по умолчанию |
|
|
183
|
+
|---|---|
|
|
184
|
+
| Сервер без секции `graphql` | `['rest']` |
|
|
185
|
+
| Сервер с секцией `graphql` | `['rest', 'graphql']` |
|
|
186
|
+
| Отдельный вызов `generateOpenapi()` | `['rest']` |
|
|
187
|
+
| Отдельный вызов `generateGraphql()` | `['graphql']` |
|
|
188
|
+
|
|
189
|
+
Явный `['rest']` включает REST-маршруты базы; `['graphql']` — операции GraphQL; `['rest', 'graphql']` — оба API. OpenAPI описывает REST-маршруты при наличии секции `openapi`. Корневой `api: []` недопустим; `[]` разрешён только на уровне модели.
|
|
190
|
+
|
|
191
|
+
Модель без собственного `api` наследует значение из корня. Указанный у модели массив может содержать `['rest']`, `['graphql']`, оба варианта или `[]`. Модель не может включить API, отсутствующий в корневом `api`. OpenAPI включает модели, доступные через REST. Для генерации GraphQL нужна хотя бы одна GraphQL-модель; для генерации OpenAPI с маршрутами базы — хотя бы одна REST-модель. Если база доступна только через GraphQL, OpenAPI всё равно может описывать настроенные маршруты auth и файлов. Связанные модели должны разрешать тот же API. Имена моделей должны быть допустимыми идентификаторами; конфликты типов и операций вызывают ошибку. Имена `and`, `or`, `not` зарезервированы фильтрами.
|
|
192
|
+
|
|
193
|
+
Таким образом, явный корневой `"api": ["rest", "graphql"]` включает оба API у моделей, наследующих настройку из корня. Порядок элементов массива не важен.
|
|
181
194
|
|
|
182
195
|
Например, при корневом `api: ['rest', 'graphql']` модель с `api: ['rest']` получает REST-маршруты и пути в OpenAPI без операций GraphQL, а модель с `api: ['graphql']` — операции GraphQL без REST-маршрутов и путей в OpenAPI.
|
|
183
196
|
|
|
@@ -213,7 +226,7 @@ REST без схемы создаёт ключ `id` и сохраняет про
|
|
|
213
226
|
| `minLength`, `maxLength`, `pattern` | Ограничения строк |
|
|
214
227
|
| `format` | `date`, `date-time`, `email`, `uri`, `uuid` |
|
|
215
228
|
| `minimum`, `maximum` | Включительные числовые границы |
|
|
216
|
-
| `source`, `target`, `onDelete` | Описание связи |
|
|
229
|
+
| `keyOn`, `source`, `target`, `onDelete` | Описание связи |
|
|
217
230
|
|
|
218
231
|
Ограничения строк и чисел применяются к каждому элементу `string[]`/`number[]`. `required` и `nullable` относятся ко всему массиву; `default` и `example` содержат массив целиком. Элементы массива должны соответствовать его типу и быть отличны от `null`. `required` требует наличия хранимого поля массива, но не требует элементов в нём.
|
|
219
232
|
|
|
@@ -231,26 +244,35 @@ REST без схемы создаёт ключ `id` и сохраняет про
|
|
|
231
244
|
{
|
|
232
245
|
"actors.genres": {
|
|
233
246
|
"type": "Genre[]",
|
|
247
|
+
"keyOn": "current",
|
|
234
248
|
"source": "actors.genreIds",
|
|
235
249
|
"required": true
|
|
236
250
|
}
|
|
237
251
|
}
|
|
238
252
|
```
|
|
239
253
|
|
|
240
|
-
`Genre` возвращает объект, `Genre[]` — список.
|
|
254
|
+
`Genre` возвращает объект, `Genre[]` — список. Каждую явную связь нужно объявить с обеих сторон. В обоих полях обязателен `keyOn`: `current` обозначает сторону, где хранится ключ, `related` — обратную. В паре должно быть по одному полю каждого вида. Для примера выше добавьте `Genre.movies = {"type":"Movie[]","keyOn":"related","target":"actors.genreIds"}`.
|
|
255
|
+
|
|
256
|
+
На стороне `current` поле `target` по умолчанию равно первичному ключу связанной модели. `source` выводится из имени поля связи и этого ключа: `User.country` → `User.countryId`; для списка берётся единственное число имени связи и множественное число ключа: `Genre.parents` → `Genre.parentIds`. Поэтому `Genre.parents = {"type":"Genre[]","keyOn":"current"}` и `Genre.children = {"type":"Genre[]","keyOn":"related"}` используют один `Genre.parentIds`. Вложенные поля сохраняют родительский путь: `Movie.actors.genres` → `Movie.actors.genreIds`. Нестандартный первичный ключ меняет суффикс: `Movie.publishers` при ключе `Publisher.ref` → `Movie.publisherRefs`.
|
|
241
257
|
|
|
242
|
-
|
|
258
|
+
`source` всегда указывает на поле текущей модели, а `target` — на поле модели из `type`. Для обратной связи пути меняются местами: у `Country.users` это `source: "id"` и `target: "countryId"`. Если ключ хранится под нестандартным именем, задайте путь явно. Когда между двумя моделями несколько связей, уточните обратные поля: например, `Country.birthUsers = {"type":"User[]","keyOn":"related","target":"birthCountryId"}` и `Country.residenceUsers = {"type":"User[]","keyOn":"related","target":"residenceCountryId"}`. Отсутствующая, противоречивая или неоднозначная пара вызывает ошибку при загрузке схемы.
|
|
243
259
|
|
|
244
|
-
|
|
260
|
+
Ключи связей остаются в базе, даже если их нет в `fields`. На стороне `current` поле `source` можно не объявлять отдельно, если известен тип целевого ключа: сервер выведет массив для множественной связи или одно значение для одиночной. Это действует и для первичного, и для непервичного целевого поля. Такой ключ остаётся внутренним: REST и GraphQL не позволяют читать, выбирать, фильтровать, сортировать или записывать его напрямую, а OpenAPI его не описывает. Сама связь при этом доступна. Чтобы открыть ключ в API, объявите его в `fields`, например `"countryId": { "type": "string" }`. Неоднозначное сопоставление тоже требует явного объявления.
|
|
261
|
+
|
|
262
|
+
Например, `publishers: { "type": "Publisher[]", "keyOn": "current", "target": "code" }` выводит `publisherCodes: { "type": "string[]" }`, если `Publisher.code` — строка, даже при первичном ключе `Publisher.id`. Типы явно объявленных хранимых ключей сохраняются.
|
|
263
|
+
|
|
264
|
+
Обратная связь: `User.movies = {"type":"Movie[]","keyOn":"related","target":"actors.userId"}` соответствует `Movie.actors.user = {"type":"User","keyOn":"current","source":"actors.userId"}`. Фильм возвращается один раз, даже если совпало несколько актёров. Несколько совпадений для одиночной связи — ошибка.
|
|
245
265
|
|
|
246
266
|
Каждый переданный напрямую и явно объявленный ключ связи должен указывать на существующую запись. `required: true` у одиночной связи требует целевую запись. У связи-списка с собственным массивом ключей `source` оно требует наличия этого массива, но `[]` допустим. Список без собственного массива ключей, включая обратную связь, вычисляется всегда и может быть пустым даже при `required: true`. Отсутствующая одиночная связь возвращает `null`.
|
|
247
267
|
|
|
268
|
+
Если не передать связь `related` в `PUT`, существующие связи сохранятся: их ключи хранятся в других записях. Чтобы заменить состав связи, передайте её явно; например, `"users": []` удалит всех пользователей из обратного списка страны.
|
|
269
|
+
|
|
248
270
|
`onDelete` срабатывает **при удалении целевой записи**:
|
|
249
271
|
|
|
250
272
|
- `restrict` — по умолчанию: запретить удаление, пока на цель ссылается сохраняемая запись.
|
|
251
273
|
- `cascade` — удалить ссылающуюся запись. Для `User.country` удаление страны удаляет пользователей. Для `Movie.actors.user` удаление пользователя удаляет соответствующие элементы `actors`, сохраняя фильм.
|
|
252
274
|
|
|
253
|
-
Каскадное удаление выполняется целиком, включая циклические связи. Ошибка проверки отменяет всю операцию.
|
|
275
|
+
Каскадное удаление выполняется целиком, включая циклические связи. Ошибка проверки отменяет всю операцию. Обратная связь `related` без `onDelete` сама по себе не запрещает удаление записи из-за ключа на другой стороне; задайте `onDelete`, если для неё нужно дополнительное правило.
|
|
254
276
|
|
|
255
277
|
## Пример базы данных
|
|
256
278
|
|
|
@@ -259,37 +281,31 @@ REST без схемы создаёт ключ `id` и сохраняет про
|
|
|
259
281
|
"countries": [
|
|
260
282
|
{
|
|
261
283
|
"id": "1",
|
|
262
|
-
"isArchived": false,
|
|
263
284
|
"name": "Ardenia"
|
|
264
285
|
},
|
|
265
286
|
{
|
|
266
287
|
"id": "2",
|
|
267
|
-
"isArchived": false,
|
|
268
288
|
"name": "Veloria"
|
|
269
289
|
}
|
|
270
290
|
],
|
|
271
291
|
"genres": [
|
|
272
292
|
{
|
|
273
293
|
"id": "1",
|
|
274
|
-
"isArchived": false,
|
|
275
294
|
"name": "Crime",
|
|
276
295
|
"parentIds": []
|
|
277
296
|
},
|
|
278
297
|
{
|
|
279
298
|
"id": "2",
|
|
280
|
-
"isArchived": false,
|
|
281
299
|
"name": "Gangster",
|
|
282
300
|
"parentIds": ["1"]
|
|
283
301
|
},
|
|
284
302
|
{
|
|
285
303
|
"id": "3",
|
|
286
|
-
"isArchived": false,
|
|
287
304
|
"name": "Drama",
|
|
288
305
|
"parentIds": []
|
|
289
306
|
},
|
|
290
307
|
{
|
|
291
308
|
"id": "4",
|
|
292
|
-
"isArchived": false,
|
|
293
309
|
"name": "Comedy",
|
|
294
310
|
"parentIds": []
|
|
295
311
|
}
|
|
@@ -311,7 +327,6 @@ REST без схемы создаёт ключ `id` и сохраняет про
|
|
|
311
327
|
"coverSrc": "https://example.com/covers/shadows-of-ardenia.jpg",
|
|
312
328
|
"description": "An heiress in a port city uncovers a plot involving two rival families.",
|
|
313
329
|
"id": "1",
|
|
314
|
-
"isArchived": false,
|
|
315
330
|
"publisherIds": ["2"],
|
|
316
331
|
"title": "Shadows of Ardenia"
|
|
317
332
|
},
|
|
@@ -320,7 +335,6 @@ REST без схемы создаёт ключ `id` и сохраняет про
|
|
|
320
335
|
"coverSrc": "https://example.com/covers/northern-star.jpg",
|
|
321
336
|
"description": "A night clerk at an old hotel gets drawn into the search for a missing painting.",
|
|
322
337
|
"id": "2",
|
|
323
|
-
"isArchived": false,
|
|
324
338
|
"publisherIds": ["1"],
|
|
325
339
|
"title": "Midnight at the Northern Star"
|
|
326
340
|
}
|
|
@@ -328,12 +342,10 @@ REST без схемы создаёт ключ `id` и сохраняет про
|
|
|
328
342
|
"publishers": [
|
|
329
343
|
{
|
|
330
344
|
"id": "1",
|
|
331
|
-
"isArchived": false,
|
|
332
345
|
"name": "Northlight Studio"
|
|
333
346
|
},
|
|
334
347
|
{
|
|
335
348
|
"id": "2",
|
|
336
|
-
"isArchived": false,
|
|
337
349
|
"name": "Aurora Pictures"
|
|
338
350
|
}
|
|
339
351
|
],
|
|
@@ -342,15 +354,13 @@ REST без схемы создаёт ключ `id` и сохраняет про
|
|
|
342
354
|
"bornAt": "1988-03-14",
|
|
343
355
|
"countryId": "1",
|
|
344
356
|
"fullName": "Mira Volkova",
|
|
345
|
-
"id": "1"
|
|
346
|
-
"isArchived": false
|
|
357
|
+
"id": "1"
|
|
347
358
|
},
|
|
348
359
|
{
|
|
349
360
|
"bornAt": "1991-11-02",
|
|
350
361
|
"countryId": "2",
|
|
351
362
|
"fullName": "Leon Vetrov",
|
|
352
|
-
"id": "2"
|
|
353
|
-
"isArchived": false
|
|
363
|
+
"id": "2"
|
|
354
364
|
}
|
|
355
365
|
]
|
|
356
366
|
}
|
|
@@ -488,20 +498,20 @@ Content-Type: application/json
|
|
|
488
498
|
|
|
489
499
|
| Значение в связи | Действие |
|
|
490
500
|
|---|---|
|
|
491
|
-
| Объект только с первичным ключом | Связать существующую запись без её
|
|
501
|
+
| Объект только с первичным ключом | Связать существующую запись без обновления её полей, дат и авторства |
|
|
492
502
|
| Объект с первичным ключом и другими полями | PATCH обновляет переданные поля, PUT заменяет связанную запись |
|
|
493
503
|
| Объект без первичного ключа | Создать запись со значениями по умолчанию и сгенерированным ключом |
|
|
494
504
|
|
|
495
|
-
Имя и тип ключа берутся из целевой модели. Объект только с ключом
|
|
505
|
+
Имя и тип ключа берутся из целевой модели. Объект только с этим ключом считается ссылкой в POST, PUT и PATCH; остальные обязательные поля целевой модели ему не нужны. Если добавить другие поля, это уже обновление: PUT требует все обязательные поля, удаляет пропущенные необязательные или задаёт им значения по умолчанию; POST/PATCH обновляют переданные поля. При замене сохраняются первичный ключ, генерируемые значения и поля `readOnly`. Если запись по ключу не найдена, операция завершится ошибкой. Для создания вложенной записи без ключа нужна его автоматическая генерация. Объявленные хранимые ключи, например `countryId`, тоже позволяют менять ссылки напрямую.
|
|
496
506
|
|
|
497
|
-
Переданный список заменяет состав связи. PATCH сохраняет пропущенные
|
|
507
|
+
Переданный список заменяет состав связи. PATCH сохраняет пропущенные связи. PUT заменяет доступные для записи поля текущей записи, включая её собственные ключи связей, но сохраняет пропущенные связи `keyOn: "related"`, чьи ключи хранятся в других записях. `[]` очищает список, `null` — одиночную связь с разрешённым `nullable`. Разрыв связи не удаляет связанную запись. Обязательная одиночная связь должна сохраняться; обязательная связь-список может быть пустой.
|
|
498
508
|
|
|
499
509
|
Если хранимый ключ объявлен в `fields`, в одном объекте указывайте либо связь, либо её ключ: например, `genres` или `genreIds`. Одновременная передача обоих полей возвращает `400 INVALID_INPUT`, даже если наборы ключей совпадают:
|
|
500
510
|
|
|
501
511
|
```json
|
|
502
512
|
{
|
|
503
513
|
"genreIds": ["1", "2"],
|
|
504
|
-
"genres": ["1", "2"]
|
|
514
|
+
"genres": [{ "id": "1" }, { "id": "2" }]
|
|
505
515
|
}
|
|
506
516
|
```
|
|
507
517
|
|
|
@@ -509,7 +519,7 @@ Content-Type: application/json
|
|
|
509
519
|
|
|
510
520
|
Все вложенные изменения входят в транзакцию основной записи. Ошибка проверки, отсутствующая запись или неверный выбор полей ответа отменяет всю операцию. Изменения общей записи видны всем, кто с ней связан.
|
|
511
521
|
|
|
512
|
-
GraphQL принимает в полях связей типизированные объекты. Чтобы при
|
|
522
|
+
GraphQL принимает в полях связей типизированные объекты. Чтобы изменить только ссылки, в том числе при замене, передайте объекты только с первичными ключами либо объявленные поля ключей, например `genreIds`. Обязательные поля вложенного создания или замены с дополнительными полями проверяются при выполнении. Пример:
|
|
513
523
|
|
|
514
524
|
```graphql
|
|
515
525
|
mutation {
|
|
@@ -753,7 +763,7 @@ query {
|
|
|
753
763
|
|
|
754
764
|
Служебные сведения восстановления хранятся вместе с записями и переживают перезапуск; сохраняйте их при резервном копировании базы. Внутреннее поле `djsDeletion` зарезервировано и не выдаётся через API.
|
|
755
765
|
|
|
756
|
-
При включённом auth создавать записи может любой вошедший пользователь. Изменять, удалять и восстанавливать — владелец по `createdById` или пользователь с `isAdmin: true`. Записи без владельца изменяет только администратор. Его правки не меняют владельца. Правила действуют на вложенные записи, изменение ключей связей, каскады и восстановление. Ссылка
|
|
766
|
+
При включённом auth создавать записи может любой вошедший пользователь. Изменять, удалять и восстанавливать — владелец по `createdById` или пользователь с `isAdmin: true`. Записи без владельца изменяет только администратор. Его правки не меняют владельца. Правила действуют на вложенные записи, изменение ключей связей, каскады и восстановление. Ссылка через объект только с первичным ключом или объявленный хранимый ключ не требует владения неизменяемой целевой записью. Вложенный объект с дополнительными полями требует права изменять её. При изменении обратных связей нужны права на каждую запись, чей хранимый ключ меняется. Каждая мутация атомарна: отказ сохраняет прежнее состояние всех затронутых записей.
|
|
757
767
|
|
|
758
768
|
REST возвращает 401 при отсутствии действительного токена и 403 при недостатке прав. GraphQL проверяет мутации по тем же правилам и возвращает коды `UNAUTHENTICATED` или `FORBIDDEN`; токен получают через REST-вход и передают в `Authorization: Bearer <token>`. GraphQL-запросы чтения, OPTIONS и все операции с файлами остаются открытыми.
|
|
759
769
|
|
|
@@ -832,7 +842,7 @@ Content-Type: application/json
|
|
|
832
842
|
|
|
833
843
|
При хранении на диске бинарный файл находится по пути `<files.source>/<directory>/<name>`. Метаданные по умолчанию хранятся в `<files.source>/.files.json`; другой путь можно задать через `files.metadata`. Директории и файл метаданных создаются по мере необходимости.
|
|
834
844
|
|
|
835
|
-
Используйте один процесс сервера для дисковой базы и файлового хранилища. Второй сервер с тем же файлом базы не запустится; закрытие первого освобождает блокировку, а после аварийного завершения она снимается при следующем запуске. Перед ручным изменением файлов или метаданных остановите сервер. Пути
|
|
845
|
+
Используйте один процесс сервера для дисковой базы и файлового хранилища. Второй сервер с тем же файлом базы не запустится; закрытие первого освобождает блокировку, а после аварийного завершения она снимается при следующем запуске. Перед ручным изменением файлов или метаданных остановите сервер. Пути внутри каталога файлового хранилища не могут содержать символические ссылки. Загрузка и переименование не могут перезаписать базу, её блокировку, счётчики, схему, учётные записи auth, загруженную конфигурацию или файл метаданных.
|
|
836
846
|
|
|
837
847
|
Отправляйте файл как бинарное тело запроса. В браузере для этого можно использовать `xhr.send(file)`, а прогресс отслеживать через `XMLHttpRequest.upload.onprogress`. Максимальный размер по умолчанию равен 100 МиБ и настраивается через `server.maxFileSize`. Отсутствующие или небезопасные заголовки и пути возвращают `400`, превышение лимита — `413`, а отсутствующий, некорректный или не поддерживаемый Fastify `Content-Type` — `400` либо `415` в зависимости от этапа проверки.
|
|
838
848
|
|
|
@@ -3,4 +3,4 @@ export declare const DEFAULT_MAX_FILE_SIZE: number;
|
|
|
3
3
|
export declare const DEFAULT_MAX_PAGE_SIZE = 100;
|
|
4
4
|
export declare const DEFAULT_PAGE_SIZE = 10;
|
|
5
5
|
export declare const DEFAULT_PORT = 4001;
|
|
6
|
-
export declare const VERSION = "1.0.0-rc.
|
|
6
|
+
export declare const VERSION = "1.0.0-rc.6";
|
|
@@ -3,5 +3,5 @@ export const DEFAULT_MAX_FILE_SIZE = 100 * 1024 * 1024;
|
|
|
3
3
|
export const DEFAULT_MAX_PAGE_SIZE = 100;
|
|
4
4
|
export const DEFAULT_PAGE_SIZE = 10;
|
|
5
5
|
export const DEFAULT_PORT = 4001;
|
|
6
|
-
export const VERSION = '1.0.0-rc.
|
|
6
|
+
export const VERSION = '1.0.0-rc.6';
|
|
7
7
|
//# sourceMappingURL=constants.js.map
|
package/dist/src/core/engine.js
CHANGED
|
@@ -200,6 +200,10 @@ export class Engine {
|
|
|
200
200
|
if (node.virtual)
|
|
201
201
|
continue;
|
|
202
202
|
if (node.relation) {
|
|
203
|
+
// A paired inverse view does not own a storage key. Its default must not block
|
|
204
|
+
// deletion of a record that merely appears in that view.
|
|
205
|
+
if (node.keyOn === 'related' && node.onDelete === undefined)
|
|
206
|
+
continue;
|
|
203
207
|
const owner = { ref, node, targets: related(ref, node) };
|
|
204
208
|
owners.push(owner);
|
|
205
209
|
for (const target of owner.targets) {
|