@kollors/deep-json-server 1.0.0-alpha.6 → 1.0.0-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +373 -230
- package/README.ru.md +370 -227
- package/dist/bin/deep-json-server.js +1 -1
- package/dist/bin/deep-json-server.js.map +1 -1
- package/dist/index.d.ts +10 -8
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/src/auth/contract.d.ts +6 -2
- package/dist/src/auth/contract.js +8 -1
- package/dist/src/auth/contract.js.map +1 -1
- package/dist/src/auth/input.d.ts +18 -0
- package/dist/src/auth/input.js +34 -0
- package/dist/src/auth/input.js.map +1 -0
- package/dist/src/auth/password.d.ts +13 -0
- package/dist/src/auth/password.js +17 -1
- package/dist/src/auth/password.js.map +1 -1
- package/dist/src/auth/routes.d.ts +3 -0
- package/dist/src/auth/routes.js +6 -0
- package/dist/src/auth/routes.js.map +1 -1
- package/dist/src/auth/service.d.ts +64 -4
- package/dist/src/auth/service.js +167 -61
- package/dist/src/auth/service.js.map +1 -1
- package/dist/src/auth/store.d.ts +26 -0
- package/dist/src/auth/store.js +82 -0
- package/dist/src/auth/store.js.map +1 -0
- package/dist/src/cli/index.d.ts +7 -0
- package/dist/src/{cli.js → cli/index.js} +31 -15
- package/dist/src/cli/index.js.map +1 -0
- package/dist/src/core/config-values.d.ts +28 -0
- package/dist/src/core/config-values.js +53 -0
- package/dist/src/core/config-values.js.map +1 -0
- package/dist/src/{constants.d.ts → core/constants.d.ts} +1 -1
- package/dist/src/{constants.js → core/constants.js} +1 -1
- package/dist/src/core/constants.js.map +1 -0
- package/dist/src/core/database.d.ts +53 -0
- package/dist/src/{database.js → core/database.js} +23 -3
- package/dist/src/core/database.js.map +1 -0
- package/dist/src/core/engine.d.ts +65 -0
- package/dist/src/{engine.js → core/engine.js} +114 -59
- package/dist/src/core/engine.js.map +1 -0
- package/dist/src/{errors.d.ts → core/errors.d.ts} +4 -1
- package/dist/src/core/errors.js +12 -0
- package/dist/src/core/errors.js.map +1 -0
- package/dist/src/core/http-errors.d.ts +9 -0
- package/dist/src/core/http-errors.js +15 -0
- package/dist/src/core/http-errors.js.map +1 -0
- package/dist/src/core/lifecycle/mutation.d.ts +49 -0
- package/dist/src/core/lifecycle/mutation.js +170 -0
- package/dist/src/core/lifecycle/mutation.js.map +1 -0
- package/dist/src/core/lifecycle/options.d.ts +21 -0
- package/dist/src/core/lifecycle/options.js +19 -0
- package/dist/src/core/lifecycle/options.js.map +1 -0
- package/dist/src/core/model.d.ts +131 -0
- package/dist/src/{model.js → core/model.js} +106 -10
- package/dist/src/core/model.js.map +1 -0
- package/dist/src/{mutations → core/mutations}/write.d.ts +4 -4
- package/dist/src/{mutations → core/mutations}/write.js +14 -4
- package/dist/src/core/mutations/write.js.map +1 -0
- package/dist/src/core/operations.d.ts +33 -0
- package/dist/src/core/operations.js +10 -0
- package/dist/src/core/operations.js.map +1 -0
- package/dist/src/core/pagination.d.ts +10 -0
- package/dist/src/core/pagination.js +13 -0
- package/dist/src/core/pagination.js.map +1 -0
- package/dist/src/core/paths.d.ts +16 -0
- package/dist/src/{paths.js → core/paths.js} +16 -0
- package/dist/src/core/paths.js.map +1 -0
- package/dist/src/core/query/contract.d.ts +6 -0
- package/dist/src/{query → core/query}/contract.js +3 -0
- package/dist/src/core/query/contract.js.map +1 -0
- package/dist/src/core/query/filter.d.ts +6 -0
- package/dist/src/{query → core/query}/filter.js +18 -4
- package/dist/src/core/query/filter.js.map +1 -0
- package/dist/src/core/query/options.d.ts +30 -0
- package/dist/src/{query → core/query}/options.js +12 -0
- package/dist/src/core/query/options.js.map +1 -0
- package/dist/src/core/records.d.ts +46 -0
- package/dist/src/{records.js → core/records.js} +24 -3
- package/dist/src/core/records.js.map +1 -0
- package/dist/src/core/relation-metadata.d.ts +15 -0
- package/dist/src/{relation-metadata.js → core/relation-metadata.js} +7 -1
- package/dist/src/core/relation-metadata.js.map +1 -0
- package/dist/src/core/types.d.ts +10 -0
- package/dist/src/{types.js.map → core/types.js.map} +1 -1
- package/dist/src/core/utils.d.ts +56 -0
- package/dist/src/core/utils.js +102 -0
- package/dist/src/core/utils.js.map +1 -0
- package/dist/src/files/contract.d.ts +33 -0
- package/dist/src/files/contract.js +25 -3
- package/dist/src/files/contract.js.map +1 -1
- package/dist/src/files/disk-store.d.ts +3 -0
- package/dist/src/files/disk-store.js +25 -18
- package/dist/src/files/disk-store.js.map +1 -1
- package/dist/src/files/http.d.ts +8 -3
- package/dist/src/files/http.js +9 -0
- package/dist/src/files/http.js.map +1 -1
- package/dist/src/files/index.d.ts +4 -3
- package/dist/src/files/index.js +3 -1
- package/dist/src/files/index.js.map +1 -1
- package/dist/src/files/memory-store.d.ts +4 -1
- package/dist/src/files/memory-store.js +21 -15
- package/dist/src/files/memory-store.js.map +1 -1
- package/dist/src/files/routes.d.ts +3 -0
- package/dist/src/files/routes.js +25 -1
- package/dist/src/files/routes.js.map +1 -1
- package/dist/src/files/streams.d.ts +5 -0
- package/dist/src/files/streams.js +20 -0
- package/dist/src/files/streams.js.map +1 -0
- package/dist/src/graphql/entry.d.ts +12 -3
- package/dist/src/graphql/entry.js +12 -2
- package/dist/src/graphql/entry.js.map +1 -1
- package/dist/src/graphql/generate.d.ts +11 -0
- package/dist/src/graphql/generate.js +17 -0
- package/dist/src/graphql/generate.js.map +1 -0
- package/dist/src/graphql/preflight.d.ts +4 -1
- package/dist/src/graphql/preflight.js +3 -0
- package/dist/src/graphql/preflight.js.map +1 -1
- package/dist/src/graphql/resolvers.d.ts +10 -2
- package/dist/src/graphql/resolvers.js +8 -2
- package/dist/src/graphql/resolvers.js.map +1 -1
- package/dist/src/graphql/routes.d.ts +6 -2
- package/dist/src/graphql/routes.js +6 -3
- package/dist/src/graphql/routes.js.map +1 -1
- package/dist/src/graphql/schema.d.ts +6 -0
- package/dist/src/{graphql.js → graphql/schema.js} +20 -14
- package/dist/src/graphql/schema.js.map +1 -0
- package/dist/src/graphql/write.d.ts +3 -0
- package/dist/src/graphql/write.js +3 -0
- package/dist/src/graphql/write.js.map +1 -1
- package/dist/src/openapi/auth.d.ts +13 -0
- package/dist/src/openapi/auth.js +139 -0
- package/dist/src/openapi/auth.js.map +1 -0
- package/dist/src/openapi/document.d.ts +5 -2
- package/dist/src/openapi/document.js +41 -42
- package/dist/src/openapi/document.js.map +1 -1
- package/dist/src/openapi/entry.d.ts +14 -3
- package/dist/src/openapi/entry.js +12 -1
- package/dist/src/openapi/entry.js.map +1 -1
- package/dist/src/openapi/files.d.ts +6 -0
- package/dist/src/{files/openapi.js → openapi/files.js} +6 -3
- package/dist/src/openapi/files.js.map +1 -0
- package/dist/src/openapi/generate.d.ts +19 -0
- package/dist/src/openapi/generate.js +39 -0
- package/dist/src/openapi/generate.js.map +1 -0
- package/dist/src/openapi/helpers.d.ts +10 -1
- package/dist/src/openapi/helpers.js +9 -0
- package/dist/src/openapi/helpers.js.map +1 -1
- package/dist/src/openapi/options.d.ts +20 -0
- package/dist/src/openapi/options.js +13 -0
- package/dist/src/openapi/options.js.map +1 -0
- package/dist/src/{types.d.ts → openapi/types.d.ts} +1 -10
- package/dist/src/openapi/types.js +2 -0
- package/dist/src/openapi/types.js.map +1 -0
- package/dist/src/openapi/write.d.ts +5 -0
- package/dist/src/openapi/write.js +12 -0
- package/dist/src/openapi/write.js.map +1 -0
- package/dist/src/rest/options.d.ts +11 -2
- package/dist/src/rest/options.js +12 -3
- package/dist/src/rest/options.js.map +1 -1
- package/dist/src/rest/projection.d.ts +10 -4
- package/dist/src/rest/projection.js +10 -4
- package/dist/src/rest/projection.js.map +1 -1
- package/dist/src/rest/routes.d.ts +6 -2
- package/dist/src/rest/routes.js +12 -11
- package/dist/src/rest/routes.js.map +1 -1
- package/dist/src/server/config.d.ts +76 -0
- package/dist/src/{config.js → server/config.js} +71 -74
- package/dist/src/server/config.js.map +1 -0
- package/dist/src/server/create.d.ts +18 -0
- package/dist/src/{server.js → server/create.js} +33 -28
- package/dist/src/server/create.js.map +1 -0
- package/dist/src/server/features.d.ts +15 -0
- package/dist/src/{features.js → server/features.js} +8 -2
- package/dist/src/server/features.js.map +1 -0
- package/dist/src/server/public.d.ts +3 -3
- package/dist/src/server/public.js +1 -1
- package/dist/src/server/public.js.map +1 -1
- package/package.json +1 -1
- package/dist/src/auth/openapi.d.ts +0 -10
- package/dist/src/auth/openapi.js +0 -62
- package/dist/src/auth/openapi.js.map +0 -1
- package/dist/src/cli.d.ts +0 -4
- package/dist/src/cli.js.map +0 -1
- package/dist/src/config.d.ts +0 -86
- package/dist/src/config.js.map +0 -1
- package/dist/src/constants.js.map +0 -1
- package/dist/src/database.d.ts +0 -20
- package/dist/src/database.js.map +0 -1
- package/dist/src/engine.d.ts +0 -36
- package/dist/src/engine.js.map +0 -1
- package/dist/src/errors.js +0 -9
- package/dist/src/errors.js.map +0 -1
- package/dist/src/features.d.ts +0 -9
- package/dist/src/features.js.map +0 -1
- package/dist/src/files/openapi.d.ts +0 -3
- package/dist/src/files/openapi.js.map +0 -1
- package/dist/src/graphql/public.d.ts +0 -3
- package/dist/src/graphql/public.js +0 -11
- package/dist/src/graphql/public.js.map +0 -1
- package/dist/src/graphql.d.ts +0 -3
- package/dist/src/graphql.js.map +0 -1
- package/dist/src/http/errors.d.ts +0 -6
- package/dist/src/http/errors.js +0 -3
- package/dist/src/http/errors.js.map +0 -1
- package/dist/src/model.d.ts +0 -72
- package/dist/src/model.js.map +0 -1
- package/dist/src/mutations/write.js.map +0 -1
- package/dist/src/openapi/index.d.ts +0 -7
- package/dist/src/openapi/index.js +0 -28
- package/dist/src/openapi/index.js.map +0 -1
- package/dist/src/openapi/public.d.ts +0 -18
- package/dist/src/openapi/public.js +0 -20
- package/dist/src/openapi/public.js.map +0 -1
- package/dist/src/pagination.d.ts +0 -7
- package/dist/src/pagination.js +0 -12
- package/dist/src/pagination.js.map +0 -1
- package/dist/src/paths.d.ts +0 -7
- package/dist/src/paths.js.map +0 -1
- package/dist/src/query/contract.d.ts +0 -3
- package/dist/src/query/contract.js.map +0 -1
- package/dist/src/query/filter.d.ts +0 -3
- package/dist/src/query/filter.js.map +0 -1
- package/dist/src/query/options.d.ts +0 -18
- package/dist/src/query/options.js.map +0 -1
- package/dist/src/records.d.ts +0 -25
- package/dist/src/records.js.map +0 -1
- package/dist/src/relation-metadata.d.ts +0 -9
- package/dist/src/relation-metadata.js.map +0 -1
- package/dist/src/schema.d.ts +0 -8
- package/dist/src/schema.js +0 -13
- package/dist/src/schema.js.map +0 -1
- package/dist/src/server.d.ts +0 -12
- package/dist/src/server.js.map +0 -1
- package/dist/src/utils.d.ts +0 -12
- package/dist/src/utils.js +0 -58
- package/dist/src/utils.js.map +0 -1
- /package/dist/src/{types.js → core/types.js} +0 -0
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
[Русский](README.ru.md)
|
|
4
4
|
|
|
5
|
-
A JSON
|
|
5
|
+
A JSON mock server with REST, GraphQL, related records, file uploads and schema exports. Supports user login, owner and administrator permissions, record timestamps and soft deletion. Requires Node.js 22 or newer.
|
|
6
6
|
|
|
7
|
-
**1.0.0-alpha.
|
|
7
|
+
**1.0.0-alpha.8 is a prerelease.** REST queries use `scope=[fields, arguments?]` at every level. When upgrading from an earlier version, update query parameters using the examples below; upgrading from 0.x also requires the new model schema.
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
@@ -12,7 +12,7 @@ A JSON-backed mock server with REST, GraphQL, nested queries, binary files and s
|
|
|
12
12
|
npm install @kollors/deep-json-server@alpha
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
To install a specific version, use `@1.0.0-alpha.
|
|
15
|
+
To install a specific version, use `@1.0.0-alpha.8`.
|
|
16
16
|
|
|
17
17
|
## Quick start
|
|
18
18
|
|
|
@@ -42,12 +42,16 @@ npx deep-json-server server.config.js
|
|
|
42
42
|
|
|
43
43
|
The user list is available at `http://127.0.0.1:4001/users`.
|
|
44
44
|
|
|
45
|
+
Add a [model schema](#model-schema) to define relations and validation. See [queries](#queries-and-responses), [authentication](#authentication), [soft deletion](#record-dates-deletion-and-ownership), [files](#files) and the [programmatic API](#programmatic-api) for more.
|
|
46
|
+
|
|
45
47
|
## Configuration
|
|
46
48
|
|
|
47
49
|
| Setting | Meaning |
|
|
48
50
|
|---|---|
|
|
49
|
-
| `database.path` / `database.data` |
|
|
51
|
+
| `database.path` / `database.data` | Choose one to start the server: a JSON file or an in-memory collection object |
|
|
50
52
|
| `database.schema` | Model object or JSON schema-file path; optional for REST |
|
|
53
|
+
| `database.timestamps` | Add creation and update times; default `false` |
|
|
54
|
+
| `database.softDelete` | Mark records as deleted and allow restoration; default `false` |
|
|
51
55
|
| `openapi.enabled` | Enable the specification endpoint; default `false` |
|
|
52
56
|
| `openapi.endpoint` | Specification path; default `/openapi.json` |
|
|
53
57
|
| `openapi.path` | YAML export destination |
|
|
@@ -55,7 +59,6 @@ The user list is available at `http://127.0.0.1:4001/users`.
|
|
|
55
59
|
| `graphql.enabled` | Enable GraphQL HTTP endpoint; default `false` |
|
|
56
60
|
| `graphql.endpoint` | Endpoint path; default `/graphql` |
|
|
57
61
|
| `graphql.path` | GraphQL SDL export destination |
|
|
58
|
-
| `auth.enabled` | Enable authentication; default `false` |
|
|
59
62
|
| `auth.users` | Path to a JSON array of auth users, or an in-memory array |
|
|
60
63
|
| `auth.expiresIn` | Session lifetime in seconds; default 3600 |
|
|
61
64
|
| `server.host`, `server.port` | Defaults `127.0.0.1`, `4001`; CLI also reads `HOST`/`PORT` |
|
|
@@ -69,38 +72,21 @@ Relative paths resolve from the configuration file's directory. When passing a c
|
|
|
69
72
|
|
|
70
73
|
Set `server.port` to `0` to let the operating system choose an available port. The OpenAPI endpoint uses a relative server URL.
|
|
71
74
|
|
|
75
|
+
### CLI
|
|
76
|
+
|
|
72
77
|
| CLI flag | Action |
|
|
73
78
|
|---|---|
|
|
74
|
-
| `--files` | Enable file routes |
|
|
79
|
+
| `--files` | Enable file routes; requires `files` configuration |
|
|
80
|
+
| `--timestamps` | Enable record timestamps globally |
|
|
81
|
+
| `--soft-delete` | Enable soft deletion globally |
|
|
75
82
|
| `--graphql` | Enable the GraphQL API |
|
|
76
83
|
| `--openapi` | Enable the OpenAPI endpoint |
|
|
77
|
-
| `--auth` | Enable authentication using `auth.users` |
|
|
78
84
|
| `--host <host>` | Server address |
|
|
79
85
|
| `--port <port>` | Server port |
|
|
80
86
|
| `--help`, `-h` | Show help |
|
|
81
87
|
| `--version`, `-v` | Show package version |
|
|
82
88
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
To generate schemas, specify the format and configuration file:
|
|
86
|
-
|
|
87
|
-
```sh
|
|
88
|
-
npx deep-json-server generate openapi server.config.js
|
|
89
|
-
npx deep-json-server generate graphql server.config.js
|
|
90
|
-
npx deep-json-server generate openapi,graphql server.config.js
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
The command reads `database.schema` and writes schemas to `openapi.path` and `graphql.path`. A configuration for generation only can contain:
|
|
94
|
-
|
|
95
|
-
```js
|
|
96
|
-
export default {
|
|
97
|
-
database: { schema: './schema.json' },
|
|
98
|
-
openapi: { path: './generated/openapi.yaml' },
|
|
99
|
-
graphql: { path: './generated/schema.graphql' },
|
|
100
|
-
};
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
Each format needs its own output file. The command rejects destinations that would overwrite the configuration, database, schema, auth users or file metadata.
|
|
89
|
+
Host and port priority: CLI → configuration → `HOST`/`PORT` → defaults. The `files` section enables file routes; `auth` enables login and permission checks. Model settings can override global `timestamps` and `softDelete` values.
|
|
104
90
|
|
|
105
91
|
## Model schema
|
|
106
92
|
|
|
@@ -142,6 +128,8 @@ Explicit schemas are strict: undeclared fields and collections are rejected, exc
|
|
|
142
128
|
|
|
143
129
|
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=[{"*":true}]`. Fields with mixed value types can be read, but filtering, ordering and paging heterogeneous lists require an explicit schema.
|
|
144
130
|
|
|
131
|
+
Each model requires `collection`, the database collection and REST path name, and `fields`, its field definitions. The model name (`User`) determines GraphQL type and operation names. `api` controls format availability; `timestamps` and `softDelete` override global settings for that model.
|
|
132
|
+
|
|
145
133
|
### Fields
|
|
146
134
|
|
|
147
135
|
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`.
|
|
@@ -172,10 +160,12 @@ Objects used in GraphQL must have at least one field visible in responses; REST
|
|
|
172
160
|
### Relations
|
|
173
161
|
|
|
174
162
|
```json
|
|
175
|
-
|
|
176
|
-
"
|
|
177
|
-
|
|
178
|
-
|
|
163
|
+
{
|
|
164
|
+
"actors.genres": {
|
|
165
|
+
"type": "Genre[]",
|
|
166
|
+
"source": "actors.genreIds",
|
|
167
|
+
"required": true
|
|
168
|
+
}
|
|
179
169
|
}
|
|
180
170
|
```
|
|
181
171
|
|
|
@@ -183,19 +173,25 @@ Objects used in GraphQL must have at least one field visible in responses; REST
|
|
|
183
173
|
|
|
184
174
|
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.
|
|
185
175
|
|
|
186
|
-
Reverse example: `User.movies = {"type":"Movie[]","target":"actors.userId"}`. A movie is returned once even if several actors match. A single relation
|
|
176
|
+
Reverse example: `User.movies = {"type":"Movie[]","target":"actors.userId"}`. A movie is returned once even if several actors match. A single relation that matches multiple records causes an error.
|
|
187
177
|
|
|
188
178
|
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`.
|
|
189
179
|
|
|
190
180
|
`onDelete` describes what happens **when a target record is deleted**:
|
|
191
181
|
|
|
192
|
-
- `restrict` (default): refuse deletion while a
|
|
193
|
-
- `cascade`: delete the referring
|
|
182
|
+
- `restrict` (default): refuse deletion while a retained record refers to the target.
|
|
183
|
+
- `cascade`: delete the referring record. For `User.country`, deleting the country deletes its users. For `Movie.actors.user`, deleting the user removes matching actor elements and retains the movie.
|
|
194
184
|
|
|
195
185
|
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.
|
|
196
186
|
|
|
197
187
|
## Queries and responses
|
|
198
188
|
|
|
189
|
+
The movie, actor and genre examples use the full [example schema](examples/schema.json). Run them with the [example configuration](examples/server.config.js):
|
|
190
|
+
|
|
191
|
+
```sh
|
|
192
|
+
npx deep-json-server examples/server.config.js
|
|
193
|
+
```
|
|
194
|
+
|
|
199
195
|
Collections and lists of objects, including embedded `object[]` fields, return:
|
|
200
196
|
|
|
201
197
|
```json
|
|
@@ -206,7 +202,7 @@ Primitive arrays are returned as plain arrays. Every object list accepts optiona
|
|
|
206
202
|
|
|
207
203
|
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`.
|
|
208
204
|
|
|
209
|
-
`where` uses field operators `eq`, `ne`, `in`, string `contains`/`startsWith`/`endsWith`, and comparisons `gt`, `gte`, `lt`, `lte`.
|
|
205
|
+
`where` uses field operators `eq`, `ne`, `in`, string `contains`/`startsWith`/`endsWith`, and comparisons `gt`, `gte`, `lt`, `lte`. Conditions in the same object must all match. `and` and `or` take arrays of conditions; `not` takes one condition and can also be used inside a field filter. Arrays support `some`, `every`, `none`; primitive arrays also support `contains`, `in`. String `contains`, `startsWith` and `endsWith` ignore case; `eq`, `ne` and `in` compare exact values. A field condition is an object containing an operator, such as `{ "id": { "eq": "1" } }`.
|
|
210
206
|
|
|
211
207
|
```json
|
|
212
208
|
{
|
|
@@ -224,10 +220,12 @@ Use `page` and `pageSize` in `pager`. The default is the first page with the siz
|
|
|
224
220
|
|
|
225
221
|
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.
|
|
226
222
|
|
|
227
|
-
`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`.
|
|
223
|
+
`order` is an array of `{ "field": "fullName", "direction": "ASC" }` rules. `ASC` sorts in ascending order and `DESC` in descending order. 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`.
|
|
228
224
|
|
|
229
225
|
### REST
|
|
230
226
|
|
|
227
|
+
`GET /` returns collection names: `{ "resources": ["users", "movies"] }`. Each collection has these routes:
|
|
228
|
+
|
|
231
229
|
| Method | Path | Operation |
|
|
232
230
|
|---|---|---|
|
|
233
231
|
| GET | `/users` | `userList` |
|
|
@@ -237,62 +235,13 @@ Root `where` selects records from the main collection. `where` inside a relation
|
|
|
237
235
|
| PATCH | `/users/{id}` | `userUpdate` |
|
|
238
236
|
| DELETE | `/users/{id}` | `userDelete` |
|
|
239
237
|
|
|
240
|
-
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`.
|
|
238
|
+
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`.
|
|
241
239
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
Storage keys such as `genreIds: ["1"]` only set a relation. Relation fields also accept records to create or update:
|
|
245
|
-
|
|
246
|
-
```http
|
|
247
|
-
PATCH /movies/1
|
|
248
|
-
Content-Type: application/json
|
|
249
|
-
|
|
250
|
-
{
|
|
251
|
-
"actors": [
|
|
252
|
-
{
|
|
253
|
-
"userId": "1",
|
|
254
|
-
"genres": [
|
|
255
|
-
"1",
|
|
256
|
-
{ "id": "2", "name": "Updated genre" },
|
|
257
|
-
{ "name": "New genre" }
|
|
258
|
-
]
|
|
259
|
-
}
|
|
260
|
-
]
|
|
261
|
-
}
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
| Relation value | Behavior |
|
|
265
|
-
|---|---|
|
|
266
|
-
| A key, such as `"1"` | Link an existing record without changing it |
|
|
267
|
-
| An object with a primary key | PATCH updates supplied fields; PUT replaces the related record |
|
|
268
|
-
| An object without a primary key | Create a related record with defaults and a generated key |
|
|
269
|
-
|
|
270
|
-
The key name and type follow the target model. An object containing only a key still counts as an update: in PUT it must include the model's required fields. Replacement preserves primary keys, generated values and `readOnly` fields. In POST, nested objects with existing keys receive partial updates. A missing target is an error; creating a nested record without a key requires an autogenerated primary key.
|
|
271
|
-
|
|
272
|
-
A supplied list replaces the relation's membership. PATCH preserves omitted relations; PUT clears omitted writable links. `[]` clears a list and `null` clears a nullable single relation. Removing a link does not delete the related record. Required relations must remain populated.
|
|
273
|
-
|
|
274
|
-
Use either the relation field or its storage key in an object, for example `genres` or `genreIds`. Reverse relations update the target key. If a target path crosses an array and the server cannot identify one element to attach, provide the array with the intended keys explicitly. Protected keys cannot be changed.
|
|
275
|
-
|
|
276
|
-
All nested changes belong to the main record's transaction. A validation error, missing record or invalid response selection rolls back the entire operation. Updating a shared record affects every record linked to it.
|
|
277
|
-
|
|
278
|
-
GraphQL accepts typed objects in relation fields. To change only the links in a replace mutation, use storage keys such as `genreIds`. For example:
|
|
279
|
-
|
|
280
|
-
```graphql
|
|
281
|
-
mutation {
|
|
282
|
-
movieUpdate(id: "1", data: {
|
|
283
|
-
actors: [{
|
|
284
|
-
userId: "1"
|
|
285
|
-
genres: [{ id: "2", name: "Updated genre" }, { name: "New genre" }]
|
|
286
|
-
}]
|
|
287
|
-
}) {
|
|
288
|
-
actors { data { genres { data { id name } } } }
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
```
|
|
240
|
+
POST returns the created record with status `201`; PUT, PATCH and DELETE return the updated or deleted record with status `200`. REST errors use `{ "error": "Error description" }`.
|
|
292
241
|
|
|
293
242
|
### REST query parameters
|
|
294
243
|
|
|
295
|
-
REST
|
|
244
|
+
REST record routes accept one query parameter, `scope`, containing a JSON array `[fields, arguments?]`. The first object selects fields; the optional second object supplies `where`, `order` and `pager` for a list. The same format applies to the root query, embedded objects and relations.
|
|
296
245
|
|
|
297
246
|
Select users and their movies with independent ordering and pagination:
|
|
298
247
|
|
|
@@ -344,13 +293,71 @@ Omitting `scope` returns own fields, as with `[{"*":true}]`. An empty selection
|
|
|
344
293
|
|
|
345
294
|
Arguments are available only on lists. Single-record queries and mutation responses can set arguments on their embedded lists. Parameters are validated even on empty data; an invalid response selection rolls back record changes. Invalid scopes return `400`. The JSON length limit is 10,000 characters; selection depth is limited to 32 levels.
|
|
346
295
|
|
|
347
|
-
|
|
296
|
+
### Nested writes
|
|
297
|
+
|
|
298
|
+
Storage keys such as `genreIds: ["1"]` only set a relation. Relation fields also accept records to create or update:
|
|
299
|
+
|
|
300
|
+
```http
|
|
301
|
+
PATCH /movies/1
|
|
302
|
+
Content-Type: application/json
|
|
303
|
+
|
|
304
|
+
{
|
|
305
|
+
"actors": [
|
|
306
|
+
{
|
|
307
|
+
"userId": "1",
|
|
308
|
+
"genres": [
|
|
309
|
+
"1",
|
|
310
|
+
{ "id": "2", "name": "Updated genre" },
|
|
311
|
+
{ "name": "New genre" }
|
|
312
|
+
]
|
|
313
|
+
}
|
|
314
|
+
]
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
| Relation value | Behavior |
|
|
319
|
+
|---|---|
|
|
320
|
+
| A key, such as `"1"` | Link an existing record without changing it |
|
|
321
|
+
| An object with a primary key | PATCH updates supplied fields; PUT replaces the related record |
|
|
322
|
+
| An object without a primary key | Create a related record with defaults and a generated key |
|
|
323
|
+
|
|
324
|
+
The key name and type follow the target model. An object containing only a key still counts as an update: in PUT it must include the model's required fields. Replacement preserves primary keys, generated values and `readOnly` fields. In POST, nested objects with existing keys receive partial updates. A missing target is an error; creating a nested record without a key requires an autogenerated primary key.
|
|
325
|
+
|
|
326
|
+
A supplied list replaces the relation's membership. PATCH preserves omitted relations; PUT clears omitted writable links. `[]` clears a list and `null` clears a nullable single relation. Removing a link does not delete the related record. Required relations must remain populated.
|
|
327
|
+
|
|
328
|
+
Use either the relation field or its storage key in an object, for example `genres` or `genreIds`. Reverse relations update the target key. If a target path crosses an array and the server cannot identify one element to attach, provide the array with the intended keys explicitly. Protected keys cannot be changed.
|
|
329
|
+
|
|
330
|
+
All nested changes belong to the main record's transaction. A validation error, missing record or invalid response selection rolls back the entire operation. Updating a shared record affects every record linked to it.
|
|
331
|
+
|
|
332
|
+
GraphQL accepts typed objects in relation fields. To change only the links in a replace mutation, use storage keys such as `genreIds`. For example:
|
|
333
|
+
|
|
334
|
+
```graphql
|
|
335
|
+
mutation {
|
|
336
|
+
movieUpdate(id: "1", data: {
|
|
337
|
+
actors: [{
|
|
338
|
+
userId: "1"
|
|
339
|
+
genres: [{ id: "2", name: "Updated genre" }, { name: "New genre" }]
|
|
340
|
+
}]
|
|
341
|
+
}) {
|
|
342
|
+
actors { data { genres { data { id name } } } }
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
```
|
|
348
346
|
|
|
349
347
|
### GraphQL
|
|
350
348
|
|
|
349
|
+
Set `database.schema` and enable `graphql.enabled: true` in the configuration, or start the server with `--graphql`:
|
|
350
|
+
|
|
351
|
+
```sh
|
|
352
|
+
npx deep-json-server --graphql server.config.js
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Send requests to `/graphql` using POST with `Content-Type: application/json` and a body of `{ "query": "…", "variables": {} }`. Change the path through `graphql.endpoint`.
|
|
356
|
+
|
|
351
357
|
```graphql
|
|
352
358
|
query {
|
|
353
359
|
userList(
|
|
360
|
+
where: { fullName: { contains: "Мира" } }
|
|
354
361
|
order: [{ field: fullName, direction: ASC }]
|
|
355
362
|
pager: { page: 1, pageSize: 20 }
|
|
356
363
|
) {
|
|
@@ -358,7 +365,11 @@ query {
|
|
|
358
365
|
data {
|
|
359
366
|
id
|
|
360
367
|
fullName
|
|
361
|
-
movies(
|
|
368
|
+
movies(
|
|
369
|
+
where: { title: { contains: "Тени" } }
|
|
370
|
+
order: [{ field: title, direction: ASC }]
|
|
371
|
+
pager: { pageSize: 5 }
|
|
372
|
+
) {
|
|
362
373
|
total
|
|
363
374
|
data { id title }
|
|
364
375
|
}
|
|
@@ -369,7 +380,272 @@ query {
|
|
|
369
380
|
|
|
370
381
|
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.
|
|
371
382
|
|
|
372
|
-
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.
|
|
383
|
+
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.
|
|
384
|
+
|
|
385
|
+
Errors include `extensions.code`: `INVALID_INPUT`, `INVALID_QUERY`, `NOT_FOUND`, `CONFLICT`, `UNAUTHENTICATED`, `FORBIDDEN` or `INTERNAL_ERROR`. GraphQL syntax and type errors appear in the standard `errors` array. Query depth is limited to 32 levels.
|
|
386
|
+
|
|
387
|
+
## OpenAPI and schema exports
|
|
388
|
+
|
|
389
|
+
Exports use OpenAPI 3.0.3. The purpose of each `scope` array position is described in text; the server validates their order.
|
|
390
|
+
|
|
391
|
+
To serve the specification over HTTP, set `database.schema` and enable `openapi.enabled: true`, or start the server with `--openapi`. The JSON document is available at `/openapi.json` by default; change the path through `openapi.endpoint`. Open it in a separately installed Swagger UI or import it into an API client.
|
|
392
|
+
|
|
393
|
+
To generate schemas, specify the format and configuration file:
|
|
394
|
+
|
|
395
|
+
```sh
|
|
396
|
+
npx deep-json-server generate openapi server.config.js
|
|
397
|
+
npx deep-json-server generate graphql server.config.js
|
|
398
|
+
npx deep-json-server generate openapi,graphql server.config.js
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
Generation runs without starting the server or reading database records. The command reads `database.schema` and writes schemas to `openapi.path` and `graphql.path`. The `enabled` flags control HTTP endpoints and are not required for exports. A configuration for generation only can contain:
|
|
402
|
+
|
|
403
|
+
```js
|
|
404
|
+
export default {
|
|
405
|
+
database: { schema: './schema.json' },
|
|
406
|
+
openapi: { path: './generated/openapi.yaml' },
|
|
407
|
+
graphql: { path: './generated/schema.graphql' },
|
|
408
|
+
};
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Each format needs its own output file. The command rejects destinations that would overwrite the configuration, database, schema, auth users or file metadata.
|
|
412
|
+
|
|
413
|
+
## Authentication
|
|
414
|
+
|
|
415
|
+
The `auth` section enables registration, login and permission checks for record changes in REST and GraphQL. Reads and all file operations remain public.
|
|
416
|
+
|
|
417
|
+
Define the first administrator in the initial data. For example, create `auth.json` with `setup-auth.mjs`:
|
|
418
|
+
|
|
419
|
+
```js
|
|
420
|
+
import { writeFile } from 'node:fs/promises';
|
|
421
|
+
import { hashPassword } from '@kollors/deep-json-server/auth';
|
|
422
|
+
|
|
423
|
+
const password = process.env.DJS_PASSWORD;
|
|
424
|
+
if (!password) throw new Error('Set DJS_PASSWORD');
|
|
425
|
+
await writeFile('./auth.json', JSON.stringify([
|
|
426
|
+
{ id: '1', username: 'admin', passwordHash: await hashPassword(password), isAdmin: true },
|
|
427
|
+
], null, 2), { flag: 'wx', mode: 0o600 });
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
Set `DJS_PASSWORD` and run `node setup-auth.mjs`. Add the file to your server configuration:
|
|
431
|
+
|
|
432
|
+
```js
|
|
433
|
+
export default {
|
|
434
|
+
database: { path: './database.json' },
|
|
435
|
+
auth: { users: './auth.json', expiresIn: 3600 },
|
|
436
|
+
};
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
Start with `npx deep-json-server server.config.js`. Each initial user needs a unique string `id`, a unique `username` and a `passwordHash` created by the helper. `isAdmin` defaults to `false`. Passwords use salted scrypt hashes.
|
|
440
|
+
|
|
441
|
+
You can pass an array in `auth.users` instead of a path:
|
|
442
|
+
|
|
443
|
+
```js
|
|
444
|
+
import { hashPassword } from '@kollors/deep-json-server/auth';
|
|
445
|
+
|
|
446
|
+
const password = process.env.DJS_PASSWORD;
|
|
447
|
+
if (!password) throw new Error('Set DJS_PASSWORD');
|
|
448
|
+
|
|
449
|
+
export default {
|
|
450
|
+
database: { data: { items: [] } },
|
|
451
|
+
auth: {
|
|
452
|
+
users: [
|
|
453
|
+
{ id: '1', username: 'admin', passwordHash: await hashPassword(password), isAdmin: true },
|
|
454
|
+
],
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
Auth users are stored separately from database collections. With a string in `auth.users`, registration, password changes and admin status changes are saved to that JSON file through the same `lowdb` used by the main database. With an array, changes remain in an internal memory copy and disappear on restart; the original array is unchanged. This choice is independent of `database.path` or `database.data`. A file write failure leaves the user and active sessions unchanged. The file is read at startup; restart the server after editing it manually.
|
|
460
|
+
|
|
461
|
+
| REST request | JSON body | Response |
|
|
462
|
+
|---|---|---|
|
|
463
|
+
| `POST /auth/register` | `{ "username": "anna", "password": "…" }` | `201`: `{ id, username, isAdmin: false }` |
|
|
464
|
+
| `POST /auth/login` | `{ "username": "anna", "password": "…" }` | `{ accessToken, expiresIn, user: { id, username, isAdmin } }` |
|
|
465
|
+
| `GET /auth/me` | — | `{ id, username, isAdmin }` |
|
|
466
|
+
| `POST /auth/logout` | — | `{ success: true }` |
|
|
467
|
+
| `PATCH /auth/users/:id/password` | `{ "currentPassword": "…", "newPassword": "…" }` | `{ success: true }` |
|
|
468
|
+
| `PATCH /auth/users/:id/admin` | `{ "isAdmin": true }` | `{ id, username, isAdmin }` |
|
|
469
|
+
|
|
470
|
+
Registration and login are public. Send `Authorization: Bearer <accessToken>` for the other methods. Registration creates an ordinary user with a generated `id`; requests cannot include `id`, `passwordHash` or `isAdmin`. Usernames are case-sensitive and unique; duplicates return `409`. A username must contain a non-whitespace character and be at most 256 characters long. Passwords must contain 1–1024 characters. Values are not trimmed. Registration does not create a session: log in afterwards.
|
|
471
|
+
|
|
472
|
+
Users can change only their own password by supplying `currentPassword` and `newPassword`. Administrators follow the same rule for their own password. An administrator can change an ordinary user's password with just `newPassword`. Changing another administrator's password returns `403`. A successful password change ends all sessions of the target user, including the current session when changing your own password; log in again. An incorrect current password returns `401` without changing sessions.
|
|
473
|
+
|
|
474
|
+
Only administrators can change `isAdmin`. They can grant or remove another user's admin status. An administrator can remove their own status only if another administrator remains; otherwise the request returns `409`. This check accounts for concurrent requests. Existing tokens use the new permissions as soon as the change is saved, including for GraphQL mutations. An administrator may demote another administrator and then change their password as an ordinary user.
|
|
475
|
+
|
|
476
|
+
Invalid or expired tokens return `401`, insufficient permissions return `403`, and an absent user for an otherwise permitted operation returns `404`. Invalid request bodies return `400`. Login, registration and password changes may return `429` when too many password computations are running; login also limits active sessions. Sessions are kept in memory and disappear on restart. Logout revokes only the supplied token.
|
|
477
|
+
|
|
478
|
+
OpenAPI describes all auth routes and their Bearer token requirements. In Swagger UI, paste a token from login into **Authorize**. For schema exports, enable auth in the configuration and run `generate openapi server.config.js`; the users file is not read during generation. Auth methods are exposed through REST. GraphQL checks the same token when changing records. GraphQL and OpenAPI require `database.schema`.
|
|
479
|
+
|
|
480
|
+
## Record dates, deletion and ownership
|
|
481
|
+
|
|
482
|
+
Configure defaults in `database`:
|
|
483
|
+
|
|
484
|
+
```js
|
|
485
|
+
export default {
|
|
486
|
+
database: {
|
|
487
|
+
path: './database.json',
|
|
488
|
+
schema: './schema.json',
|
|
489
|
+
timestamps: true,
|
|
490
|
+
softDelete: true,
|
|
491
|
+
},
|
|
492
|
+
auth: { users: './auth.json' },
|
|
493
|
+
};
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
Each model can override `timestamps` and `softDelete` beside `collection` and `fields`. An omitted setting inherits the global value; `true` or `false` overrides it. CLI flags override global configuration, and model settings take priority over both. Without a schema, global settings apply to every collection. This model disables timestamps and keeps deleted records regardless of the global settings:
|
|
497
|
+
|
|
498
|
+
```json
|
|
499
|
+
{
|
|
500
|
+
"Note": {
|
|
501
|
+
"collection": "notes",
|
|
502
|
+
"timestamps": false,
|
|
503
|
+
"softDelete": true,
|
|
504
|
+
"fields": {
|
|
505
|
+
"id": { "type": "string", "primary": true, "generated": "uuid" },
|
|
506
|
+
"text": { "type": "string", "required": true }
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
| Field | Enabled by | Meaning |
|
|
513
|
+
|---|---|---|
|
|
514
|
+
| `createdAt`, `updatedAt` | `timestamps` | Creation and last update time |
|
|
515
|
+
| `deletedAt` | `softDelete` | Deletion time, or `null` for an active record |
|
|
516
|
+
| `createdById`, `updatedById` | `auth` | Creator/owner and last editor |
|
|
517
|
+
| `deletedById` | `auth` + `softDelete` | User who deleted the record, or `null` |
|
|
518
|
+
|
|
519
|
+
Dates are UTC ISO 8601 strings. New records receive the same creation and update time and, with auth, the authenticated user's ID as creator and editor. PUT/PATCH preserve the creator and creation time. DELETE updates the deletion fields and the enabled last-update fields. Existing records with unknown dates or authors expose `null`. These fields are read-only in REST and GraphQL; embedded plain objects do not receive their own audit fields. Previously stored audit values are retained when their features are disabled.
|
|
520
|
+
|
|
521
|
+
With soft deletion, DELETE retains the record in the database. Repeating DELETE on an already deleted record leaves its deletion details unchanged. Fetching by primary key returns deleted records too. A successful PUT/PATCH restores the record by clearing `deletedAt` and `deletedById`. An empty PATCH restores it without replacing other fields; PUT requires all mandatory fields.
|
|
522
|
+
|
|
523
|
+
Lists return active records by default. To select deleted records, specify `deletedAt` in `where`:
|
|
524
|
+
|
|
525
|
+
```json
|
|
526
|
+
[
|
|
527
|
+
{ "id": true, "deletedAt": true },
|
|
528
|
+
{ "where": { "deletedAt": { "ne": null } } }
|
|
529
|
+
]
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
Use `eq: null` for active records, `ne: null` for deleted records, or combine both with `or` for all records. The same filters work in GraphQL. An explicit `deletedAt` condition, including one inside `and`, `or` or `not`, replaces the default at that level. Relation filters and relation lists apply this rule independently. Singular relations hide deleted targets; primary-key queries can still retrieve them directly.
|
|
533
|
+
|
|
534
|
+
Cascade deletion follows `onDelete` and each affected model's `softDelete`. Restoring the record that initiated a cascade also restores records deleted by that operation, excluding those deleted earlier. Physically deleted records cannot be recovered. If a cascade removed an embedded object, restoration requires the affected object field to remain unchanged since deletion. A conflict or missing required relation rejects the whole operation.
|
|
535
|
+
|
|
536
|
+
Restoration metadata is stored with the records and survives restarts; include it when backing up the database. The internal `djsDeletion` field is reserved and is not exposed by either API.
|
|
537
|
+
|
|
538
|
+
With auth, any authenticated user can create records. Updating, deleting and restoring require ownership through `createdById` or `isAdmin: true`. Only administrators can change unowned records. Administrator edits preserve the original owner. The rules cover nested writes, changes to relation storage keys, cascades and restoration. Linking an existing record without changing it does not require owning it. Each mutation is atomic: denied changes leave all affected records unchanged.
|
|
539
|
+
|
|
540
|
+
REST returns 401 for an invalid or missing token and 403 for insufficient permissions. GraphQL applies the same rules to mutations and returns `UNAUTHENTICATED` or `FORBIDDEN`; obtain the token through REST login and send `Authorization: Bearer <token>`. GraphQL reads remain public, as do OPTIONS requests and every file operation.
|
|
541
|
+
|
|
542
|
+
## Files
|
|
543
|
+
|
|
544
|
+
Files are available through REST and documented in OpenAPI. Add storage to the configuration:
|
|
545
|
+
|
|
546
|
+
```js
|
|
547
|
+
export default {
|
|
548
|
+
database: { path: './database.json' },
|
|
549
|
+
files: { directory: './uploads', metadata: './files.json' },
|
|
550
|
+
};
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
Start the server:
|
|
554
|
+
|
|
555
|
+
```bash
|
|
556
|
+
npx deep-json-server server.config.js
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
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.
|
|
560
|
+
|
|
561
|
+
Upload one file directly as the request body. `Content-Name` contains the URI-encoded file name, `Content-Type` contains its MIME type, and the optional `Content-Directory` contains the URI-encoded relative directory:
|
|
562
|
+
|
|
563
|
+
```http
|
|
564
|
+
POST /_files/storage
|
|
565
|
+
Content-Name: shadows-of-ardenia.jpg
|
|
566
|
+
Content-Directory: posters
|
|
567
|
+
Content-Type: image/jpeg
|
|
568
|
+
|
|
569
|
+
<binary body>
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
A new file returns status `201` and its computed metadata:
|
|
573
|
+
|
|
574
|
+
```json
|
|
575
|
+
{
|
|
576
|
+
"directory": "posters",
|
|
577
|
+
"downloadUrl": "/_files/download/posters/shadows-of-ardenia.jpg",
|
|
578
|
+
"metadataUrl": "/_files/metadata/posters/shadows-of-ardenia.jpg",
|
|
579
|
+
"mimeType": "image/jpeg",
|
|
580
|
+
"name": "shadows-of-ardenia.jpg",
|
|
581
|
+
"size": 182340,
|
|
582
|
+
"url": "/_files/storage/posters/shadows-of-ardenia.jpg"
|
|
583
|
+
}
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
The combination of `directory` and `name` identifies a file. Uploading to an existing path returns `409`. Pass `Content-Override: true` to replace it; a successful replacement returns `200`. The server supports these file routes:
|
|
587
|
+
|
|
588
|
+
```text
|
|
589
|
+
POST /_files/storage Upload or replace a file
|
|
590
|
+
GET /_files/storage/* Return file contents inline
|
|
591
|
+
PATCH /_files/storage/* Rename or move a file
|
|
592
|
+
DELETE /_files/storage/* Delete a file
|
|
593
|
+
|
|
594
|
+
GET /_files/metadata/* Return file metadata as JSON
|
|
595
|
+
GET /_files/download/* Download a file as an attachment
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
Rename, move, or perform both operations with a JSON body. At least one field is required:
|
|
599
|
+
|
|
600
|
+
```http
|
|
601
|
+
PATCH /_files/storage/posters/shadows-of-ardenia.jpg
|
|
602
|
+
Content-Type: application/json
|
|
603
|
+
|
|
604
|
+
{
|
|
605
|
+
"directory": "archive/posters",
|
|
606
|
+
"name": "ardenia-shadows.jpg"
|
|
607
|
+
}
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
`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.
|
|
611
|
+
|
|
612
|
+
In disk mode, the binary is stored at `<files.directory>/<directory>/<name>`. Metadata stores `directory`, `mimeType` and `name`; the server reads the size from the file and builds its URLs. Directories and the metadata file are created when needed.
|
|
613
|
+
|
|
614
|
+
Use one server process per disk database and file store. Stop it before editing stored files or metadata manually. Storage paths cannot contain symbolic links. Uploads and renames cannot overwrite the database, counters, schema, auth users, loaded configuration or metadata file.
|
|
615
|
+
|
|
616
|
+
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.
|
|
617
|
+
|
|
618
|
+
## Programmatic API
|
|
619
|
+
|
|
620
|
+
```js
|
|
621
|
+
import { createServer } from '@kollors/deep-json-server/server';
|
|
622
|
+
import config from './server.config.js';
|
|
623
|
+
|
|
624
|
+
const facade = await createServer(config);
|
|
625
|
+
const server = facade.fastify();
|
|
626
|
+
await server.listen();
|
|
627
|
+
// await server.close();
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
The `openapi()` and `graphql()` methods return schemas and require `database.schema`. `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.
|
|
631
|
+
|
|
632
|
+
The second argument overrides module settings, for example `createServer(config, { files: false, graphql: true })`. Supported flags are `files`, `graphql`, `openapi` and `auth`. Enabling auth or files requires the corresponding configuration section; GraphQL and OpenAPI require a model schema.
|
|
633
|
+
|
|
634
|
+
The root import `@kollors/deep-json-server` also provides these functions. Server adapters load when enabled. Generators can be used independently:
|
|
635
|
+
|
|
636
|
+
```js
|
|
637
|
+
import { generateOpenapi, writeOpenapi } from '@kollors/deep-json-server/openapi';
|
|
638
|
+
import { generateGraphql, writeGraphql } from '@kollors/deep-json-server/graphql';
|
|
639
|
+
|
|
640
|
+
const document = await generateOpenapi('./schema.json', { files: true });
|
|
641
|
+
const sdl = await generateGraphql('./schema.json');
|
|
642
|
+
await writeOpenapi(document, './generated/openapi.yaml');
|
|
643
|
+
await writeGraphql(sdl, './generated/schema.graphql');
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
Both generators accept `timestamps`, `softDelete` and `auth` options for the record fields. With `{ auth: true }`, OpenAPI also includes REST auth routes and mutation security requirements. `hashPassword()` is also available from the root package.
|
|
647
|
+
|
|
648
|
+
`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. Pagination sizes must be positive integers; `pageSize` cannot exceed `maxPageSize`.
|
|
373
649
|
|
|
374
650
|
## Example database
|
|
375
651
|
|
|
@@ -475,146 +751,13 @@ String primary keys use GraphQL `ID`; ordinary strings use `String`, numbers use
|
|
|
475
751
|
}
|
|
476
752
|
```
|
|
477
753
|
|
|
478
|
-
##
|
|
479
|
-
|
|
480
|
-
Add `files.directory` and `files.metadata` to the configuration and start the server:
|
|
481
|
-
|
|
482
|
-
```bash
|
|
483
|
-
deep-json-server server.config.js
|
|
484
|
-
```
|
|
485
|
-
|
|
486
|
-
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.
|
|
487
|
-
|
|
488
|
-
Upload one file directly as the request body. `Content-Name` contains the URI-encoded file name, `Content-Type` contains its MIME type, and the optional `Content-Directory` contains the URI-encoded relative directory:
|
|
489
|
-
|
|
490
|
-
```http
|
|
491
|
-
POST /_files/storage
|
|
492
|
-
Content-Name: shadows-of-ardenia.jpg
|
|
493
|
-
Content-Directory: posters
|
|
494
|
-
Content-Type: image/jpeg
|
|
495
|
-
|
|
496
|
-
<binary body>
|
|
497
|
-
```
|
|
498
|
-
|
|
499
|
-
A new file returns status `201` and its computed metadata:
|
|
500
|
-
|
|
501
|
-
```json
|
|
502
|
-
{
|
|
503
|
-
"directory": "posters",
|
|
504
|
-
"downloadUrl": "/_files/download/posters/shadows-of-ardenia.jpg",
|
|
505
|
-
"metadataUrl": "/_files/metadata/posters/shadows-of-ardenia.jpg",
|
|
506
|
-
"mimeType": "image/jpeg",
|
|
507
|
-
"name": "shadows-of-ardenia.jpg",
|
|
508
|
-
"size": 182340,
|
|
509
|
-
"url": "/_files/storage/posters/shadows-of-ardenia.jpg"
|
|
510
|
-
}
|
|
511
|
-
```
|
|
512
|
-
|
|
513
|
-
The combination of `directory` and `name` identifies a file. Uploading to an existing path returns `409`. Pass `Content-Override: true` to replace it; a successful replacement returns `200`. The server supports these file routes:
|
|
514
|
-
|
|
515
|
-
```text
|
|
516
|
-
POST /_files/storage Upload or replace a file
|
|
517
|
-
GET /_files/storage/* Return file contents inline
|
|
518
|
-
PATCH /_files/storage/* Rename or move a file
|
|
519
|
-
DELETE /_files/storage/* Delete a file
|
|
520
|
-
|
|
521
|
-
GET /_files/metadata/* Return file metadata as JSON
|
|
522
|
-
GET /_files/download/* Download a file as an attachment
|
|
523
|
-
```
|
|
524
|
-
|
|
525
|
-
Rename, move, or perform both operations with a JSON body. At least one field is required:
|
|
526
|
-
|
|
527
|
-
```http
|
|
528
|
-
PATCH /_files/storage/posters/shadows-of-ardenia.jpg
|
|
529
|
-
Content-Type: application/json
|
|
530
|
-
|
|
531
|
-
{
|
|
532
|
-
"directory": "archive/posters",
|
|
533
|
-
"name": "ardenia-shadows.jpg"
|
|
534
|
-
}
|
|
535
|
-
```
|
|
536
|
-
|
|
537
|
-
`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.
|
|
538
|
-
|
|
539
|
-
In disk mode, the binary is stored at `<files.directory>/<directory>/<name>`. Metadata stores `directory`, `mimeType` and `name`; the server reads the size from the file and builds its URLs. Directories and the metadata file are created when needed.
|
|
540
|
-
|
|
541
|
-
Use one server process per disk database and file store. Stop it before editing stored files or metadata manually. Storage paths cannot contain symbolic links. Uploads and renames cannot overwrite the database, counters, schema, auth users, loaded configuration or metadata file.
|
|
542
|
-
|
|
543
|
-
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.
|
|
544
|
-
|
|
545
|
-
## Authentication
|
|
546
|
-
|
|
547
|
-
The optional auth module provides REST routes for login, current user and logout. **It does not restrict access to REST records, files or GraphQL.**
|
|
548
|
-
|
|
549
|
-
Create an auth user in a separate file with `setup-auth.mjs`:
|
|
550
|
-
|
|
551
|
-
```js
|
|
552
|
-
import { writeFile } from 'node:fs/promises';
|
|
553
|
-
import { hashPassword } from '@kollors/deep-json-server/auth';
|
|
554
|
-
|
|
555
|
-
const password = process.env.DJS_PASSWORD;
|
|
556
|
-
if (!password) throw new Error('Set DJS_PASSWORD');
|
|
557
|
-
await writeFile('./auth.json', JSON.stringify([
|
|
558
|
-
{ id: '1', username: 'admin', passwordHash: await hashPassword(password) },
|
|
559
|
-
], null, 2), { flag: 'wx', mode: 0o600 });
|
|
560
|
-
```
|
|
561
|
-
|
|
562
|
-
Set `DJS_PASSWORD` and run `node setup-auth.mjs`. Add the file to your server configuration:
|
|
563
|
-
|
|
564
|
-
```js
|
|
565
|
-
export default {
|
|
566
|
-
database: { path: './database.json' },
|
|
567
|
-
auth: { users: './auth.json', expiresIn: 3600 },
|
|
568
|
-
};
|
|
569
|
-
```
|
|
570
|
-
|
|
571
|
-
Start with `npx deep-json-server --auth server.config.js`, or set `auth.enabled: true`. Each user needs a unique string `id`, a unique `username` and a `passwordHash` created by the helper. Passwords use salted scrypt hashes. The file is read at startup; restart the server after editing it.
|
|
572
|
-
|
|
573
|
-
| REST request | Input | Response |
|
|
574
|
-
|---|---|---|
|
|
575
|
-
| `POST /auth/login` | JSON `{ "username": "admin", "password": "…" }` | `{ accessToken, expiresIn, user: { id, username } }` |
|
|
576
|
-
| `GET /auth/me` | Bearer token | `{ id, username }` |
|
|
577
|
-
| `POST /auth/logout` | Bearer token | `{ success: true }` |
|
|
578
|
-
|
|
579
|
-
Pass the token in `Authorization: Bearer <accessToken>`. Incorrect credentials and invalid or expired tokens return HTTP 401. Sessions are kept in memory and disappear on restart; logout revokes the supplied token. Login can return HTTP 429 when the server has too many concurrent login attempts or active sessions.
|
|
580
|
-
|
|
581
|
-
OpenAPI includes these REST operations and a Bearer security scheme for `/auth/me` and `/auth/logout`. In Swagger UI, paste a token from login into **Authorize**. For schema exports, enable auth in the configuration or use `generate openapi --auth server.config.js`; the users file is not read during generation. GraphQL and OpenAPI still require `database.schema`.
|
|
582
|
-
|
|
583
|
-
## Programmatic API
|
|
584
|
-
|
|
585
|
-
```js
|
|
586
|
-
import { createServer } from '@kollors/deep-json-server/server';
|
|
587
|
-
import config from './server.config.js';
|
|
588
|
-
|
|
589
|
-
const facade = await createServer(config);
|
|
590
|
-
const server = facade.fastify();
|
|
591
|
-
await server.listen();
|
|
592
|
-
// await server.close();
|
|
593
|
-
```
|
|
594
|
-
|
|
595
|
-
The `openapi()` and `graphql()` methods return schemas and require `database.schema`. `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, auth: true })`.
|
|
596
|
-
|
|
597
|
-
The root import `@kollors/deep-json-server` also provides these functions. Server adapters load when enabled. Generators can be used independently:
|
|
598
|
-
|
|
599
|
-
```js
|
|
600
|
-
import { generateOpenapi, writeOpenapi } from '@kollors/deep-json-server/openapi';
|
|
601
|
-
import { generateGraphql, writeGraphql } from '@kollors/deep-json-server/graphql';
|
|
602
|
-
|
|
603
|
-
const document = await generateOpenapi('./schema.json', { files: true });
|
|
604
|
-
const sdl = await generateGraphql('./schema.json');
|
|
605
|
-
await writeOpenapi(document, './generated/openapi.yaml');
|
|
606
|
-
await writeGraphql(sdl, './generated/schema.graphql');
|
|
607
|
-
```
|
|
608
|
-
|
|
609
|
-
`generateOpenapi()` accepts `{ auth: true }` to include auth operations. `hashPassword()` is also available from the root package.
|
|
610
|
-
|
|
611
|
-
`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. Pagination sizes must be positive integers; `pageSize` cannot exceed `maxPageSize`.
|
|
612
|
-
|
|
613
|
-
## Storage and development
|
|
754
|
+
## Data storage
|
|
614
755
|
|
|
615
756
|
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.
|
|
616
757
|
|
|
617
|
-
|
|
758
|
+
## Development
|
|
759
|
+
|
|
760
|
+
Source modules: `rest`, `graphql`, `openapi`, `auth`, `files`, `cli` and `server`. Shared models, storage, queries and mutation rules live in `core`. `server` connects the modules; API generators load independently of the HTTP runtime.
|
|
618
761
|
|
|
619
762
|
```sh
|
|
620
763
|
npm ci
|
|
@@ -623,6 +766,6 @@ npm run verify
|
|
|
623
766
|
|
|
624
767
|
The command checks types, code style, test coverage and installation from the package archive.
|
|
625
768
|
|
|
626
|
-
To publish a new alpha, update the version in `package.json`, `package-lock.json` and `src/constants.ts`, then 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`.
|
|
769
|
+
To publish a new alpha, update the version in `package.json`, `package-lock.json` and `src/core/constants.ts`, then 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`.
|
|
627
770
|
|
|
628
771
|
License: MIT.
|