@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/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0-rc.8
4
+
5
+ - Persist hashed auth sessions with users in file storage so valid tokens survive restarts, with atomic logout and password-change revocation.
6
+ - Replace the lock directory with one readable `<source-name>-lock.json` file and native OS locking. Lock the database, auth store, upload root and file metadata; recover after crashes and reject competing writers.
7
+ - Protect configured files and their symlink targets from upload, deletion and schema export. Reject malformed Unicode file paths before changing storage.
8
+ - Preserve protected nested fields when a nullable ancestor is cleared, and infer relations across numeric seed IDs and generated string IDs.
9
+ - Use `key` for the primary-key argument of GraphQL update/replace mutations when the primary field is named `data`.
10
+ - Bound REST and GraphQL selection, expansion, query work and response size; document that GraphQL result errors do not undo committed mutations.
11
+ - Derive the CLI version from package metadata and verify the installed CLI over HTTP, including paths with spaces and Unicode and persistence after a crash.
12
+ - Run type checks, lint, coverage and installed-package checks on Linux, Windows and macOS with Node.js 22, 24 and 26. Publish only after the full matrix succeeds for the selected commit.
13
+
14
+ When upgrading from an earlier RC, stop all old server processes before starting RC8: the lock protocol has changed. Existing auth files without `sessions` remain valid. Regenerate GraphQL clients if a model uses `data` as its primary field.
15
+
16
+ ## 1.0.0-rc.7
17
+
18
+ - Treat nested objects containing only a primary key as references in POST, PUT and PATCH, preserving the target's fields and audit values. Actual changes, including inverse key changes, still require ownership.
19
+ - Allow key-only references in GraphQL replace inputs and OpenAPI request schemas; validate required fields when a nested object creates or changes a record.
20
+ - Infer array storage keys for `keyOn: "current"` list relations targeting non-primary fields, preserving explicitly declared key types.
21
+ - Expand the migration guide from 0.9.0 in English and Russian, document API defaults, and remove the archive flag from examples.
22
+
3
23
  ## 1.0.0-rc.6
4
24
 
5
25
  - Require both sides of every explicit model relation and `keyOn: "current" | "related"` to identify where its key is stored.
package/MIGRATION.md CHANGED
@@ -1,49 +1,211 @@
1
- # Migrating from 0.9.0 to 1.0.0-rc.6
1
+ # Migrating from 0.9.0 to 1.0.0-rc.8
2
2
 
3
- Version 1.0 changes configuration and request syntax. Update the server configuration and client requests together. Back up any file database, auth records, and file metadata before changing the running server.
3
+ [Русский](MIGRATION.ru.md) · [Current README](README.md)
4
4
 
5
- ## Configuration
5
+ This guide compares `v0.9.0` with `1.0.0-rc.8` and also covers the relation changes since RC6. Update configuration and clients together. Node.js 22 or newer is still required. Back up the database, config, schema, uploaded files and metadata before migrating.
6
6
 
7
- Declare a storage mode and use `source` for each enabled component:
7
+ ## 1. Choose whether you need a schema
8
8
 
9
- | 0.9.0 | 1.0.0-rc.6 |
9
+ For REST without schema validation or exports, the smallest configuration change is:
10
+
11
+ ```js
12
+ // 0.9.0
13
+ export default { database: { path: './database.json' } };
14
+ ```
15
+
16
+ ```js
17
+ // 1.0.0-rc.8
18
+ export default { storage: 'file', database: { source: './database.json' } };
19
+ ```
20
+
21
+ This keeps schemaless REST and relations inferred from stored `...Id` / `...Ids` names. The query and response changes below still apply. OpenAPI and GraphQL now require an explicit model schema; generators no longer infer field definitions from database records.
22
+
23
+ ## 2. Convert configuration
24
+
25
+ | 0.9.0 | 1.0.0-rc.8 |
10
26
  |---|---|
11
- | `database.path` | `storage: 'file'`, `database.source` |
12
- | `database.data` | `storage: 'memory'`, `database.source` |
13
- | `files.directory` | `files.source` in file mode |
14
- | `files.data` | `files.source` in memory mode |
27
+ | `database.path` | `storage: 'file'` and `database.source` |
28
+ | `database.data` | `storage: 'memory'` and `database.source` |
29
+ | `database.schema` with `$info` / `$schema` | Same config key, new schema format with `models` |
30
+ | `files.directory` / `files.data` | `files.source` |
31
+ | `files.metadata` | Keep the existing path to preserve file metadata |
15
32
  | `openapi.path` | `openapi.target` |
