@kollors/deep-json-server 0.9.0 → 1.0.0-alpha.2
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/README.md +313 -403
- package/README.ru.md +307 -397
- package/dist/index.d.ts +5 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/src/cli.js +95 -66
- package/dist/src/cli.js.map +1 -1
- package/dist/src/config.d.ts +18 -2
- package/dist/src/config.js +39 -10
- package/dist/src/config.js.map +1 -1
- package/dist/src/constants.d.ts +2 -1
- package/dist/src/constants.js +2 -1
- package/dist/src/constants.js.map +1 -1
- package/dist/src/database.d.ts +4 -3
- package/dist/src/database.js +32 -19
- package/dist/src/database.js.map +1 -1
- package/dist/src/engine.d.ts +47 -0
- package/dist/src/engine.js +371 -0
- package/dist/src/engine.js.map +1 -0
- package/dist/src/errors.d.ts +6 -0
- package/dist/src/errors.js +9 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/features.d.ts +8 -0
- package/dist/src/features.js +18 -0
- package/dist/src/features.js.map +1 -0
- package/dist/src/files/disk-store.js +17 -6
- package/dist/src/files/disk-store.js.map +1 -1
- package/dist/src/files/openapi.d.ts +3 -0
- package/dist/src/files/openapi.js +86 -0
- package/dist/src/files/openapi.js.map +1 -0
- package/dist/src/graphql/preflight.d.ts +3 -0
- package/dist/src/graphql/preflight.js +28 -0
- package/dist/src/graphql/preflight.js.map +1 -0
- package/dist/src/graphql/routes.d.ts +4 -0
- package/dist/src/graphql/routes.js +28 -0
- package/dist/src/graphql/routes.js.map +1 -0
- package/dist/src/graphql.d.ts +4 -0
- package/dist/src/graphql.js +227 -0
- package/dist/src/graphql.js.map +1 -0
- package/dist/src/model.d.ts +65 -0
- package/dist/src/model.js +345 -0
- package/dist/src/model.js.map +1 -0
- package/dist/src/openapi/document.d.ts +7 -7
- package/dist/src/openapi/document.js +237 -320
- package/dist/src/openapi/document.js.map +1 -1
- package/dist/src/openapi/index.d.ts +3 -6
- package/dist/src/openapi/index.js +2 -3
- package/dist/src/openapi/index.js.map +1 -1
- package/dist/src/query/contract.d.ts +3 -0
- package/dist/src/query/contract.js +19 -0
- package/dist/src/query/contract.js.map +1 -0
- package/dist/src/query/filter.d.ts +3 -5
- package/dist/src/query/filter.js +76 -240
- package/dist/src/query/filter.js.map +1 -1
- package/dist/src/query/options.d.ts +18 -0
- package/dist/src/query/options.js +30 -0
- package/dist/src/query/options.js.map +1 -0
- package/dist/src/rest/options.d.ts +14 -0
- package/dist/src/rest/options.js +108 -0
- package/dist/src/rest/options.js.map +1 -0
- package/dist/src/rest/projection.d.ts +6 -0
- package/dist/src/rest/projection.js +37 -0
- package/dist/src/rest/projection.js.map +1 -0
- package/dist/src/rest/routes.d.ts +3 -0
- package/dist/src/rest/routes.js +48 -0
- package/dist/src/rest/routes.js.map +1 -0
- package/dist/src/schema.d.ts +17 -0
- package/dist/src/schema.js +23 -0
- package/dist/src/schema.js.map +1 -0
- package/dist/src/server.d.ts +3 -5
- package/dist/src/server.js +76 -162
- package/dist/src/server.js.map +1 -1
- package/dist/src/types.d.ts +1 -3
- package/dist/src/utils.d.ts +0 -1
- package/dist/src/utils.js +0 -1
- package/dist/src/utils.js.map +1 -1
- package/package.json +9 -4
- package/dist/src/openapi/config.d.ts +0 -11
- package/dist/src/openapi/config.js +0 -204
- package/dist/src/openapi/config.js.map +0 -1
- package/dist/src/openapi/inference.d.ts +0 -14
- package/dist/src/openapi/inference.js +0 -145
- package/dist/src/openapi/inference.js.map +0 -1
- package/dist/src/query/index.d.ts +0 -3
- package/dist/src/query/index.js +0 -4
- package/dist/src/query/index.js.map +0 -1
- package/dist/src/query/pagination.d.ts +0 -11
- package/dist/src/query/pagination.js +0 -28
- package/dist/src/query/pagination.js.map +0 -1
- package/dist/src/query/sort.d.ts +0 -1
- package/dist/src/query/sort.js +0 -54
- package/dist/src/query/sort.js.map +0 -1
- package/dist/src/relations.d.ts +0 -12
- package/dist/src/relations.js +0 -155
- package/dist/src/relations.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,206 +1,378 @@
|
|
|
1
1
|
# Deep JSON Server
|
|
2
2
|
|
|
3
|
-
[
|
|
3
|
+
[Русский](README.ru.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
A JSON-backed mock server with REST, GraphQL, nested queries, binary files and schema exports. Requires Node.js 22 or newer.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
**1.0.0-alpha.2 is a prerelease.** When upgrading from 0.x, update your model schema and query parameters using the examples below.
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
npm install --save-dev @kollors/deep-json-server
|
|
11
|
+
```sh
|
|
12
|
+
npm install @kollors/deep-json-server@alpha
|
|
15
13
|
```
|
|
16
14
|
|
|
15
|
+
To install a specific version, use `@1.0.0-alpha.2`.
|
|
16
|
+
|
|
17
17
|
## Quick start
|
|
18
18
|
|
|
19
|
-
Create
|
|
19
|
+
Create two files in the same directory.
|
|
20
|
+
|
|
21
|
+
`database.json`:
|
|
20
22
|
|
|
21
23
|
```json
|
|
22
24
|
{
|
|
23
|
-
"
|
|
24
|
-
{ "id": "1", "
|
|
25
|
+
"users": [
|
|
26
|
+
{ "id": "1", "fullName": "Мира Волкова" }
|
|
25
27
|
]
|
|
26
28
|
}
|
|
27
29
|
```
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
`server.config.js`:
|
|
30
32
|
|
|
31
33
|
```js
|
|
32
34
|
export default {
|
|
33
|
-
database: {
|
|
34
|
-
path: 'mock/database.json',
|
|
35
|
-
},
|
|
35
|
+
database: { path: './database.json' },
|
|
36
36
|
};
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
```bash
|
|
39
|
+
```sh
|
|
42
40
|
npx deep-json-server server.config.js
|
|
43
41
|
```
|
|
44
42
|
|
|
45
|
-
The
|
|
43
|
+
The user list is available at `http://127.0.0.1:4001/users`.
|
|
44
|
+
|
|
45
|
+
## Configuration
|
|
46
|
+
|
|
47
|
+
| Setting | Meaning |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `database.path` / `database.data` | Exactly one: JSON file or in-memory collection object |
|
|
50
|
+
| `database.schema` | Model object or JSON schema-file path; optional for REST |
|
|
51
|
+
| `openapi.enabled` | Enable the specification endpoint; default `false` |
|
|
52
|
+
| `openapi.endpoint` | Specification path; default `/openapi.json` |
|
|
53
|
+
| `openapi.path` | YAML export destination |
|
|
54
|
+
| `openapi.info` | Optional `title`, `version`, `description` |
|
|
55
|
+
| `graphql.enabled` | Enable GraphQL HTTP endpoint; default `false` |
|
|
56
|
+
| `graphql.endpoint` | Endpoint path; default `/graphql` |
|
|
57
|
+
| `graphql.path` | GraphQL SDL export destination |
|
|
58
|
+
| `server.host`, `server.port` | Defaults `127.0.0.1`, `4001`; CLI also reads `HOST`/`PORT` |
|
|
59
|
+
| `server.pageSize`, `server.maxPageSize` | Defaults 10 and 100; default size is capped by the maximum |
|
|
60
|
+
| `server.cors`, `server.logger` | Default `true`; logger also accepts Fastify logger options |
|
|
61
|
+
| `server.maxFileSize` | Default 100 MiB |
|
|
62
|
+
| `files.data` | In-memory binary files |
|
|
63
|
+
| `files.directory`, `files.metadata` | Disk storage directory and metadata JSON file; both required |
|
|
64
|
+
|
|
65
|
+
Relative paths resolve from the configuration file's directory. When passing a configuration object to `createServer()`, paths resolve from the working directory. The server works with a copy of in-memory input.
|
|
66
|
+
|
|
67
|
+
| CLI flag | Action |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `--files` | Enable file routes |
|
|
70
|
+
| `--graphql` | Enable the GraphQL API |
|
|
71
|
+
| `--openapi` | Enable the OpenAPI endpoint |
|
|
72
|
+
| `--host <host>` | Server address |
|
|
73
|
+
| `--port <port>` | Server port |
|
|
74
|
+
| `--help`, `-h` | Show help |
|
|
75
|
+
| `--version`, `-v` | Show package version |
|
|
76
|
+
|
|
77
|
+
Setting priority: CLI → configuration → `HOST`/`PORT` → defaults. File routes are enabled when `files` is configured.
|
|
78
|
+
|
|
79
|
+
To generate schemas, specify the format and configuration file:
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
npx deep-json-server generate openapi server.config.js
|
|
83
|
+
npx deep-json-server generate graphql server.config.js
|
|
84
|
+
npx deep-json-server generate openapi,graphql server.config.js
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The command reads `database.schema` and writes schemas to `openapi.path` and `graphql.path`. Generation requires no database contents or file store.
|
|
88
|
+
|
|
89
|
+
## Model schema
|
|
90
|
+
|
|
91
|
+
Examples: [database](examples/database.json), [model schema](examples/schema.json), [configuration](examples/server.config.js).
|
|
46
92
|
|
|
47
93
|
```json
|
|
48
94
|
{
|
|
49
|
-
"
|
|
50
|
-
|
|
95
|
+
"Country": {
|
|
96
|
+
"collection": "countries",
|
|
97
|
+
"api": ["openapi", "graphql"],
|
|
98
|
+
"fields": {
|
|
99
|
+
"id": { "type": "string", "primary": true, "generated": "uuid" },
|
|
100
|
+
"name": { "type": "string", "required": true },
|
|
101
|
+
"users": { "type": "User[]", "target": "countryId" }
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
"User": {
|
|
105
|
+
"collection": "users",
|
|
106
|
+
"api": ["openapi", "graphql"],
|
|
107
|
+
"fields": {
|
|
108
|
+
"id": { "type": "string", "primary": true, "generated": "uuid" },
|
|
109
|
+
"fullName": { "type": "string", "required": true },
|
|
110
|
+
"country": { "type": "Country", "source": "countryId" }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
51
113
|
}
|
|
52
114
|
```
|
|
53
115
|
|
|
54
|
-
|
|
116
|
+
`api` defaults to `["openapi", "graphql"]`. `[]` excludes the model from both exports and GraphQL, while REST remains available. Related models must enable the same export format. Names must be valid identifiers and generated type/operation names must not collide. `and`, `or`, `not` are reserved filter names.
|
|
55
117
|
|
|
56
|
-
|
|
118
|
+
| Capability | With schema | Without schema |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| REST CRUD | Model validation | JSON/body/identifier validation only |
|
|
121
|
+
| Relations | Explicit model fields | `countryId`, `genreIds`, etc. naming conventions |
|
|
122
|
+
| OpenAPI 3.0.3 export | Available | Error when requested |
|
|
123
|
+
| GraphQL SDL / API | Available | Error when requested |
|
|
57
124
|
|
|
58
|
-
|
|
59
|
-
import process from 'node:process';
|
|
125
|
+
Explicit schemas are strict: undeclared fields and collections are rejected, except storage keys inferred from relations. Existing data is validated on startup. Generation uses the model definitions. Schemaless REST generates an `id` and preserves arbitrary JSON fields. Filters and individual field selections use identifier-style names; other fields are returned through `scope=*`.
|
|
60
126
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
127
|
+
### Fields
|
|
128
|
+
|
|
129
|
+
The `type` property accepts `string`, `number`, `boolean`, `object`, or a model name. Append `[]` for an array: `string[]`, `object[]`, `Genre[]`. Use dotted paths for nested fields, such as `actors.fullName`.
|
|
130
|
+
|
|
131
|
+
| Properties | Meaning |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `type` | Required type |
|
|
134
|
+
| `description`, `example` | Documentation and example value |
|
|
135
|
+
| `required`, `nullable` | Defaults `false`; presence and explicit null are separate |
|
|
136
|
+
| `default` | Value when omitted on create/replace; PATCH does not insert defaults |
|
|
137
|
+
| `enum` | Allowed values; for arrays, allowed element values |
|
|
138
|
+
| `primary` | Root primary key; mandatory, unique, non-null and immutable |
|
|
139
|
+
| `generated` | `uuid` for strings, `increment` for numbers; server supplies the value |
|
|
140
|
+
| `readOnly`, `writeOnly` | Output-only or input-only; mutually exclusive |
|
|
141
|
+
| `minLength`, `maxLength`, `pattern` | String constraints |
|
|
142
|
+
| `format` | `date`, `date-time`, `email`, `uri`, `uuid` |
|
|
143
|
+
| `minimum`, `maximum` | Inclusive numeric bounds |
|
|
144
|
+
| `source`, `target`, `onDelete` | Relation metadata |
|
|
145
|
+
|
|
146
|
+
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 the field to be present; an ordinary array may still be empty.
|
|
147
|
+
|
|
148
|
+
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.
|
|
149
|
+
|
|
150
|
+
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.
|
|
151
|
+
|
|
152
|
+
### Relations
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
"actors.genres": {
|
|
156
|
+
"type": "Genre[]",
|
|
157
|
+
"source": "actors.genreIds",
|
|
158
|
+
"required": true
|
|
159
|
+
}
|
|
82
160
|
```
|
|
83
161
|
|
|
84
|
-
|
|
162
|
+
`Genre` returns an object; `Genre[]` returns a list. `source` defaults to the current model's primary key, `target` to the target model's primary key. These defaults also apply to nested relations. Paths start at the root of their respective records: in this example, `actors.genreIds` contains the current actor's genre keys.
|
|
163
|
+
|
|
164
|
+
Relation keys are stored in the database and included among the record's own fields. Their types are inferred from the matched keys. A `source` field pointing to a target primary key can be omitted from the field declarations: the schema infers an array of keys for a list relation or a scalar key for a single relation. Declare the storage field explicitly when the mapping is ambiguous.
|
|
85
165
|
|
|
86
|
-
|
|
87
|
-
| --- | --- | --- |
|
|
88
|
-
| `database.path` | Exactly one of `path` or `data` is required | Existing JSON database file |
|
|
89
|
-
| `database.data` | Exactly one of `path` or `data` is required | Database object stored in memory |
|
|
90
|
-
| `database.schema` | No | Path to JSON overrides or an object with request-validation and OpenAPI settings |
|
|
91
|
-
| `files.directory` | Together with `files.metadata` | Directory for binary contents on disk |
|
|
92
|
-
| `files.metadata` | Together with `files.directory` | JSON file containing file metadata on disk |
|
|
93
|
-
| `files.data` | Instead of the `directory` and `metadata` pair | In-memory files with `Uint8Array` contents |
|
|
94
|
-
| `openapi.path` | Required by the `--openapi` and `--openapi-only` CLI flags | Generated OpenAPI YAML file; the programmatic API can return a document without this path |
|
|
95
|
-
| `server.cors` | No | Enables permissive CORS headers and `OPTIONS` routes; defaults to `true` |
|
|
96
|
-
| `server.host` | No | Host used by the CLI, `server.openapi()`, and argument-less `server.fastify().listen()`; defaults to `127.0.0.1` |
|
|
97
|
-
| `server.logger` | No | Fastify logger settings; defaults to `true` |
|
|
98
|
-
| `server.maxFileSize` | No | Maximum uploaded-file size in bytes when file routes are enabled; defaults to 100 MiB |
|
|
99
|
-
| `server.maxPageSize` | No | Maximum allowed `_perPage` in the API and OpenAPI; defaults to `1000` |
|
|
100
|
-
| `server.port` | No | Port used by the CLI, `server.openapi()`, and argument-less `server.fastify().listen()`; defaults to `4001` |
|
|
166
|
+
Reverse example: `User.movies = {"type":"Movie[]","target":"actors.userId"}`. A movie is returned once even if several actors match. A single relation resolving to multiple targets is invalid.
|
|
101
167
|
|
|
102
|
-
|
|
168
|
+
Every supplied direct relation key must point to an existing record. `required: true` on a relation requires at least one target before response filtering/pagination. Reverse relations using the primary key as `source` may be empty unless required. Missing single relations return `null`.
|
|
103
169
|
|
|
104
|
-
|
|
170
|
+
`onDelete` describes what happens **when a target record is deleted**:
|
|
105
171
|
|
|
106
|
-
|
|
172
|
+
- `restrict` (default): refuse deletion while a surviving owner refers to the target.
|
|
173
|
+
- `cascade`: delete the referring owner. For `User.country`, deleting the country deletes its users. For `Movie.actors.user`, deleting the user removes matching actor elements and retains the movie.
|
|
174
|
+
|
|
175
|
+
Cascading deletion runs as one operation, including cyclic relations. A validation failure cancels the entire operation. `onDelete` rules also apply to explicitly declared reverse relations; account for both rules when defining both directions.
|
|
176
|
+
|
|
177
|
+
## Queries and responses
|
|
178
|
+
|
|
179
|
+
Collections and lists of objects, including embedded `object[]` fields, return:
|
|
180
|
+
|
|
181
|
+
```json
|
|
182
|
+
{ "data": [], "total": 0 }
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Primitive arrays are returned as plain arrays. Every object list accepts optional `where`, `order` and `pager`. Processing order is filter → sort → pagination. `total` is the filtered record count before pagination.
|
|
186
|
+
|
|
187
|
+
Use `page` and `pageSize` in `pager`. The default is the first page with the size from `server.pageSize`. Both values must be positive integers; `pageSize` is limited by `server.maxPageSize`. Out-of-range pages return empty `data` with the total matching record count in `total`.
|
|
188
|
+
|
|
189
|
+
`where` uses field operators `eq`, `ne`, `in`, string `contains`/`startsWith`/`endsWith`, and comparisons `gt`, `gte`, `lt`, `lte`. Combine conditions with `and`, `or`, `not`. Arrays support `some`, `every`, `none`; primitive arrays also support `contains`, `in`. String matching is case-insensitive. A field condition is an object containing an operator, such as `{ "id": { "eq": "1" } }`.
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{
|
|
193
|
+
"movies": {
|
|
194
|
+
"some": {
|
|
195
|
+
"actors": {
|
|
196
|
+
"some": {
|
|
197
|
+
"genres": { "some": { "id": { "in": ["2", "3"] } } }
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Root `where` selects records from the main collection. `where` inside a relation filters its elements while retaining the parent record. Each nested list is processed independently. Filtering by a relation works independently of its inclusion in the response.
|
|
206
|
+
|
|
207
|
+
`order` is an array of `{ "field": "fullName", "direction": "ASC" }` rules. Earlier rules have priority; equal values retain storage order. Null and missing values compare equally. REST uses dotted field paths; GraphQL uses generated enums (`profile_name` for `profile.name`). Ambiguous enum names cause a generation error. Sorting supports scalar fields of the current object, including nested fields. Related lists accept their own `order`.
|
|
208
|
+
|
|
209
|
+
### REST
|
|
210
|
+
|
|
211
|
+
| Method | Path | Operation |
|
|
212
|
+
|---|---|---|
|
|
213
|
+
| GET | `/users` | `userList` |
|
|
214
|
+
| GET | `/users/{id}` | `user` |
|
|
215
|
+
| POST | `/users` | `userCreate` |
|
|
216
|
+
| PUT | `/users/{id}` | `userReplace` |
|
|
217
|
+
| PATCH | `/users/{id}` | `userUpdate` |
|
|
218
|
+
| DELETE | `/users/{id}` | `userDelete` |
|
|
219
|
+
|
|
220
|
+
The path parameter name follows the primary key. POST, PUT and PATCH accept a JSON record object. PUT replaces the record while retaining its key and server-managed fields. PATCH merges fields at the top level; supplied nested objects are replaced while preserving their read-only fields. Creation and replacement require all mandatory fields. Updates validate supplied values and the final record. Missing records return `404`; conflicts return `409`. DELETE returns the deleted record.
|
|
221
|
+
|
|
222
|
+
Query parameters `where`, `order`, `pager`, `nested` contain JSON. `scope` is a selection string. Example shown before URL encoding:
|
|
223
|
+
|
|
224
|
+
```text
|
|
225
|
+
GET /users?where={"fullName":{"contains":"Мира"}}&order=[{"field":"fullName","direction":"ASC"}]&pager={"page":1,"pageSize":20}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Construct encoded URLs with `URLSearchParams`:
|
|
107
229
|
|
|
108
230
|
```js
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
files: {
|
|
115
|
-
data: [{ content: new Uint8Array([1, 2, 3]), directory: 'examples', mimeType: 'application/octet-stream', name: 'example.bin' }],
|
|
116
|
-
},
|
|
117
|
-
};
|
|
231
|
+
const params = new URLSearchParams({
|
|
232
|
+
scope: 'id,fullName,movies(id,title)',
|
|
233
|
+
nested: JSON.stringify({ movies: { order: [{ field: 'title', direction: 'ASC' }], pager: { page: 1, pageSize: 5 } } }),
|
|
234
|
+
});
|
|
235
|
+
const response = await fetch(`/users?${params}`);
|
|
118
236
|
```
|
|
119
237
|
|
|
120
|
-
|
|
238
|
+
`scope=*,actors(user(id,fullName),genres(*))` selects own fields and the specified relations. `*` includes the current object's own fields and stored keys, except `writeOnly` fields. Relations are listed explicitly. The default selection is own fields. Lists retain the `{ data, total }` response structure.
|
|
121
239
|
|
|
122
|
-
|
|
240
|
+
`nested` maps full response paths to list options:
|
|
123
241
|
|
|
124
242
|
```json
|
|
125
243
|
{
|
|
126
|
-
"
|
|
127
|
-
|
|
128
|
-
"
|
|
129
|
-
"
|
|
130
|
-
"openapi": "deep-json-server --openapi-only server.config.js",
|
|
131
|
-
"openapi:files": "deep-json-server --files --openapi-only server.config.js"
|
|
244
|
+
"actors": { "pager": { "pageSize": 5 } },
|
|
245
|
+
"actors.genres": {
|
|
246
|
+
"where": { "id": { "in": ["2", "3"] } },
|
|
247
|
+
"order": [{ "field": "name", "direction": "ASC" }]
|
|
132
248
|
}
|
|
133
249
|
}
|
|
134
250
|
```
|
|
135
251
|
|
|
136
|
-
|
|
252
|
+
A path in `nested` must be selected by `scope` and point to an object list. Single-record routes and mutations accept `scope` and `nested`; root `where`, `order` and `pager` apply to collection GET. Invalid names and unsafe paths return `400`.
|
|
253
|
+
|
|
254
|
+
### GraphQL
|
|
255
|
+
|
|
256
|
+
```graphql
|
|
257
|
+
query {
|
|
258
|
+
userList(
|
|
259
|
+
order: [{ field: fullName, direction: ASC }]
|
|
260
|
+
pager: { page: 1, pageSize: 20 }
|
|
261
|
+
) {
|
|
262
|
+
total
|
|
263
|
+
data {
|
|
264
|
+
id
|
|
265
|
+
fullName
|
|
266
|
+
movies(order: [{ field: title, direction: ASC }], pager: { pageSize: 5 }) {
|
|
267
|
+
total
|
|
268
|
+
data { id title }
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
```
|
|
137
274
|
|
|
138
|
-
|
|
139
|
-
| --- | --- |
|
|
140
|
-
| `deep-json-server server.config.js` | Starts the CRUD server without file routes |
|
|
141
|
-
| `deep-json-server --files server.config.js` | Starts the CRUD server with file routes |
|
|
142
|
-
| `deep-json-server --openapi server.config.js` | Generates OpenAPI and starts the CRUD server |
|
|
143
|
-
| `deep-json-server --files --openapi server.config.js` | Generates OpenAPI with file routes and starts the server with them |
|
|
144
|
-
| `deep-json-server --openapi-only server.config.js` | Generates OpenAPI and exits |
|
|
145
|
-
| `deep-json-server --files --openapi-only server.config.js` | Generates OpenAPI with file routes and exits |
|
|
275
|
+
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.
|
|
146
276
|
|
|
147
|
-
|
|
277
|
+
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. Errors include `extensions.code`: `INVALID_INPUT`, `INVALID_QUERY`, `NOT_FOUND`, `CONFLICT` or `INTERNAL_ERROR`.
|
|
148
278
|
|
|
149
279
|
## Example database
|
|
150
280
|
|
|
151
|
-
Below is an example movie catalog with sample data. `Gangster` is linked to its parent genre, `Crime`.
|
|
152
|
-
|
|
153
281
|
```json
|
|
154
282
|
{
|
|
155
283
|
"countries": [
|
|
156
|
-
{
|
|
157
|
-
|
|
284
|
+
{
|
|
285
|
+
"id": "1",
|
|
286
|
+
"isArchived": false,
|
|
287
|
+
"name": "Ардения"
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
"id": "2",
|
|
291
|
+
"isArchived": false,
|
|
292
|
+
"name": "Велория"
|
|
293
|
+
}
|
|
158
294
|
],
|
|
159
295
|
"genres": [
|
|
160
|
-
{
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
296
|
+
{
|
|
297
|
+
"id": "1",
|
|
298
|
+
"isArchived": false,
|
|
299
|
+
"name": "Криминал",
|
|
300
|
+
"parentIds": []
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
"id": "2",
|
|
304
|
+
"isArchived": false,
|
|
305
|
+
"name": "Гангстер",
|
|
306
|
+
"parentIds": ["1"]
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
"id": "3",
|
|
310
|
+
"isArchived": false,
|
|
311
|
+
"name": "Драма",
|
|
312
|
+
"parentIds": []
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
"id": "4",
|
|
316
|
+
"isArchived": false,
|
|
317
|
+
"name": "Комедия",
|
|
318
|
+
"parentIds": []
|
|
319
|
+
}
|
|
164
320
|
],
|
|
165
321
|
"movies": [
|
|
166
322
|
{
|
|
167
323
|
"actors": [
|
|
168
|
-
{
|
|
169
|
-
|
|
324
|
+
{
|
|
325
|
+
"genreIds": ["2", "3"],
|
|
326
|
+
"id": "movie-1-actor-1",
|
|
327
|
+
"userId": "1"
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
"genreIds": ["3"],
|
|
331
|
+
"id": "movie-1-actor-2",
|
|
332
|
+
"userId": "2"
|
|
333
|
+
}
|
|
170
334
|
],
|
|
171
335
|
"coverSrc": "https://example.com/covers/shadows-of-ardenia.jpg",
|
|
172
|
-
"description": "
|
|
336
|
+
"description": "Наследница портового города раскрывает заговор двух соперничающих семей.",
|
|
173
337
|
"id": "1",
|
|
174
338
|
"isArchived": false,
|
|
175
339
|
"publisherIds": ["2"],
|
|
176
|
-
"title": "
|
|
340
|
+
"title": "Тени Ардении"
|
|
177
341
|
},
|
|
178
342
|
{
|
|
179
343
|
"actors": [],
|
|
180
344
|
"coverSrc": "https://example.com/covers/northern-star.jpg",
|
|
181
|
-
"description": "
|
|
345
|
+
"description": "Ночной администратор старого отеля случайно становится участником поисков пропавшей картины.",
|
|
182
346
|
"id": "2",
|
|
183
347
|
"isArchived": false,
|
|
184
348
|
"publisherIds": ["1"],
|
|
185
|
-
"title": "
|
|
349
|
+
"title": "Полночь в «Северной звезде»"
|
|
186
350
|
}
|
|
187
351
|
],
|
|
188
352
|
"publishers": [
|
|
189
|
-
{
|
|
190
|
-
|
|
353
|
+
{
|
|
354
|
+
"id": "1",
|
|
355
|
+
"isArchived": false,
|
|
356
|
+
"name": "Northlight Studio"
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
"id": "2",
|
|
360
|
+
"isArchived": false,
|
|
361
|
+
"name": "Aurora Pictures"
|
|
362
|
+
}
|
|
191
363
|
],
|
|
192
364
|
"users": [
|
|
193
365
|
{
|
|
194
366
|
"bornAt": "1988-03-14",
|
|
195
367
|
"countryId": "1",
|
|
196
|
-
"fullName": "
|
|
368
|
+
"fullName": "Мира Волкова",
|
|
197
369
|
"id": "1",
|
|
198
370
|
"isArchived": false
|
|
199
371
|
},
|
|
200
372
|
{
|
|
201
373
|
"bornAt": "1991-11-02",
|
|
202
374
|
"countryId": "2",
|
|
203
|
-
"fullName": "
|
|
375
|
+
"fullName": "Леон Ветров",
|
|
204
376
|
"id": "2",
|
|
205
377
|
"isArchived": false
|
|
206
378
|
}
|
|
@@ -208,150 +380,12 @@ Below is an example movie catalog with sample data. `Gangster` is linked to its
|
|
|
208
380
|
}
|
|
209
381
|
```
|
|
210
382
|
|
|
211
|
-
Every top-level array becomes a REST resource:
|
|
212
|
-
|
|
213
|
-
```text
|
|
214
|
-
GET /movies
|
|
215
|
-
GET /movies/:id
|
|
216
|
-
POST /movies
|
|
217
|
-
PUT /movies/:id
|
|
218
|
-
PATCH /movies/:id
|
|
219
|
-
DELETE /movies/:id
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
`POST` generates a string ID. `PUT` completely replaces the selected record, while `PATCH` updates only supplied fields; both preserve the existing ID and its type. An `id` supplied in any request body cannot override the server-controlled ID. All write operations — `POST`, `PUT`, `PATCH` and `DELETE` — are serialized; disk storage persists them in JSON, while memory storage retains them until the process exits.
|
|
223
|
-
|
|
224
|
-
The database file must exist before startup. Resource names may contain Latin letters, numbers, `_` and `-`, and must start with a letter. Every resource is an array of JSON objects. Every record must have a non-empty string or finite numeric `id`; IDs must be unique within a resource when compared as strings, so `1` and `"1"` cannot coexist. All nested values must be JSON-compatible: finite numbers, strings, booleans, `null`, arrays, and plain objects. The server rereads the file before every resource GET and write operation, so valid edits to existing resources become visible immediately. Resource names and routes are discovered at startup; restart the server after adding, removing, or renaming a top-level resource.
|
|
225
|
-
|
|
226
|
-
Successful writes return the created, replaced, updated or deleted record. Errors use an appropriate HTTP status and this JSON shape:
|
|
227
|
-
|
|
228
|
-
```json
|
|
229
|
-
{ "error": "..." }
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
## Pagination and sorting
|
|
233
|
-
|
|
234
|
-
```http
|
|
235
|
-
GET /movies?_page=1&_perPage=10&_sort=-id,title
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
A collection GET always returns the current page data and the total number of records after filtering. `_page` defaults to `1`, and `_perPage` defaults to `10`:
|
|
239
|
-
|
|
240
|
-
```json
|
|
241
|
-
{
|
|
242
|
-
"data": [],
|
|
243
|
-
"total": 0
|
|
244
|
-
}
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
`data` contains only the records on the requested page. `total` is the number of all records matching the filter before pagination is applied. When needed, a client can calculate the last page as `Math.max(1, Math.ceil(total / pageSize))`.
|
|
248
|
-
|
|
249
|
-
Both pagination parameters must be positive integers. `_perPage` cannot exceed `1000` by default; change the limit through `server.maxPageSize` in the config passed to either the CLI or `createServer()`. Invalid values return `400` instead of being silently corrected. A page beyond the last page returns an empty `data` array while preserving the actual `total` value.
|
|
250
|
-
|
|
251
|
-
`_sort` accepts comma-separated field paths. Rules are applied from left to right; prefix a field with `-` for descending order. Dot paths can address nested object fields, including fields added by `_embed`, for example `GET /users?_embed=country&_sort=country.name,-id`. Unknown or unsafe sort fields return `400`.
|
|
252
|
-
|
|
253
|
-
## Filters
|
|
254
|
-
|
|
255
|
-
Pass a JSON object through `_where`:
|
|
256
|
-
|
|
257
|
-
```http
|
|
258
|
-
GET /movies?_where={"title":{"contains":"ardenia"}}
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
Nested objects and arrays can be filtered at any depth. Conditions in one object use `AND` by default:
|
|
262
|
-
|
|
263
|
-
```json
|
|
264
|
-
{
|
|
265
|
-
"actors": { "some": { "userId": { "eq": "1" } } },
|
|
266
|
-
"title": { "contains": "ardenia" }
|
|
267
|
-
}
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
Use `and`, `or` and `not` for explicit logical groups:
|
|
271
|
-
|
|
272
|
-
```json
|
|
273
|
-
{
|
|
274
|
-
"and": [
|
|
275
|
-
{
|
|
276
|
-
"or": [
|
|
277
|
-
{ "title": { "contains": "ardenia" } },
|
|
278
|
-
{ "actors": { "some": { "userId": { "eq": "2" } } } }
|
|
279
|
-
]
|
|
280
|
-
},
|
|
281
|
-
{ "not": { "isArchived": { "eq": true } } }
|
|
282
|
-
]
|
|
283
|
-
}
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
Field operators:
|
|
287
|
-
|
|
288
|
-
| Operator | Behavior |
|
|
289
|
-
| --- | --- |
|
|
290
|
-
| `eq`, `ne` | Equality or inequality |
|
|
291
|
-
| `contains` | Case-insensitive substring for strings, or matching member for arrays |
|
|
292
|
-
| `startsWith`, `endsWith` | Case-insensitive string prefix or suffix |
|
|
293
|
-
| `gt`, `gte`, `lt`, `lte` | Ordered comparison; ISO date strings can be compared lexically |
|
|
294
|
-
| `in` | Matches a scalar or array member against the supplied values |
|
|
295
|
-
| `some`, `every`, `none` | Applies a nested condition to array elements |
|
|
296
|
-
| `not` | Negates a nested field condition |
|
|
297
|
-
|
|
298
|
-
Simple query parameters are supported too:
|
|
299
|
-
|
|
300
|
-
```http
|
|
301
|
-
GET /movies?title:contains=ardenia
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
Simple filter values recognize JSON primitives: numbers, `true`, `false` and `null`. Values with leading zeroes, such as `001`, remain strings. Unknown operators, invalid logical conditions and filter paths that do not exist in a non-empty resource return `400`.
|
|
305
|
-
|
|
306
|
-
Different simple query filters are combined with `AND`. Repeating the same equality filter selects any of its values, so `GET /movies?id=1&id=2` is equivalent to `GET /movies?id:in=1,2`. For an `in` filter, values are separated with commas. On an array field, `in` means that at least one field element matches at least one supplied value. `every` returns `true` for an empty array, while `some` returns `false`.
|
|
307
|
-
|
|
308
|
-
If `_where` is present, it is the complete filter and other simple filter parameters are ignored. The examples show readable JSON; an HTTP client must URL-encode `_where` when constructing the URL manually, for example with `encodeURIComponent(JSON.stringify(where))`.
|
|
309
|
-
|
|
310
|
-
Filtering is performed after `_embed`. This means a filter can address fields added by an embedded relation when the same request includes that `_embed`; stored `...Id` and `...Ids` fields can always be filtered directly.
|
|
311
|
-
|
|
312
|
-
## Relationships
|
|
313
|
-
|
|
314
|
-
Use `_embed` to add related records to the response:
|
|
315
|
-
|
|
316
|
-
```http
|
|
317
|
-
GET /movies/1?_embed=actors.user.country&_embed=actors.genres&_embed=publishers
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
The response contains actors, each actor's user and genres, the user's country, and publishers. Original ID fields remain in the response, and the database file is not modified. The server imposes no fixed depth limit, but every required level must be written explicitly in the finite `_embed` path:
|
|
321
|
-
|
|
322
|
-
```http
|
|
323
|
-
GET /movies/1?_embed=actors.user.country
|
|
324
|
-
GET /genres/2?_embed=parents.parents
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
Unknown or malformed `_embed` paths return `400`.
|
|
328
|
-
|
|
329
|
-
Pass `_embed` more than once, as above, or provide a comma-separated list in one parameter. Pagination applies only to the requested root collection; embedded related records are returned in full.
|
|
330
|
-
|
|
331
|
-
Reverse relationships work as well:
|
|
332
|
-
|
|
333
|
-
```http
|
|
334
|
-
GET /countries/1?_embed=users
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
Relations are inferred by convention. A field named `<relation>Id` creates a single relation, while `<relation>Ids` creates a collection relation. The relation name is matched to a top-level resource directly or through its singular form. For example:
|
|
338
|
-
|
|
339
|
-
- `countryId` points to `countries`;
|
|
340
|
-
- `userId` points to `users` when the requested relation is `user`;
|
|
341
|
-
- `genreIds` points to `genres`;
|
|
342
|
-
- `publisherIds` points to `publishers`;
|
|
343
|
-
- `parentIds` points back to the current resource when `_embed=parents` is requested; `_embed=children` resolves the reverse self-relation.
|
|
344
|
-
|
|
345
|
-
Reverse relations use the source resource name. For example, `_embed=users` on a country finds users whose nested data contains the corresponding `countryId`. Relations are resolved when requested, but referential integrity is not enforced when data is written.
|
|
346
|
-
|
|
347
|
-
Explicit `...Id` and `...Ids` fields determine relations. If a record also contains an outdated embedded value, `_embed` replaces that response property with the current related record. A missing related record becomes `null` for a single relation or is omitted from the resulting array for a collection relation. Per-request ID indexes are created only for resources used by the current relation lookup.
|
|
348
|
-
|
|
349
383
|
## Files
|
|
350
384
|
|
|
351
|
-
Add `files.directory` and `files.metadata` to the
|
|
385
|
+
Add `files.directory` and `files.metadata` to the configuration and start the server:
|
|
352
386
|
|
|
353
387
|
```bash
|
|
354
|
-
deep-json-server
|
|
388
|
+
deep-json-server server.config.js
|
|
355
389
|
```
|
|
356
390
|
|
|
357
391
|
For temporary tests, use `files.data` instead. Each initial record contains `name`, `mimeType`, binary `content` as a `Uint8Array`, and an optional `directory`. Uploaded files then remain in memory until the process exits.
|
|
@@ -407,176 +441,52 @@ Content-Type: application/json
|
|
|
407
441
|
|
|
408
442
|
`PATCH` returns the updated metadata with status `200`; if a file already exists at the new path, the server returns `409`. `DELETE` returns `204` without a response body. A missing file returns `404` on every path-based operation. File paths in URLs are relative to `files.directory`, and all returned URLs are relative to the server origin.
|
|
409
443
|
|
|
410
|
-
In disk mode, the binary is stored at `<files.directory>/<directory>/<name>`. The metadata file contains only `directory`, `mimeType`, and `name`; `size` is read from the actual file, while response URLs are computed. The server creates directories automatically and keeps validated metadata in memory while running. Use a disk-backed database and file storage from only one server process at a time, and do not edit stored files or metadata until that process stops. Paths below `files.directory` may not contain symbolic links, and file names are restricted to values that are portable across supported operating systems. The metadata file may be absent initially and is created on the first upload.
|
|
411
|
-
|
|
412
|
-
The upload is raw binary rather than `multipart/form-data`, so `XMLHttpRequest.upload.onprogress` can report progress while the browser sends a `File` directly with `xhr.send(file)`. 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.
|
|
413
|
-
|
|
414
|
-
## Database schema and OpenAPI generation
|
|
444
|
+
In disk mode, the binary is stored at `<files.directory>/<directory>/<name>`. The metadata file contains only `directory`, `mimeType`, and `name`; `size` is read from the actual file, while response URLs are computed. The server creates directories automatically and keeps validated metadata in memory while running. Use a disk-backed database and file storage from only one server process at a time, and do not edit stored files or metadata until that process stops. Paths below `files.directory` may not contain symbolic links, and file names are restricted to values that are portable across supported operating systems. The metadata file may be absent initially and is created on the first upload.
|
|
415
445
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
```json
|
|
419
|
-
{
|
|
420
|
-
"$info": {
|
|
421
|
-
"title": "Movie Catalog API",
|
|
422
|
-
"version": "1.0.0"
|
|
423
|
-
},
|
|
424
|
-
"$schema": {
|
|
425
|
-
"movies": {
|
|
426
|
-
"required": ["actors", "actors.genreIds", "actors.userId", "publisherIds", "title"],
|
|
427
|
-
"formats": {
|
|
428
|
-
"coverSrc": "uri"
|
|
429
|
-
}
|
|
430
|
-
},
|
|
431
|
-
"users": {
|
|
432
|
-
"required": ["bornAt", "fullName"],
|
|
433
|
-
"formats": {
|
|
434
|
-
"avatarSrc": "uri",
|
|
435
|
-
"bornAt": "date"
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
```
|
|
441
|
-
|
|
442
|
-
Schema configuration:
|
|
443
|
-
|
|
444
|
-
| Key | Purpose |
|
|
445
|
-
| --- | --- |
|
|
446
|
-
| `$info` | OpenAPI `info`; when present, non-empty `title` and `version` are required |
|
|
447
|
-
| `$schema.<resource>.name` | Explicit component name when automatic singularization is unsuitable or collides |
|
|
448
|
-
| `$schema.<resource>.required` | Required field paths; nested paths use dots, such as `actors.userId` |
|
|
449
|
-
| `$schema.<resource>.formats` | OpenAPI formats for inferred or explicit string fields, such as `date`, `date-time` or `uri` |
|
|
450
|
-
| `$schema.<resource>.properties` | Recursive OpenAPI-compatible field schemas merged with inference |
|
|
451
|
-
|
|
452
|
-
`formats` is shorthand for assigning `format` to an existing string field. `properties` can fully describe a field—including its `type`, `format`, constraints, and nested properties—or add a field that is absent from the data. If both mechanisms assign a format to the same field, the value from `formats` is applied last.
|
|
453
|
-
|
|
454
|
-
Set `openapi.path` in the server config, then generate an OpenAPI 3.0.3 file and exit:
|
|
455
|
-
|
|
456
|
-
```bash
|
|
457
|
-
deep-json-server --openapi-only server.config.js
|
|
458
|
-
```
|
|
459
|
-
|
|
460
|
-
To include file routes in the document, configure the `files` section and add `--files`: `deep-json-server --files --openapi-only server.config.js`.
|
|
461
|
-
|
|
462
|
-
The generator infers resources and field types from all database records. Every inferred field is optional by default, while the top-level `id` is always required in response schemas and is omitted from create and update request schemas. Add other required fields to `required`. A nested required path marks that nested property as required; it does not automatically make every parent path required, so list the parent separately when necessary.
|
|
463
|
-
|
|
464
|
-
Different non-overlapping value types are inferred independently and combined through `oneOf`; mixed integers and decimal numbers are represented by one `number` schema. Configuration is validated before generation: `$info`, resource and component names, supported `properties` keywords and their value types are checked, while paths from `required` and `formats` must exist in the resulting schema. Explicit component names may contain ASCII letters, digits, dots, underscores, and hyphens.
|
|
465
|
-
|
|
466
|
-
Use `properties` to describe fields that cannot be inferred, particularly for an empty resource. Explicit properties are merged with inferred properties:
|
|
467
|
-
|
|
468
|
-
```json
|
|
469
|
-
{
|
|
470
|
-
"$schema": {
|
|
471
|
-
"reviews": {
|
|
472
|
-
"properties": {
|
|
473
|
-
"rating": { "type": "integer", "minimum": 1, "maximum": 5 },
|
|
474
|
-
"text": { "type": "string" }
|
|
475
|
-
},
|
|
476
|
-
"required": ["rating"]
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
```
|
|
481
|
-
|
|
482
|
-
An empty resource still receives a required string `id` property because IDs created by the server are strings. Generation stops with an actionable error when resources produce duplicate schema names or operation IDs; use an explicit `name` to resolve schema-name collisions. The output directory is created automatically, and the configured YAML file is replaced on every generation.
|
|
483
|
-
|
|
484
|
-
`$info` becomes the OpenAPI `info` object, while resource settings live under `$schema`. In the CLI, the OpenAPI `servers` entry uses `server.host` and `server.port`, then the `HOST` and `PORT` environment-variable fallbacks, and finally `http://127.0.0.1:4001`. A direct `createServer()` call does not read those environment variables automatically: `server.openapi()` uses the config values or the same default URL.
|
|
485
|
-
|
|
486
|
-
Use `name` when a resource needs an explicit schema name instead of the automatically singularized name:
|
|
487
|
-
|
|
488
|
-
```json
|
|
489
|
-
{
|
|
490
|
-
"$schema": {
|
|
491
|
-
"equipment": {
|
|
492
|
-
"name": "Equipment"
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
```
|
|
497
|
-
|
|
498
|
-
The generated document describes CRUD endpoints, pagination, sorting, nested data filters, `_embed`, and both direct and reverse response relations inferred from `...Id` and `...Ids` fields. When `--files` is present, it also describes raw binary upload, download and deletion endpoints for arbitrary media types. A numeric database ID is described as `integer | string`, because a later `POST` creates a string ID in the same resource. Generation rejects duplicate schema names, operation IDs, and invalid schema overrides before producing a document. The document can be used as input for tools such as RTK Query OpenAPI Codegen. OpenAPI is generated only with `--openapi` or `--openapi-only`; normal server startup does not rewrite the file.
|
|
499
|
-
|
|
500
|
-
During normal startup, request bodies are validated against the same inferred and configured schemas. `POST` and `PUT` enforce configured required fields; `PATCH` validates only fields that are actually supplied. `formats` and `properties` apply to all three methods. Unlisted additional object fields remain allowed. Invalid bodies return `400`.
|
|
446
|
+
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.
|
|
501
447
|
|
|
502
448
|
## Programmatic API
|
|
503
449
|
|
|
504
450
|
```js
|
|
505
451
|
import { createServer } from '@kollors/deep-json-server';
|
|
452
|
+
import config from './server.config.js';
|
|
453
|
+
|
|
454
|
+
const facade = await createServer(config);
|
|
455
|
+
const openapi = await facade.openapi();
|
|
456
|
+
const sdl = await facade.graphql();
|
|
457
|
+
const server = facade.fastify();
|
|
458
|
+
await server.listen();
|
|
459
|
+
// await server.close();
|
|
460
|
+
```
|
|
506
461
|
|
|
507
|
-
|
|
508
|
-
database: {
|
|
509
|
-
path: 'mock/database.json',
|
|
510
|
-
schema: 'mock/database-schema.json',
|
|
511
|
-
},
|
|
512
|
-
files: {
|
|
513
|
-
directory: 'mock/files',
|
|
514
|
-
metadata: 'mock/files/_database.json',
|
|
515
|
-
},
|
|
516
|
-
openapi: {
|
|
517
|
-
path: 'mock/openapi-schema.yaml',
|
|
518
|
-
},
|
|
519
|
-
server: {
|
|
520
|
-
cors: true,
|
|
521
|
-
host: '127.0.0.1',
|
|
522
|
-
logger: false,
|
|
523
|
-
maxFileSize: 100 * 1024 * 1024,
|
|
524
|
-
maxPageSize: 1000,
|
|
525
|
-
port: 4001,
|
|
526
|
-
},
|
|
527
|
-
};
|
|
528
|
-
|
|
529
|
-
// Make a request without opening a network port—useful in automated tests.
|
|
530
|
-
const server = await createServer(config);
|
|
531
|
-
const fastify = server.fastify();
|
|
532
|
-
const response = await fastify.inject({ method: 'GET', url: '/movies' });
|
|
533
|
-
|
|
534
|
-
console.log(response.json());
|
|
535
|
-
|
|
536
|
-
// Return the document and write it to config.openapi.path.
|
|
537
|
-
const document = await server.openapi();
|
|
462
|
+
The `openapi()` and `graphql()` methods return schemas. `fastify()` returns the server instance for configuration and startup. The database and enabled services initialize on `ready()`, `listen()` or the first `inject()`; initialization errors stop startup. Override server features with `createServer(config, { files: false, graphql: true, openapi: true })`.
|
|
538
463
|
|
|
539
|
-
|
|
464
|
+
Generators can also be used independently:
|
|
540
465
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
const runningFastify = runningServer.fastify();
|
|
466
|
+
```js
|
|
467
|
+
import { generateOpenapi, generateGraphql, writeOpenapi, writeGraphql } from '@kollors/deep-json-server';
|
|
544
468
|
|
|
545
|
-
await
|
|
469
|
+
const document = await generateOpenapi('./schema.json', { files: true });
|
|
470
|
+
const sdl = await generateGraphql('./schema.json');
|
|
471
|
+
await writeOpenapi(document, './generated/openapi.yaml');
|
|
472
|
+
await writeGraphql(sdl, './generated/schema.graphql');
|
|
473
|
+
```
|
|
546
474
|
|
|
547
|
-
|
|
548
|
-
await runningFastify.close();
|
|
475
|
+
`generateOpenapi()` also accepts `host`, `port`, `pageSize`, `maxPageSize` and `info`. Pass a schema object instead of a path if preferred. Servers and generators use their own copy of the model.
|
|
549
476
|
|
|
550
|
-
|
|
551
|
-
const memoryServer = await createServer({
|
|
552
|
-
database: {
|
|
553
|
-
data: { movies: [{ id: '1', title: 'Shadows of Ardenia' }] },
|
|
554
|
-
schema: { $info: { title: 'Movie API', version: '1.0.0' } },
|
|
555
|
-
},
|
|
556
|
-
files: {
|
|
557
|
-
data: [{ content: new Uint8Array([1, 2, 3]), directory: 'examples', mimeType: 'application/octet-stream', name: 'example.bin' }],
|
|
558
|
-
},
|
|
559
|
-
});
|
|
477
|
+
## Storage and development
|
|
560
478
|
|
|
561
|
-
|
|
562
|
-
const memoryResponse = await memoryFastify.inject({ method: 'GET', url: '/movies/1' });
|
|
479
|
+
Updates run sequentially within one server instance and are validated on a copy of the data before saving. Use one server process per database file. `increment` counters are stored next to the database in `<database path>.counters.json`; keep that file with the database. Numbers are reserved before the data write, so a failed write can leave gaps but cannot reuse a reserved number.
|
|
563
480
|
|
|
564
|
-
|
|
481
|
+
The server is intended for mocking APIs. Implement authentication and password hashing in your application if needed.
|
|
565
482
|
|
|
566
|
-
|
|
483
|
+
```sh
|
|
484
|
+
npm ci
|
|
485
|
+
npm run verify
|
|
567
486
|
```
|
|
568
487
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
| Method | Purpose |
|
|
572
|
-
| --- | --- |
|
|
573
|
-
| `server.fastify()` | Lazily creates and caches the real Fastify instance; every native method remains available, and argument-less `listen()` uses `server.host` and `server.port` |
|
|
574
|
-
| `server.openapi()` | Returns an OpenAPI document and also writes it when `openapi.path` is configured |
|
|
575
|
-
|
|
576
|
-
File routes are enabled programmatically when a `files` section is present. The second argument has the shape `{ files?: boolean }`: pass `{ files: false }` to keep a configured store disabled, or `{ files: true }` to require a `files` section and enable the routes. `server.openapi()` uses the same file-route setting as `server.fastify()`.
|
|
577
|
-
|
|
578
|
-
An argument-less `server.fastify().listen()` uses `server.host` and `server.port`, falling back to `127.0.0.1:4001`. Explicit `listen(options)` values take precedence. Relative paths passed directly to `createServer()` resolve from the current working directory; paths loaded from `server.config.js` resolve from the config directory. The package includes generated TypeScript declarations for the returned object and every config variant.
|
|
488
|
+
The command checks types, code style, test coverage and installation from the package archive.
|
|
579
489
|
|
|
580
|
-
|
|
490
|
+
To publish a new alpha, update the version in `package.json` and push to `main`. GitHub Actions creates the version tag and publishes to npm `alpha` through trusted publishing. Already published versions are skipped. If the tag exists but publication failed, a retry uses that tag and verifies that the package files match it. Pushing a version tag also triggers publication; stable versions publish to `latest`.
|
|
581
491
|
|
|
582
|
-
|
|
492
|
+
License: MIT.
|