33
+ | Schema `$info` | `package.source`: `name`, `version`, optional `description` |
34
+ | Default `server.maxPageSize: 1000` | Default `100`; set `1000` explicitly to retain the old limit |
35
+
36
+ `storage` is required and applies to every source, including the schema and package metadata. File mode requires paths; memory mode requires objects/arrays. Mixed file and memory components are no longer supported. Export `target` values remain file paths in either mode.
37
+
38
+ A file-based configuration that exports OpenAPI becomes:
39
+
40
+ ```js
41
+ export default {
42
+ storage: 'file',
43
+ database: { source: './database.json', schema: './schema.json' },
44
+ files: { source: './files', metadata: './files/_database.json' },
45
+ openapi: { target: './generated/openapi.yaml' },
46
+ package: { source: './package.json' },
47
+ server: { maxPageSize: 1000 },
48
+ };
49
+ ```
50
+
51
+ Create `schema.json` as described below. `package.source` is required when `openapi` is configured: package `name` becomes OpenAPI `info.title`, with `version` and optional `description`. Other old `$info` properties have no configuration equivalent. In memory mode, use `package: { source: { name: 'example-api', version: '1.0.0' } }`.
52
+
53
+ Keep an existing `files.metadata` path explicitly. Omitting it selects `<files.source>/.files.json`, which does not import an old `_database.json`. The metadata format from 0.9.0 and the `/_files/*` routes remain compatible; raw upload headers and `DELETE` returning `204` also remain unchanged.
54
+
55
+ Relative paths still resolve from the config directory in the CLI, and from the working directory in `createServer(config)`.
56
+
57
+ ## 3. Replace schema overrides with complete models
58
+
59
+ In 0.9.0, `$schema` supplemented field types inferred from data. It was not the current model/relation format. Each collection now needs a model with `collection`, `fields`, and exactly one top-level primary key of type `string` or `number`.
60
+
61
+ | Old override | New definition |
62
+ |---|---|
63
+ | `$schema.users.name` | Model name under `models`, for example `User` |
64
+ | `$schema.users.required: ['fullName']` | `fields.fullName.required: true` |
65
+ | `$schema.users.formats.bornAt: 'date'` | `fields.bornAt: { type: 'string', format: 'date' }` |
66
+ | Recursive `properties` and `items` | Typed fields, `[]` for arrays, dotted paths for children |
67
+ | Inferred generated string `id` | Explicit `{ type: 'string', primary: true, generated: 'uuid' }` |
68
+
69
+ Define every stored field, not just former overrides. For example, declare `actors` as `object[]` and `actors.fullName` as `string`. Additional fields and collections are rejected with an explicit schema, except inferred relation storage keys. Existing records are validated when the HTTP server initializes.
70
+
71
+ There is no general replacement for mixed-type `oneOf` fields or an `integer`-only field constraint. Normalize those fields to a supported type before adopting the schema, or keep schemaless REST. Model names must be identifiers; old custom component names containing dots or hyphens must change.
72
+
73
+ ### Complete example with a paired relation
74
+
75
+ For existing users containing `id`, `fullName`, and `countryId`, and countries containing `id` and `name`:
76
+
77
+ ```json
78
+ {
79
+ "api": ["rest"],
80
+ "models": {
81
+ "User": {
82
+ "collection": "users",
83
+ "fields": {
84
+ "id": { "type": "string", "primary": true, "generated": "uuid" },
85
+ "fullName": { "type": "string", "required": true },
86
+ "countryId": { "type": "string" },
87
+ "country": { "type": "Country", "keyOn": "current" }
88
+ }
89
+ },
90
+ "Country": {
91
+ "collection": "countries",
92
+ "fields": {
93
+ "id": { "type": "string", "primary": true, "generated": "uuid" },
94
+ "name": { "type": "string", "required": true },
95
+ "users": { "type": "User[]", "keyOn": "related" }
96
+ }
97
+ }
98
+ }
99
+ }
100
+ ```
101
+
102
+ Every explicit relation needs both declarations and a `keyOn` value. `current` stores the foreign key; `related` reads that key in the other model. No second copy of the key or stored `users` array is needed.
16
103
 
17
- The `storage` mode applies to the database, schema, auth records, files, and package metadata. GraphQL and OpenAPI require a model schema; OpenAPI also requires `package.source`. The schema format has changed: define models under `models`, with a `collection`, fields, and one primary key per model. Use root `api` to enable REST, GraphQL, or both for the database; a model's optional `api` array can narrow that choice. Start with the [current schema example](examples/schema.json), then validate your existing records against it. The [configuration example](examples/server.config.js) shows all required paths.
104
+ - Direct `target` defaults to the related model's primary key. Direct `source` uses the relation field name and target key: `country` → `countryId`, `publishers` → `publisherIds`. A custom primary key `ref` produces `publisherRefs`.
105
+ - Inverse paths swap the direct paths. For `Country.users`, `source` is `id` and `target` is `countryId`.
106
+ - Self-relations pair `Genre.parents: { type: 'Genre[]', keyOn: 'current' }` with `Genre.children: { type: 'Genre[]', keyOn: 'related' }`; both use stored `Genre.parentIds`.
107
+ - Multiple relations between the same models require disambiguation: inverse `Country.birthUsers` uses `target: 'birthCountryId'`, and `Country.residenceUsers` uses `target: 'residenceCountryId'`. Ambiguous or missing pairs fail schema loading.
108
+ - Declare existing keys explicitly, as above, if clients read, filter, sort or write them. Inferred keys remain in storage but are hidden from both APIs and OpenAPI, even when selected by name.
109
+ - RC7 also infers source arrays for lists targeting **non-primary** fields. For example, `publishers` with `keyOn: 'current'` and `target: 'code'` infers `publisherCodes: { type: 'string[]' }` when `Publisher.code` is a string. Explicitly declare the key if it must be exposed in the API. If RC6 stored this automatically inferred key as a scalar, convert existing values to arrays, for example `"A"` to `["A"]`, before upgrading. An explicitly declared scalar key retains its type.
18
110
 
19
- ## Relation declarations
111
+ Repair dangling direct references before startup. Writes now validate references; deleting a referenced target is restricted by default. Choose `onDelete: 'cascade'` deliberately where deleting a target should delete referring records. An inverse with no explicit `onDelete` does not add its own restriction.
20
112
 
21
- Current schemas require each explicit relation in both models and `keyOn` on each relation field. Use `current` where the stored key lives and `related` on the inverse. For example, `User.country = {"type":"Country","keyOn":"current"}` pairs with `Country.users = {"type":"User[]","keyOn":"related"}`. The server infers `User.countryId` referencing the primary key of `Country`, even if `countryId` is omitted from `fields`; existing records still keep their stored keys.
113
+ ### Preserve IDs and custom fields
22
114
 
23
- Previously, a relation could be declared on only one side, and `source` defaulted to the current model's primary key. Add the inverse declaration and review any relation that relied on that default. A list such as `Genre.parents` now infers `parentIds` from the relation name, while `Genre.children` with `keyOn: "related"` reads the same key. Keep explicit `source` or `target` for custom key paths. If multiple relations connect the same two models, specify the inverse `target` to pair each one unambiguously. Missing or ambiguous pairs now fail during schema loading.
115
+ Do not regenerate IDs while converting the schema. Existing string IDs remain valid with `generated: 'uuid'`; only new values use UUIDs. If a collection mixes numeric and string IDs, normalize them and all referencing keys to one type before enabling an explicit schema.
24
116
 
25
- Omitting a `related` inverse field from `PUT` leaves its links unchanged, because their keys are stored in other records. Supply the inverse field explicitly when replacing those links, for example `"users": []` to detach all users from a country.
117
+ `generated` is not implied by `primary`. Without it, clients must supply the primary key on create. With it, clients must omit generated fields; sending them is rejected instead of ignored as in 0.9.0.
26
118
 
27
- ## CLI
119
+ Old application fields must be declared or deliberately removed from copied data. A custom archive flag does not automatically become soft deletion. Enabling lifecycle or auth features reserves their system field names, so rename conflicting application fields first.
28
120
 
29
- Adding a `files`, `graphql`, or `openapi` section enables that feature. The old `--files`, `--openapi`, and `--openapi-only` flags are gone. Use `--generate` to export configured schemas and start the server, or `--generate-only` to export and exit. Each exported format needs a `target` path.
121
+ ## 4. Select APIs and update startup commands
30
122
 
31
- ## REST queries
123
+ If root `api` is omitted, the server uses `['rest']`, or `['rest', 'graphql']` when `graphql` is configured. To expose the database only through GraphQL, set `api: ['graphql']` and add `graphql: {}`. A model inherits root `api` or narrows it; model `api: []` hides that model. Root `api: []` is invalid. Related models must enable the same APIs.
32
124
 
33
- REST now accepts a JSON `scope` query parameter. Replace `_where`, `_sort`, `_page`, `_perPage`, `_embed`, and simple field filters with `scope`. For example:
125
+ The `graphql` section and root GraphQL flag must be enabled together. Auth and files remain REST services and appear only in OpenAPI. OpenAPI can describe these services on a GraphQL-only database server.
126
+
127
+ | Old CLI | New CLI |
128
+ |---|---|
129
+ | `--files` | Remove the flag; the `files` section enables routes |
130
+ | `--openapi` | `--generate` |
131
+ | `--openapi-only` | `--generate-only` |
132
+
133
+ Unlike the old CLI, normal startup now enables configured file routes. Remove `files` if they should be disabled. `openapi: {}` serves `/openapi.json`; `graphql: {}` serves `/graphql`. To export files, set a `target` for **each** configured format; generation flags export all configured formats. Normal startup does not write exports.
134
+
135
+ ## 5. Migrate REST queries and response readers
136
+
137
+ The only record query parameter is now URL-encoded JSON `scope: [fields, arguments?]`.
138
+
139
+ | 0.9.0 parameter | New scope entry |
140
+ |---|---|
141
+ | `_where` or simple `fullName:contains=mi` | Second object: `where: { fullName: { contains: 'mi' } }` |
142
+ | `_sort=-id,fullName` | `order: [{ field: 'id', direction: 'DESC' }, { field: 'fullName', direction: 'ASC' }]` |
143
+ | `_page=1&_perPage=10` | `pager: { page: 1, pageSize: 10 }` |
144
+ | `_embed=country` | First object: `country: [{ id: true, name: true }]` |
145
+
146
+ Example against the paired schema above:
34
147
 
35
148
  ```js
36
149
  const scope = [
37
- { id: true, title: true, publishers: [{ id: true, name: true }] },
38
- { where: { title: { contains: 'Ardenia' } }, order: [{ field: 'title', direction: 'ASC' }], pager: { page: 1, pageSize: 10 } },
150
+ { id: true, fullName: true, countryId: true, country: [{ id: true, name: true }] },
151
+ {
152
+ where: { fullName: { contains: 'mi' } },
153
+ order: [{ field: 'fullName', direction: 'ASC' }],
154
+ pager: { page: 1, pageSize: 10 },
155
+ },
39
156
  ];
40
- const url = `/movies?${new URLSearchParams({ scope: JSON.stringify(scope) })}`;
157
+ const url = `/users?${new URLSearchParams({ scope: JSON.stringify(scope) })}`;
41
158
  ```
42
159
 
43
- Without an explicit `scope`, the response contains scalar fields only. `"*": true` also selects only scalar fields that do not store relation keys. Select arrays, objects, relations, and declared relation keys explicitly. Relation keys omitted from `fields` remain in the database but are unavailable in REST, GraphQL, and OpenAPI. Related lists can have their own filters, order, and pagination. Review clients that expect embedded relations or relation keys in default responses.
160
+ Update response readers as well:
161
+
162
+ - Missing `scope` and `{ '*': true }` select only scalars that do not store relation keys. Arrays, objects, relations and declared storage keys need explicit selection. `writeOnly` fields cannot be selected.
163
+ - Collections still return `{ data, total }`. Embedded **object and relation lists now also return `{ data, total }`**, rather than plain arrays. Primitive arrays remain arrays. A single relation returns an object or `null`.
164
+ - Every object list is paginated, including nested lists: default page size `10`, maximum `100`. Embedded lists were previously returned in full. Give each list the necessary `pager` and load further pages when needed.
165
+ - Relation filters work without selecting that relation. Sorting the parent by a related field, such as old `_sort=country.name`, is not supported; sort the related list with its own `order`, or store a sortable value on the parent.
166
+ - String comparisons `gt/gte/lt/lte` now use case-insensitive, numeric-aware ordering. Review clients relying on the old lexical comparisons.
167
+ - Scope JSON is limited to 10,000 characters and 32 selection levels. Unknown fields/operators are rejected even on empty data.
168
+
169
+ ## 6. Check write behavior and optional auth
170
+
171
+ `PATCH` merges top-level fields; a supplied nested plain object replaces that object while preserving protected fields. `PUT` replaces writable fields, preserving primary, generated, read-only and system values. Required fields must be supplied for creation/replacement unless they have defaults.
172
+
173
+ Relation values are objects, for example `country: { id: '1' }`, not scalar IDs. **Starting with RC7, an object containing only the primary key is a reference in POST, PUT and PATCH.** It preserves the target's fields, dates and author values. Adding other fields invokes an update: PUT replaces the target and requires its mandatory fields; POST/PATCH update supplied fields. Changing an inverse relation still requires permission for every record whose stored key changes. Declared storage keys, such as `countryId`, can also change links directly. Do not send the relation and its storage key in the same object.
174
+
175
+ **When upgrading from RC6:** key-only objects no longer update the target, change its audit values or require ownership of an unchanged target. If an existing client intends to update or replace the target, include the fields to change. Declared storage keys remain available for link-only writes.
176
+
177
+ Omitted inverse (`keyOn: 'related'`) relations survive PUT. Explicit `users: []` changes users' foreign keys to detach them; it can fail if those keys are required or the caller lacks permission. Nested changes are atomic, and modifying a shared target affects every record referring to it.
178
+
179
+ Auth, timestamps and soft deletion are new, optional features. To first preserve unauthenticated writes and physical deletion, omit `auth` and leave `timestamps` / `softDelete` disabled (their defaults).
180
+
181
+ If enabling auth, create a separate auth user store and migrate ownership deliberately. Old records without `createdById` can be changed only by administrators. The ownership field is server-managed through the API; prepare ownership in the copied database before startup if ordinary users must edit existing records. Auth does not close read endpoints or file operations. In RC8, file storage persists hashed sessions in the auth user file and restores unexpired sessions after a restart. Existing users without `sessions` remain valid. When manually changing a password hash, also clear that user's `sessions`.
182
+
183
+ With `softDelete`, DELETE retains records, lists hide deleted records by default, direct primary-key reads can retrieve them, and successful PUT/PATCH restores them. There is no automatic conversion of an old application archive flag. See the [lifecycle rules](README.md#record-dates-deletion-and-ownership) before enabling this feature.
184
+
185
+ ## 7. Update programmatic consumers
186
+
187
+ `createServer(config)` now takes exactly one argument. Remove the old second `{ files: false }` argument and omit `files` from the config to disable the feature.
188
+
189
+ `facade.openapi()` and `facade.graphql()` return documents without writing `target`. They require their respective config sections; OpenAPI also needs package metadata and a schema. Use CLI generation flags or explicit writers:
190
+
191
+ ```js
192
+ import { createServer } from '@kollors/deep-json-server';
193
+ import { writeOpenapi } from '@kollors/deep-json-server/openapi';
194
+ import config from './server.config.js';
195
+
196
+ const facade = await createServer(config);
197
+ await writeOpenapi(await facade.openapi(), './generated/openapi.yaml');
198
+ const app = facade.fastify();
199
+ await app.ready(); // Initializes and validates the database without opening a port.
200
+ await app.close();
201
+ ```
44
202
 
45
- ## Records and auth
203
+ Schema generation alone does not validate existing database records. Call `ready()`, `inject()` or `listen()` to initialize HTTP and check data. Regenerate clients from the new OpenAPI/GraphQL documents after changing models and queries.
46
204
 
47
- Explicit models validate fields and relations more strictly than the 0.9 schemaless server. Existing records with undeclared fields or dangling direct relation keys may fail validation at startup or on write. Test the migrated database before replacing the production file.
205
+ ## 8. Validate the cutover and keep rollback data
48
206
 
49
- Authentication is optional and enabled by an `auth` section. When enabled, creating records requires login; changing existing records requires ownership or administrator access. Record timestamps and soft deletion are configured in the model schema. The [README](README.md) describes the current behavior and API endpoints.
207
+ 1. Pin the chosen RC version and migrate a **copy** of the 0.9.0 files. Run generation, then initialize the HTTP server to validate data too.
208
+ 2. Check representative reads: defaults, explicit foreign keys, nested lists, filters and pagination. Check create, PATCH, PUT, link changes and deletion using disposable copied records.
209
+ 3. If enabling auth, test an owner, an administrator and an ordinary non-owner against old records. Confirm uploaded files remain accessible through the retained metadata path.
210
+ 4. Stop every old server process before switching: RC8 replaces the old lock directory with native OS locks and readable `<source-name>-lock.json` files for the database, auth, upload root and file metadata. Old and new lock protocols do not coordinate. Run one process per writable store and stop it before manual edits.
211
+ 5. Keep the original package version, config, schema, data and file metadata together for rollback. Restore them together; do not point 0.9.0 at data already modified by the new server. Include new increment counters (`<database>.counters.json`) and soft-delete restoration metadata in subsequent backups when used.
@@ -0,0 +1,211 @@
1
+ # Переход с 0.9.0 на 1.0.0-rc.8
2
+
3
+ [English](MIGRATION.md) · [Актуальный README](README.ru.md)
4
+
5
+ Руководство сравнивает релиз `v0.9.0` с `1.0.0-rc.8` и также описывает изменения связей после RC6. Конфигурацию и клиентов нужно обновлять вместе. Требование Node.js 22 или новее сохраняется. Перед переходом скопируйте базу, конфигурацию, схему, загруженные файлы и их метаданные.
6
+
7
+ ## 1. Выберите режим со схемой или без неё
8
+
9
+ Для REST без проверки по схеме и без генерации достаточно начать с такой замены:
10
+
11
+ ```js
12
+ // 0.9.0
13
+ export default { database: { path: './database.json' } };
14
+ ```
15
+
16
+ ```js
17
+ // 1.0.0-rc.8
18
+ export default { storage: 'file', database: { source: './database.json' } };
19
+ ```
20
+
21
+ REST без схемы и связи по именам хранимых полей `...Id` / `...Ids` сохраняются. Описанные ниже изменения запросов и ответов действуют и в этом режиме. Для OpenAPI и GraphQL теперь нужна явная схема моделей: генераторы больше не определяют поля по содержимому базы.
22
+
23
+ ## 2. Обновите конфигурацию
24
+
25
+ | В 0.9.0 | В 1.0.0-rc.8 |
26
+ |---|---|
27
+ | `database.path` | `storage: 'file'` и `database.source` |
28
+ | `database.data` | `storage: 'memory'` и `database.source` |
29
+ | `database.schema` с `$info` / `$schema` | То же поле конфигурации, новый формат схемы с `models` |
30
+ | `files.directory` / `files.data` | `files.source` |
31
+ | `files.metadata` | Сохраните прежний путь к метаданным файлов |
32
+ | `openapi.path` | `openapi.target` |
33
+ | `$info` в схеме | `package.source`: `name`, `version`, необязательный `description` |
34
+ | `server.maxPageSize` по умолчанию `1000` | Теперь `100`; для прежнего ограничения задайте `1000` явно |
35
+
36
+ Обязательный `storage` действует на все источники, включая схему и метаданные пакета. В режиме `file` нужны пути, в `memory` — объекты и массивы. Смешивать дисковые источники и данные в памяти больше нельзя. Пути экспорта `target` остаются путями к файлам в обоих режимах.
37
+
38
+ Пример конфигурации с дисковой базой и экспортом OpenAPI:
39
+
40
+ ```js
41
+ export default {
42
+ storage: 'file',
43
+ database: { source: './database.json', schema: './schema.json' },
44
+ files: { source: './files', metadata: './files/_database.json' },
45
+ openapi: { target: './generated/openapi.yaml' },
46
+ package: { source: './package.json' },
47
+ server: { maxPageSize: 1000 },
48
+ };
49
+ ```
50
+
51
+ Создайте `schema.json` по следующему разделу. При настроенном `openapi` обязателен `package.source`: `name` пакета становится `info.title`, также используются `version` и необязательный `description`. Остальные свойства старого `$info` не имеют аналога в конфигурации. В памяти передавайте `package: { source: { name: 'example-api', version: '1.0.0' } }`.
52
+
53
+ **Сохраните прежний `files.metadata` явно.** Без него сервер выберет `<files.source>/.files.json` и не импортирует старый `_database.json`. Формат метаданных из 0.9.0 и маршруты `/_files/*` совместимы; заголовки загрузки бинарного тела и ответ `204` на удаление файла сохраняются.
54
+
55
+ Относительные пути по-прежнему отсчитываются от каталога конфигурации в CLI и от рабочего каталога при прямом вызове `createServer(config)`.
56
+
57
+ ## 3. Замените дополнения к схеме полными моделями
58
+
59
+ В 0.9.0 `$schema` дополнял поля, определённые по данным. Это ещё не был нынешний формат моделей и связей. Теперь каждой коллекции нужна модель с `collection`, `fields` и одним первичным ключом верхнего уровня типа `string` или `number`.
60
+
61
+ | Старое описание | Новое описание |
62
+ |---|---|
63
+ | `$schema.users.name` | Имя модели в `models`, например `User` |
64
+ | `$schema.users.required: ['fullName']` | `fields.fullName.required: true` |
65
+ | `$schema.users.formats.bornAt: 'date'` | `fields.bornAt: { type: 'string', format: 'date' }` |
66
+ | Вложенные `properties` и `items` | Поля с типами, `[]` для массивов, пути через точку для дочерних полей |
67
+ | Автоматический строковый `id` | Явный `{ type: 'string', primary: true, generated: 'uuid' }` |
68
+
69
+ Опишите все хранимые поля, а не только прежние дополнения. Например, `actors` получает тип `object[]`, а `actors.fullName` — `string`. При явной схеме неизвестные поля и коллекции запрещены; исключение — автоматически выведенные ключи связей. Существующие записи проверяются при инициализации HTTP-сервера.
70
+
71
+ У полей со смешанными типами `oneOf` и ограничения «только целые числа» через `integer` нет общего прямого аналога. Приведите данные к поддерживаемому типу или оставьте REST без схемы. Имена моделей должны быть идентификаторами: старые имена компонентов с точками и дефисами нужно заменить.
72
+
73
+ ### Полный пример с двусторонней связью
74
+
75
+ Для пользователей с полями `id`, `fullName`, `countryId` и стран с `id`, `name`:
76
+
77
+ ```json
78
+ {
79
+ "api": ["rest"],
80
+ "models": {
81
+ "User": {
82
+ "collection": "users",
83
+ "fields": {
84
+ "id": { "type": "string", "primary": true, "generated": "uuid" },
85
+ "fullName": { "type": "string", "required": true },
86
+ "countryId": { "type": "string" },
87
+ "country": { "type": "Country", "keyOn": "current" }
88
+ }
89
+ },
90
+ "Country": {
91
+ "collection": "countries",
92
+ "fields": {
93
+ "id": { "type": "string", "primary": true, "generated": "uuid" },
94
+ "name": { "type": "string", "required": true },
95
+ "users": { "type": "User[]", "keyOn": "related" }
96
+ }
97
+ }
98
+ }
99
+ }
100
+ ```
101
+
102
+ Каждая явная связь описывается с обеих сторон, везде обязателен `keyOn`. Сторона `current` хранит внешний ключ; `related` использует этот ключ в другой модели. Второй экземпляр ключа или хранимый массив `users` не нужен.
103
+
104
+ - У прямой связи `target` по умолчанию равен первичному ключу целевой модели. `source` выводится из имени поля связи и целевого ключа: `country` → `countryId`, `publishers` → `publisherIds`. При первичном ключе `ref` получится `publisherRefs`.
105
+ - У обратной связи пути меняются местами: для `Country.users` это `source: 'id'` и `target: 'countryId'`.
106
+ - Связь модели с собой: `Genre.parents: { type: 'Genre[]', keyOn: 'current' }` и `Genre.children: { type: 'Genre[]', keyOn: 'related' }` используют один хранимый массив `Genre.parentIds`.
107
+ - Если между моделями несколько связей, укажите, какие поля образуют пары: у обратной `Country.birthUsers` задайте `target: 'birthCountryId'`, у `Country.residenceUsers` — `target: 'residenceCountryId'`. Неоднозначные и отсутствующие пары отклоняются при загрузке.
108
+ - Объявляйте существующие ключи в `fields`, как в примере, если клиент их читает, фильтрует, сортирует или записывает. Автоматически выведенные ключи остаются в базе, но скрыты из обоих API и OpenAPI; выбрать их даже по имени нельзя.
109
+ - RC7 выводит массив источника и для списка с **непервичным** целевым полем. Например, `publishers` с `keyOn: 'current'` и `target: 'code'` получает `publisherCodes: { type: 'string[]' }`, если `Publisher.code` — строка. Объявите ключ явно, если его нужно открыть в API. Если RC6 сохранил такой автоматически выведенный ключ скаляром, перед обновлением преобразуйте значения в массивы, например `"A"` в `["A"]`. Явно объявленный скалярный ключ сохраняет свой тип.
110
+
111
+ До запуска исправьте ссылки на отсутствующие записи. Теперь запись проверяет целостность связей, а удаление целевой записи по умолчанию запрещено, пока на неё ссылаются. Задавайте `onDelete: 'cascade'` только там, где удаление цели должно удалять ссылающиеся записи. Обратная связь без явно указанного `onDelete` не добавляет собственного запрета.
112
+
113
+ ### Сохраните идентификаторы и пользовательские поля
114
+
115
+ Не генерируйте идентификаторы заново. Старые строковые ID допустимы с `generated: 'uuid'`; UUID получают только новые значения. Если в коллекции есть и числа, и строки в качестве ID, перед подключением явной схемы приведите их и все ссылки к одному типу.
116
+
117
+ `primary` не включает `generated` автоматически. Без `generated` клиент сам передаёт ключ при создании. С `generated` клиент должен пропускать генерируемые поля: теперь их передача отклоняется, а не игнорируется, как в 0.9.0.
118
+
119
+ Старые поля приложения нужно объявить или осознанно удалить из копии базы. Пользовательский флаг архивирования не превращается в мягкое удаление автоматически. При включении дат, мягкого удаления или auth соответствующие служебные имена резервируются; переименуйте конфликтующие поля приложения заранее.
120
+
121
+ ## 4. Выберите API и обновите команды запуска
122
+
123
+ Без корневого `api` сервер использует `['rest']`, а с секцией `graphql` — `['rest', 'graphql']`. Для доступа к базе только через GraphQL задайте `api: ['graphql']` и добавьте `graphql: {}`. Модель наследует корневой выбор или сужает его; `api: []` у модели скрывает её. Корневой `api: []` недопустим. Связанные модели должны разрешать одинаковые API.
124
+
125
+ Секцию `graphql` и GraphQL в корневом `api` нужно включать вместе. Auth и файлы остаются REST-сервисами и описываются только в OpenAPI. OpenAPI может описывать их и при базе, доступной только через GraphQL.
126
+
127
+ | Старый флаг CLI | Замена |
128
+ |---|---|
129
+ | `--files` | Уберите флаг: маршруты включает секция `files` |
130
+ | `--openapi` | `--generate` |
131
+ | `--openapi-only` | `--generate-only` |
132
+
133
+ В отличие от старого CLI, обычный запуск теперь включает настроенные файловые маршруты. Чтобы отключить их, уберите `files`. `openapi: {}` публикует `/openapi.json`, `graphql: {}` — `/graphql`. Для экспорта задайте `target` **каждому** настроенному формату: флаги генерации экспортируют все настроенные форматы. Обычный запуск файлы экспорта не записывает.
134
+
135
+ ## 5. Переведите запросы и обработку ответов на scope
136
+
137
+ Единственный параметр запроса к записям — JSON `scope: [fields, arguments?]`, закодированный для URL.
138
+
139
+ | Параметр 0.9.0 | Запись в scope |
140
+ |---|---|
141
+ | `_where` или `fullName:contains=mi` | Второй объект: `where: { fullName: { contains: 'mi' } }` |
142
+ | `_sort=-id,fullName` | `order: [{ field: 'id', direction: 'DESC' }, { field: 'fullName', direction: 'ASC' }]` |
143
+ | `_page=1&_perPage=10` | `pager: { page: 1, pageSize: 10 }` |
144
+ | `_embed=country` | Первый объект: `country: [{ id: true, name: true }]` |
145
+
146
+ Пример для схемы выше:
147
+
148
+ ```js
149
+ const scope = [
150
+ { id: true, fullName: true, countryId: true, country: [{ id: true, name: true }] },
151
+ {
152
+ where: { fullName: { contains: 'mi' } },
153
+ order: [{ field: 'fullName', direction: 'ASC' }],
154
+ pager: { page: 1, pageSize: 10 },
155
+ },
156
+ ];
157
+ const url = `/users?${new URLSearchParams({ scope: JSON.stringify(scope) })}`;
158
+ ```
159
+
160
+ Обновите и чтение ответов:
161
+
162
+ - Без `scope` и с `{ '*': true }` выбираются только скаляры, не хранящие ключи связей. Массивы, объекты, связи и объявленные ключи нужно выбирать явно. Поля `writeOnly` выбрать нельзя.
163
+ - Коллекции сохраняют `{ data, total }`. **Вложенные списки объектов и связей теперь тоже имеют `{ data, total }`**, а не обычный массив. Массивы примитивов остаются массивами. Одиночная связь возвращает объект или `null`.
164
+ - У каждого списка объектов есть пагинация, в том числе у вложенного: размер страницы по умолчанию `10`, максимум `100`. Раньше вложенные связи возвращались целиком. Задайте нужный `pager` каждому списку и загружайте следующие страницы.
165
+ - Фильтрация по связи работает без её выбора в ответе. Сортировка родительских записей по связанной модели, как старый `_sort=country.name`, не поддерживается. Сортируйте связанный список его собственным `order` либо храните сортируемое значение в родительской записи.
166
+ - Строковые сравнения `gt/gte/lt/lte` используют порядок без учёта регистра, с учётом числовых частей. Проверьте клиентов, полагавшихся на прежнее лексикографическое сравнение.
167
+ - JSON scope ограничен 10 000 символов и 32 уровнями выбора. Неизвестные поля и операторы отклоняются даже на пустых данных.
168
+
169
+ ## 6. Проверьте запись и включение auth
170
+
171
+ `PATCH` объединяет поля верхнего уровня; переданный вложенный обычный объект заменяет прежний с сохранением защищённых полей. `PUT` заменяет доступные для записи поля, сохраняя первичный ключ, генерируемые, служебные и `readOnly` значения. При создании и замене нужны все обязательные поля без значений по умолчанию.
172
+
173
+ Поле связи принимает объекты, например `country: { id: '1' }`, а не скалярные ID. **Начиная с RC7 объект только с первичным ключом считается ссылкой в POST, PUT и PATCH.** Поля, даты и авторство целевой записи сохраняются. Дополнительные поля превращают его в обновление: PUT заменяет цель и требует её обязательные поля, POST/PATCH обновляют переданные поля. При изменении обратной связи нужны права на каждую запись, чей хранимый ключ меняется. Объявленные хранимые ключи, например `countryId`, тоже позволяют менять ссылки напрямую. Не передавайте одновременно связь и её хранимый ключ.
174
+
175
+ **При переходе с RC6:** объект только с ключом больше не обновляет цель, не меняет её даты/авторство и не требует владения неизменяемой целевой записью. Если клиент намерен обновить или заменить цель, передавайте поля для изменения. Объявленные хранимые ключи по-прежнему позволяют менять только ссылки.
176
+
177
+ Пропущенные обратные связи (`keyOn: 'related'`) сохраняются при PUT. Явное `users: []` меняет ключи пользователей, чтобы отсоединить их; операция может завершиться ошибкой при обязательных ключах или отсутствии прав. Вложенные изменения атомарны, а изменение общей целевой записи видно всем, кто на неё ссылается.
178
+
179
+ Auth, даты и мягкое удаление — новые необязательные возможности. Для первого перехода с сохранением записи без входа и физического удаления не добавляйте `auth`, оставьте `timestamps` / `softDelete` выключенными — это их значения по умолчанию.
180
+
181
+ Если включаете auth, создайте отдельное хранилище учётных записей и перенесите владельцев осознанно. Старые записи без `createdById` может менять только администратор. Через API владелец управляется сервером; подготовьте его в копии базы до запуска, если старые записи должны редактировать обычные пользователи. Auth не закрывает чтение и файловые операции. В RC8 дисковый режим сохраняет хеши сессий в файле пользователей и восстанавливает действующие сессии после перезапуска. Старые пользователи без `sessions` остаются допустимыми. При ручной замене хеша пароля также очистите `sessions` этого пользователя.
182
+
183
+ При `softDelete` DELETE сохраняет запись, списки по умолчанию скрывают удалённые записи, чтение по первичному ключу может вернуть их, а успешный PUT/PATCH восстанавливает. Старый пользовательский флаг архивирования не преобразуется автоматически. Перед включением прочитайте [правила дат, удаления и владения](README.ru.md#даты-записей-удаление-и-владельцы).
184
+
185
+ ## 7. Обновите программное использование
186
+
187
+ `createServer(config)` принимает ровно один аргумент. Уберите прежний второй аргумент `{ files: false }`; для отключения файлов уберите `files` из конфигурации.
188
+
189
+ `facade.openapi()` и `facade.graphql()` возвращают документы без записи в `target`. Нужны соответствующие секции конфигурации; для OpenAPI — также метаданные пакета и схема. Используйте флаги генерации CLI или явную запись:
190
+
191
+ ```js
192
+ import { createServer } from '@kollors/deep-json-server';
193
+ import { writeOpenapi } from '@kollors/deep-json-server/openapi';
194
+ import config from './server.config.js';
195
+
196
+ const facade = await createServer(config);
197
+ await writeOpenapi(await facade.openapi(), './generated/openapi.yaml');
198
+ const app = facade.fastify();
199
+ await app.ready(); // Инициализация и проверка базы без открытия порта.
200
+ await app.close();
201
+ ```
202
+
203
+ Одна генерация схем не проверяет существующие записи базы. Для инициализации HTTP и проверки данных вызовите `ready()`, `inject()` или `listen()`. После изменения моделей и запросов пересоздайте клиентов из новых OpenAPI/GraphQL-документов.
204
+
205
+ ## 8. Проверьте переход и подготовьте откат
206
+
207
+ 1. Зафиксируйте выбранную RC-версию и переведите **копию** файлов 0.9.0. Выполните генерацию, затем инициализацию HTTP-сервера для проверки данных.
208
+ 2. Проверьте чтение по умолчанию, явные ключи, вложенные списки, фильтры и пагинацию. На одноразовых копиях записей проверьте создание, PATCH, PUT, изменение связей и удаление.
209
+ 3. При включении auth проверьте владельца, администратора и обычного пользователя без владения на старых записях. Убедитесь, что загруженные файлы доступны через прежний путь к метаданным.
210
+ 4. Перед переключением остановите все старые процессы сервера: RC8 заменяет каталог блокировки системными блокировками и читаемыми файлами `<имя-источника>-lock.json` для базы, auth, каталога загрузок и метаданных файлов. Старый и новый протоколы блокировки не взаимодействуют. Используйте один процесс на записываемое хранилище и останавливайте его перед ручными правками.
211
+ 5. Для отката храните вместе старую версию пакета, конфигурацию, схему, базу и метаданные файлов. Восстанавливайте их вместе; не направляйте 0.9.0 на данные, уже изменённые новым сервером. В последующие резервные копии включайте новые счётчики (`<database>.counters.json`) и служебные данные восстановления мягкого удаления, если используете эти функции.