@objy/server 0.0.0-stage → 0.1.0
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/API.md +1469 -0
- package/LICENSE +21 -0
- package/NOTICE.md +9 -0
- package/README.md +21 -2
- package/objydb.config.json +48 -0
- package/package.json +42 -4
- package/public/index.html +4134 -0
- package/public/objy-db-logo.png +0 -0
- package/src/aggregate.js +438 -0
- package/src/api.js +15 -0
- package/src/auth.js +472 -0
- package/src/clock.js +18 -0
- package/src/cluster-secret.js +25 -0
- package/src/cluster-status.js +562 -0
- package/src/compression-stats.js +32 -0
- package/src/config.js +360 -0
- package/src/crdt.js +1076 -0
- package/src/derived-state-scheduler.js +128 -0
- package/src/files.js +324 -0
- package/src/hnsw.js +269 -0
- package/src/http-cors.js +31 -0
- package/src/http-router.js +162 -0
- package/src/index.js +137 -0
- package/src/interfaces/auth.js +123 -0
- package/src/interfaces/cluster.js +296 -0
- package/src/interfaces/public-object.js +2452 -0
- package/src/interfaces/ui.js +38 -0
- package/src/lib/crypto.cjs +54 -0
- package/src/lib/jwt.cjs +231 -0
- package/src/local-wal.js +122 -0
- package/src/memory-guard.js +152 -0
- package/src/ndjson.js +117 -0
- package/src/node-runtime.js +29 -0
- package/src/object-hooks.js +282 -0
- package/src/object-id.js +7 -0
- package/src/object-storage.js +263 -0
- package/src/object-store.js +288 -0
- package/src/patch.js +87 -0
- package/src/permissions.js +178 -0
- package/src/persistence.js +1918 -0
- package/src/plugin-loader.js +21 -0
- package/src/query-planner.js +266 -0
- package/src/query.js +416 -0
- package/src/references.js +349 -0
- package/src/replication-scheduler.js +303 -0
- package/src/restore-status.js +77 -0
- package/src/runtime.js +47 -0
- package/src/server.js +242 -0
- package/src/shape.js +286 -0
- package/src/shard-map-store.js +85 -0
- package/src/shard-map.js +333 -0
- package/src/shard-router.js +737 -0
- package/src/store.js +3410 -0
- package/src/system-secrets.js +107 -0
- package/src/vector.js +83 -0
package/API.md
ADDED
|
@@ -0,0 +1,1469 @@
|
|
|
1
|
+
# OBJY DB HTTP API
|
|
2
|
+
|
|
3
|
+
<p align="center"><strong>Plain objects. Bulk-first operations. One public API.</strong></p>
|
|
4
|
+
|
|
5
|
+
This is the public HTTP contract for OBJY DB. The standalone [JavaScript client](https://github.com/objy-org/objy-client) exposes the same operations for browsers and Node.
|
|
6
|
+
|
|
7
|
+
[Start here](#start-here) | [JavaScript client](https://github.com/objy-org/objy-client) | [Database README](README.md)
|
|
8
|
+
|
|
9
|
+
## Start Here
|
|
10
|
+
|
|
11
|
+
Examples use `{URL}` as the server base URL. Public requests may reach any cluster node; OBJY DB routes them to the owning object shard.
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
database
|
|
15
|
+
collection
|
|
16
|
+
object _id=...
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
| Placeholder | Meaning |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| `{URL}` | Public node or gateway URL, without a trailing slash. |
|
|
22
|
+
| `{db}` | Database namespace. |
|
|
23
|
+
| `{collection}` | Collection inside the database. |
|
|
24
|
+
| `{_id}` | Caller-provided or generated object id. |
|
|
25
|
+
|
|
26
|
+
Most routes require a bearer token:
|
|
27
|
+
|
|
28
|
+
```http
|
|
29
|
+
Authorization: Bearer <accessToken>
|
|
30
|
+
Content-Type: application/json
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Objects are dynamic JSON-style records. Collections require no schema, and objects in one collection may have different shapes and value types. Every object has an `_id`; inserts generate a compact URL-safe id when it is omitted.
|
|
34
|
+
|
|
35
|
+
`PUT` replaces a complete object, `PATCH` applies Mongo-like `$set` and `$unset`, and `DELETE` creates a replicated delete. Query, search, and aggregation responses are plain arrays rather than envelopes.
|
|
36
|
+
|
|
37
|
+
## Endpoint Map
|
|
38
|
+
|
|
39
|
+
| Area | Routes |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| [Authentication](#authentication-and-permissions) | Login and renew against any auth-enabled collection. |
|
|
42
|
+
| [Databases](#databases) | List, create, bootstrap an administrator, and delete. |
|
|
43
|
+
| [Collections](#collections) | List, create, delete, and manage collection metadata. |
|
|
44
|
+
| [Objects](#objects) | Single and bulk create, read, replace, patch, and delete. |
|
|
45
|
+
| [Imports](#streamed-bulk-writes) | Bounded NDJSON insert, replace, patch, and delete. |
|
|
46
|
+
| [Search](#query) | Filters, sorting, paging, aggregation, vectors, and references. |
|
|
47
|
+
| [Files](#file-handling) | Object-backed multipart upload, replacement, download, and deletion. |
|
|
48
|
+
| [Operations](#health-and-cluster-status) | Health and authenticated public cluster status. |
|
|
49
|
+
|
|
50
|
+
## Authentication and Permissions
|
|
51
|
+
|
|
52
|
+
Authentication is collection-based: any collection may contain user objects. On first boot, OBJY DB creates `admin` in `_global/users` and prints its generated password once. `OBJYDB_ADMIN_PASSWORD` may supply that initial password for unattended startup.
|
|
53
|
+
|
|
54
|
+
Passwords are hashed on write and redacted from responses. Tokens are scoped to the database and collection used for authentication.
|
|
55
|
+
|
|
56
|
+
### Login
|
|
57
|
+
|
|
58
|
+
`POST /api/{db}/{collection}/auth`
|
|
59
|
+
|
|
60
|
+
**Request**
|
|
61
|
+
|
|
62
|
+
```http
|
|
63
|
+
POST {URL}/api/_global/users/auth
|
|
64
|
+
Content-Type: application/json
|
|
65
|
+
|
|
66
|
+
{
|
|
67
|
+
"username": "admin",
|
|
68
|
+
"password": "<initial-password>"
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Response**
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"user": { "_id": "admin", "username": "admin", "role": "admin" },
|
|
77
|
+
"accessToken": "...",
|
|
78
|
+
"refreshToken": "...",
|
|
79
|
+
"expiresIn": 3600,
|
|
80
|
+
"expires_in": 3600
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Renew Token
|
|
85
|
+
|
|
86
|
+
`POST /api/{db}/{collection}/renew`
|
|
87
|
+
|
|
88
|
+
**Request**
|
|
89
|
+
|
|
90
|
+
```http
|
|
91
|
+
POST {URL}/api/_global/users/renew
|
|
92
|
+
Content-Type: application/json
|
|
93
|
+
|
|
94
|
+
{
|
|
95
|
+
"refreshToken": "..."
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Response**
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"user": { "_id": "admin", "username": "admin", "role": "admin" },
|
|
104
|
+
"accessToken": "...",
|
|
105
|
+
"refreshToken": "...",
|
|
106
|
+
"expiresIn": 3600,
|
|
107
|
+
"expires_in": 3600
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Refresh tokens must be renewed against the same database and collection that issued them.
|
|
112
|
+
|
|
113
|
+
<details>
|
|
114
|
+
<summary><strong>Safe renewal and replay</strong></summary>
|
|
115
|
+
|
|
116
|
+
Access-token expiration rejected before an operation begins returns HTTP 401 with `code: "ACCESS_TOKEN_EXPIRED"` and `operationStarted: false`. A client may renew and replay that request once.
|
|
117
|
+
|
|
118
|
+
These markers do not authorize replay when `operationStarted` is true or `writeMayHaveSucceeded` is true. Other authentication failures and permission errors are not expiration signals.
|
|
119
|
+
|
|
120
|
+
</details>
|
|
121
|
+
|
|
122
|
+
### Permission Model
|
|
123
|
+
|
|
124
|
+
Database users can access only the database where they authenticated. A global user can address other databases, but database and collection administration requires `admin: true` or `role: "admin"`. Creating or deleting databases and reading `/api/config` specifically require a global admin token.
|
|
125
|
+
|
|
126
|
+
The built-in `_global` database is read-only through the public API after bootstrap. Object writes, metadata changes, collection changes, and database deletion are rejected even for a global administrator.
|
|
127
|
+
|
|
128
|
+
Collection grants provide broad read or write access by collection name:
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
{
|
|
132
|
+
"_id": "editor",
|
|
133
|
+
"username": "editor",
|
|
134
|
+
"password": "secret",
|
|
135
|
+
"collectionPermissions": {
|
|
136
|
+
"read": ["articles", "media"],
|
|
137
|
+
"write": ["articles"]
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`read` allows object reads and queries. `write` allows create, update, and delete, but does not imply read access. Create users through `POST /api/{db}/users/object`; permission changes apply after login or renewal issues a new token.
|
|
143
|
+
|
|
144
|
+
Object-level rules combine a query with `c`, `r`, `u`, and `d` permissions:
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"_id": "editor",
|
|
149
|
+
"username": "editor",
|
|
150
|
+
"password": "secret",
|
|
151
|
+
"permissions": [
|
|
152
|
+
{
|
|
153
|
+
"name": "own articles",
|
|
154
|
+
"query": { "ownerId": "editor" },
|
|
155
|
+
"perm": "crud"
|
|
156
|
+
}
|
|
157
|
+
]
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Read rules are merged into the requested query. Updates must permit both the stored object and its resulting value. A non-admin user without a matching rule or collection grant is denied by default.
|
|
162
|
+
|
|
163
|
+
## Databases
|
|
164
|
+
|
|
165
|
+
### List Databases
|
|
166
|
+
|
|
167
|
+
`GET /api/databases`
|
|
168
|
+
|
|
169
|
+
**Request**
|
|
170
|
+
|
|
171
|
+
```http
|
|
172
|
+
GET {URL}/api/databases
|
|
173
|
+
Authorization: Bearer <accessToken>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Response**
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
{
|
|
180
|
+
"databases": ["_global", "{db}"]
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Create Database
|
|
185
|
+
|
|
186
|
+
`POST /api/databases`
|
|
187
|
+
|
|
188
|
+
**Request**
|
|
189
|
+
|
|
190
|
+
```http
|
|
191
|
+
POST {URL}/api/databases
|
|
192
|
+
Authorization: Bearer <accessToken>
|
|
193
|
+
Content-Type: application/json
|
|
194
|
+
|
|
195
|
+
{
|
|
196
|
+
"name": "{db}",
|
|
197
|
+
"meta": { "owner": "team-a" }
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Response**
|
|
202
|
+
|
|
203
|
+
```json
|
|
204
|
+
{
|
|
205
|
+
"name": "{db}",
|
|
206
|
+
"created": true,
|
|
207
|
+
"meta": { "owner": "team-a" }
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Create Database With First Admin User
|
|
212
|
+
|
|
213
|
+
`POST /api/databases`
|
|
214
|
+
|
|
215
|
+
**Request**
|
|
216
|
+
|
|
217
|
+
```http
|
|
218
|
+
POST {URL}/api/databases
|
|
219
|
+
Authorization: Bearer <accessToken>
|
|
220
|
+
Content-Type: application/json
|
|
221
|
+
|
|
222
|
+
{
|
|
223
|
+
"name": "{db}",
|
|
224
|
+
"adminUser": {
|
|
225
|
+
"collection": "users",
|
|
226
|
+
"username": "owner",
|
|
227
|
+
"password": "secret"
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
**Response**
|
|
233
|
+
|
|
234
|
+
```json
|
|
235
|
+
{
|
|
236
|
+
"name": "{db}",
|
|
237
|
+
"created": true,
|
|
238
|
+
"meta": {},
|
|
239
|
+
"adminUser": {
|
|
240
|
+
"collection": "users",
|
|
241
|
+
"user": {
|
|
242
|
+
"_id": "mHzQzLE6k9P9s6b2",
|
|
243
|
+
"username": "owner",
|
|
244
|
+
"admin": true
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
If `adminUser.collection` is omitted, `users` is used. The users collection receives a `username` index.
|
|
251
|
+
|
|
252
|
+
### Delete Database
|
|
253
|
+
|
|
254
|
+
`DELETE /api/databases/{db}`
|
|
255
|
+
|
|
256
|
+
This requires a global admin token. Deletion removes the database metadata and objects across the cluster and attempts to remove associated file blobs. `_global` cannot be deleted.
|
|
257
|
+
|
|
258
|
+
**Request**
|
|
259
|
+
|
|
260
|
+
```http
|
|
261
|
+
DELETE {URL}/api/databases/{db}
|
|
262
|
+
Authorization: Bearer <accessToken>
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**Response**
|
|
266
|
+
|
|
267
|
+
```http
|
|
268
|
+
HTTP/1.1 204 No Content
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Collections
|
|
272
|
+
|
|
273
|
+
A collection is a dynamic group of objects plus a small metadata document. Metadata is kept in memory, persisted separately from objects, and may contain custom application keys.
|
|
274
|
+
|
|
275
|
+
These built-in keys change runtime behavior:
|
|
276
|
+
|
|
277
|
+
| Key | Purpose |
|
|
278
|
+
| --- | --- |
|
|
279
|
+
| `indexes` | Field indexes for equality filters, candidate narrowing, grouped values, and sorting. |
|
|
280
|
+
| `columns` | RID-addressed typed values for compact storage and numeric aggregation. |
|
|
281
|
+
| `tiering` | Compressed-memory offloading for selected cold object fields. |
|
|
282
|
+
| `hooks` | Names and activation settings for server-registered object middleware. |
|
|
283
|
+
|
|
284
|
+
```json
|
|
285
|
+
{
|
|
286
|
+
"indexes": ["status", "tenantId"],
|
|
287
|
+
"columns": ["score"],
|
|
288
|
+
"tiering": {
|
|
289
|
+
"mode": "compressedMemory",
|
|
290
|
+
"keep": ["name", "status", "tenantId"],
|
|
291
|
+
"minBytes": 4096
|
|
292
|
+
},
|
|
293
|
+
"hooks": {
|
|
294
|
+
"enabled": true,
|
|
295
|
+
"use": ["timestamps", "audit"]
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Field indexes accelerate supported filter and sort paths. Columns use typed arrays for numbers and booleans, dictionary codes for strings, and an overflow map for unsupported values. They support `$sum`, `$avg`, `$min`, `$max`, `$count`, and grouped aggregation without changing hydrated API objects.
|
|
301
|
+
|
|
302
|
+
Tiering accepts either `keep` or `offload`, never both. For example, `"offload": ["history", "rawPayload"]` compresses only those paths. Indexed and column fields remain hot automatically. Queries over non-indexed compressed fields remain correct but require scan-time hydration.
|
|
303
|
+
|
|
304
|
+
Hooks store configuration only; executable code is registered on the server. `use` enables hooks for their registered operations, operation-specific arrays may target `create`, `read`, `update`, or `delete`, and `enabled: false` bypasses the chain.
|
|
305
|
+
|
|
306
|
+
Changing indexes, columns, or tiering rebuilds derived in-memory state through cooperative bounded tasks. Changing hooks only invalidates the cached hook plan.
|
|
307
|
+
|
|
308
|
+
### List Collections
|
|
309
|
+
|
|
310
|
+
`GET /api/{db}/collections`
|
|
311
|
+
|
|
312
|
+
**Request**
|
|
313
|
+
|
|
314
|
+
```http
|
|
315
|
+
GET {URL}/api/{db}/collections
|
|
316
|
+
Authorization: Bearer <accessToken>
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**Response**
|
|
320
|
+
|
|
321
|
+
```json
|
|
322
|
+
{
|
|
323
|
+
"database": "{db}",
|
|
324
|
+
"collections": ["{collection}", "users"]
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Create Collection
|
|
329
|
+
|
|
330
|
+
`POST /api/{db}/collections`
|
|
331
|
+
|
|
332
|
+
**Request**
|
|
333
|
+
|
|
334
|
+
```http
|
|
335
|
+
POST {URL}/api/{db}/collections
|
|
336
|
+
Authorization: Bearer <accessToken>
|
|
337
|
+
Content-Type: application/json
|
|
338
|
+
|
|
339
|
+
{
|
|
340
|
+
"name": "{collection}",
|
|
341
|
+
"indexes": ["status", "tenantId"],
|
|
342
|
+
"columns": ["score"],
|
|
343
|
+
"hooks": {
|
|
344
|
+
"enabled": true,
|
|
345
|
+
"use": ["timestamps", "audit"]
|
|
346
|
+
},
|
|
347
|
+
"tiering": {
|
|
348
|
+
"mode": "compressedMemory",
|
|
349
|
+
"keep": ["name", "status", "tenantId"],
|
|
350
|
+
"minBytes": 4096
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
**Response**
|
|
356
|
+
|
|
357
|
+
```json
|
|
358
|
+
{
|
|
359
|
+
"name": "{collection}",
|
|
360
|
+
"created": true,
|
|
361
|
+
"meta": {
|
|
362
|
+
"indexes": ["status", "tenantId"],
|
|
363
|
+
"columns": ["score"],
|
|
364
|
+
"hooks": {
|
|
365
|
+
"enabled": true,
|
|
366
|
+
"use": ["timestamps", "audit"]
|
|
367
|
+
},
|
|
368
|
+
"tiering": {
|
|
369
|
+
"mode": "compressedMemory",
|
|
370
|
+
"keep": ["name", "status", "tenantId"],
|
|
371
|
+
"minBytes": 4096
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
You may also send `{ "name": "{collection}", "meta": { ... } }`.
|
|
378
|
+
|
|
379
|
+
### Read Collection Meta
|
|
380
|
+
|
|
381
|
+
`GET /api/{db}/{collection}/meta`
|
|
382
|
+
|
|
383
|
+
**Request**
|
|
384
|
+
|
|
385
|
+
```http
|
|
386
|
+
GET {URL}/api/{db}/{collection}/meta
|
|
387
|
+
Authorization: Bearer <accessToken>
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
**Response**
|
|
391
|
+
|
|
392
|
+
```json
|
|
393
|
+
{
|
|
394
|
+
"indexes": ["status", "tenantId"],
|
|
395
|
+
"columns": ["score"],
|
|
396
|
+
"hooks": {
|
|
397
|
+
"enabled": true,
|
|
398
|
+
"use": ["timestamps", "audit"]
|
|
399
|
+
},
|
|
400
|
+
"tiering": {
|
|
401
|
+
"mode": "compressedMemory",
|
|
402
|
+
"keep": ["name", "status", "tenantId"],
|
|
403
|
+
"minBytes": 4096
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### Replace Collection Meta
|
|
409
|
+
|
|
410
|
+
`PUT /api/{db}/{collection}/meta`
|
|
411
|
+
|
|
412
|
+
**Request**
|
|
413
|
+
|
|
414
|
+
```http
|
|
415
|
+
PUT {URL}/api/{db}/{collection}/meta
|
|
416
|
+
Authorization: Bearer <accessToken>
|
|
417
|
+
Content-Type: application/json
|
|
418
|
+
|
|
419
|
+
{
|
|
420
|
+
"indexes": ["status", "tenantId"],
|
|
421
|
+
"columns": ["score"],
|
|
422
|
+
"hooks": {
|
|
423
|
+
"enabled": true,
|
|
424
|
+
"use": ["timestamps", "audit"]
|
|
425
|
+
},
|
|
426
|
+
"tiering": {
|
|
427
|
+
"mode": "compressedMemory",
|
|
428
|
+
"keep": ["name", "status", "tenantId"],
|
|
429
|
+
"minBytes": 4096
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
**Response**
|
|
435
|
+
|
|
436
|
+
```json
|
|
437
|
+
{
|
|
438
|
+
"indexes": ["status", "tenantId"],
|
|
439
|
+
"columns": ["score"],
|
|
440
|
+
"hooks": {
|
|
441
|
+
"enabled": true,
|
|
442
|
+
"use": ["timestamps", "audit"]
|
|
443
|
+
},
|
|
444
|
+
"tiering": {
|
|
445
|
+
"mode": "compressedMemory",
|
|
446
|
+
"keep": ["name", "status", "tenantId"],
|
|
447
|
+
"minBytes": 4096
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### Delete Collection
|
|
453
|
+
|
|
454
|
+
`DELETE /api/{db}/collections/{collection}`
|
|
455
|
+
|
|
456
|
+
**Request**
|
|
457
|
+
|
|
458
|
+
```http
|
|
459
|
+
DELETE {URL}/api/{db}/collections/{collection}
|
|
460
|
+
Authorization: Bearer <accessToken>
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
**Response**
|
|
464
|
+
|
|
465
|
+
```http
|
|
466
|
+
HTTP/1.1 204 No Content
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
## Objects
|
|
470
|
+
|
|
471
|
+
Objects are plain dynamic JSON-style records. Strings, numbers, booleans, arrays, nested objects, and `null` may coexist across different shapes in one collection.
|
|
472
|
+
|
|
473
|
+
Every object has an `_id`. If `_id` is omitted on insert, the server generates a compact 16-character URL-safe id. Single and bulk operations use the same base route. Special fields such as `_vec` are handled by storage features and hidden from ordinary read responses.
|
|
474
|
+
|
|
475
|
+
### Insert One
|
|
476
|
+
|
|
477
|
+
`POST /api/{db}/{collection}/object`
|
|
478
|
+
|
|
479
|
+
**Request**
|
|
480
|
+
|
|
481
|
+
```http
|
|
482
|
+
POST {URL}/api/{db}/{collection}/object
|
|
483
|
+
Authorization: Bearer <accessToken>
|
|
484
|
+
Content-Type: application/json
|
|
485
|
+
|
|
486
|
+
{
|
|
487
|
+
"_id": "object-1",
|
|
488
|
+
"status": "active",
|
|
489
|
+
"score": 42,
|
|
490
|
+
"tags": ["new", "paid"]
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
**Response**
|
|
495
|
+
|
|
496
|
+
```json
|
|
497
|
+
{
|
|
498
|
+
"_id": "object-1",
|
|
499
|
+
"status": "active",
|
|
500
|
+
"score": 42,
|
|
501
|
+
"tags": ["new", "paid"]
|
|
502
|
+
}
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### Get One
|
|
506
|
+
|
|
507
|
+
`GET /api/{db}/{collection}/object/{_id}`
|
|
508
|
+
|
|
509
|
+
**Request**
|
|
510
|
+
|
|
511
|
+
```http
|
|
512
|
+
GET {URL}/api/{db}/{collection}/object/object-1
|
|
513
|
+
Authorization: Bearer <accessToken>
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
**Response**
|
|
517
|
+
|
|
518
|
+
```json
|
|
519
|
+
{
|
|
520
|
+
"_id": "object-1",
|
|
521
|
+
"status": "active",
|
|
522
|
+
"score": 42
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### Replace One
|
|
527
|
+
|
|
528
|
+
`PUT /api/{db}/{collection}/object/{_id}`
|
|
529
|
+
|
|
530
|
+
`PUT` is a full object replacement. The URL object id is authoritative.
|
|
531
|
+
|
|
532
|
+
**Request**
|
|
533
|
+
|
|
534
|
+
```http
|
|
535
|
+
PUT {URL}/api/{db}/{collection}/object/object-1
|
|
536
|
+
Authorization: Bearer <accessToken>
|
|
537
|
+
Content-Type: application/json
|
|
538
|
+
|
|
539
|
+
{
|
|
540
|
+
"status": "archived",
|
|
541
|
+
"score": 11
|
|
542
|
+
}
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
**Response**
|
|
546
|
+
|
|
547
|
+
```json
|
|
548
|
+
{
|
|
549
|
+
"_id": "object-1",
|
|
550
|
+
"status": "archived",
|
|
551
|
+
"score": 11
|
|
552
|
+
}
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### Patch One
|
|
556
|
+
|
|
557
|
+
`PATCH /api/{db}/{collection}/object/{_id}`
|
|
558
|
+
|
|
559
|
+
`PATCH` is a Mongo-like partial update. Use `$set` for dot-path writes and `$unset` for dot-path removals. `_id` cannot be changed.
|
|
560
|
+
|
|
561
|
+
**Request**
|
|
562
|
+
|
|
563
|
+
```http
|
|
564
|
+
PATCH {URL}/api/{db}/{collection}/object/object-1
|
|
565
|
+
Authorization: Bearer <accessToken>
|
|
566
|
+
Content-Type: application/json
|
|
567
|
+
|
|
568
|
+
{
|
|
569
|
+
"$set": {
|
|
570
|
+
"score": 12,
|
|
571
|
+
"profile.city": "Hamburg"
|
|
572
|
+
},
|
|
573
|
+
"$unset": ["legacyFlag"]
|
|
574
|
+
}
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
**Response**
|
|
578
|
+
|
|
579
|
+
```json
|
|
580
|
+
{
|
|
581
|
+
"_id": "object-1",
|
|
582
|
+
"status": "archived",
|
|
583
|
+
"score": 12,
|
|
584
|
+
"profile": {
|
|
585
|
+
"city": "Hamburg"
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
### Delete One
|
|
591
|
+
|
|
592
|
+
`DELETE /api/{db}/{collection}/object/{_id}`
|
|
593
|
+
|
|
594
|
+
**Request**
|
|
595
|
+
|
|
596
|
+
```http
|
|
597
|
+
DELETE {URL}/api/{db}/{collection}/object/object-1
|
|
598
|
+
Authorization: Bearer <accessToken>
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
**Response**
|
|
602
|
+
|
|
603
|
+
```http
|
|
604
|
+
HTTP/1.1 204 No Content
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
## File Handling
|
|
608
|
+
|
|
609
|
+
File handling is object-backed. One object can wrap one S3-backed blob, while the database object stores only a small `_file` descriptor. The S3 bucket should stay private; blob reads go through the API and use the same read permissions as the wrapping object.
|
|
610
|
+
|
|
611
|
+
```json
|
|
612
|
+
{
|
|
613
|
+
"_file": {
|
|
614
|
+
"storage": "s3",
|
|
615
|
+
"key": "objydb/files/db/{db}/collection/{collection}/object/object-1/blob/{fileId}",
|
|
616
|
+
"mimeType": "application/pdf"
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
`_file.key` is derived by the server from `{db}`, `{collection}`, `_id`, and a generated file id. Clients should not invent or change it. Replacing a file writes a new blob key, commits the object pointer, then removes the old blob best-effort. Bulk file uploads are not supported; use one multipart request per file object.
|
|
622
|
+
|
|
623
|
+
### Create Object With File
|
|
624
|
+
|
|
625
|
+
`POST /api/{db}/{collection}/object`
|
|
626
|
+
|
|
627
|
+
**Request**
|
|
628
|
+
|
|
629
|
+
```http
|
|
630
|
+
POST {URL}/api/{db}/{collection}/object
|
|
631
|
+
Authorization: Bearer <accessToken>
|
|
632
|
+
Content-Type: multipart/form-data
|
|
633
|
+
|
|
634
|
+
part "object":
|
|
635
|
+
{
|
|
636
|
+
"_id": "object-1",
|
|
637
|
+
"name": "invoice.pdf",
|
|
638
|
+
"status": "received"
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
part "file":
|
|
642
|
+
<binary file content>
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
**Response**
|
|
646
|
+
|
|
647
|
+
```json
|
|
648
|
+
{
|
|
649
|
+
"_id": "object-1",
|
|
650
|
+
"name": "invoice.pdf",
|
|
651
|
+
"status": "received",
|
|
652
|
+
"_file": {
|
|
653
|
+
"storage": "s3",
|
|
654
|
+
"key": "objydb/files/db/{db}/collection/{collection}/object/object-1/blob/{fileId}",
|
|
655
|
+
"mimeType": "application/pdf"
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
### Read File Blob
|
|
661
|
+
|
|
662
|
+
`GET /api/{db}/{collection}/object/{_id}/blob`
|
|
663
|
+
|
|
664
|
+
**Request**
|
|
665
|
+
|
|
666
|
+
```http
|
|
667
|
+
GET {URL}/api/{db}/{collection}/object/object-1/blob
|
|
668
|
+
Authorization: Bearer <accessToken>
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
**Response**
|
|
672
|
+
|
|
673
|
+
```http
|
|
674
|
+
HTTP/1.1 200 OK
|
|
675
|
+
Content-Type: application/pdf
|
|
676
|
+
|
|
677
|
+
<binary file content>
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
### Replace Object With File
|
|
681
|
+
|
|
682
|
+
`PUT /api/{db}/{collection}/object/{_id}`
|
|
683
|
+
|
|
684
|
+
`PUT` replaces the full wrapping object and stores the new file under a new server-derived blob key. No old file version is exposed by OBJY DB; old blob cleanup is best-effort after the object pointer is durable.
|
|
685
|
+
|
|
686
|
+
**Request**
|
|
687
|
+
|
|
688
|
+
```http
|
|
689
|
+
PUT {URL}/api/{db}/{collection}/object/object-1
|
|
690
|
+
Authorization: Bearer <accessToken>
|
|
691
|
+
Content-Type: multipart/form-data
|
|
692
|
+
|
|
693
|
+
part "object":
|
|
694
|
+
{
|
|
695
|
+
"name": "invoice-final.pdf",
|
|
696
|
+
"status": "approved"
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
part "file":
|
|
700
|
+
<binary file content>
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
**Response**
|
|
704
|
+
|
|
705
|
+
```json
|
|
706
|
+
{
|
|
707
|
+
"_id": "object-1",
|
|
708
|
+
"name": "invoice-final.pdf",
|
|
709
|
+
"status": "approved",
|
|
710
|
+
"_file": {
|
|
711
|
+
"storage": "s3",
|
|
712
|
+
"key": "objydb/files/db/{db}/collection/{collection}/object/object-1/blob/{fileId}",
|
|
713
|
+
"mimeType": "application/pdf"
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
### Delete File Object
|
|
719
|
+
|
|
720
|
+
`DELETE /api/{db}/{collection}/object/{_id}`
|
|
721
|
+
|
|
722
|
+
Delete the wrapping object through normal object CRUD. The object delete is durable first; blob deletion is then attempted best-effort.
|
|
723
|
+
|
|
724
|
+
**Request**
|
|
725
|
+
|
|
726
|
+
```http
|
|
727
|
+
DELETE {URL}/api/{db}/{collection}/object/object-1
|
|
728
|
+
Authorization: Bearer <accessToken>
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
**Response**
|
|
732
|
+
|
|
733
|
+
```http
|
|
734
|
+
HTTP/1.1 204 No Content
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
## Bulk Objects
|
|
738
|
+
|
|
739
|
+
Batches are split by object shard internally. Duplicate ids inside one bulk request are rejected.
|
|
740
|
+
|
|
741
|
+
### Insert Many
|
|
742
|
+
|
|
743
|
+
`POST /api/{db}/{collection}/object`
|
|
744
|
+
|
|
745
|
+
**Request**
|
|
746
|
+
|
|
747
|
+
```http
|
|
748
|
+
POST {URL}/api/{db}/{collection}/object
|
|
749
|
+
Authorization: Bearer <accessToken>
|
|
750
|
+
Content-Type: application/json
|
|
751
|
+
|
|
752
|
+
[
|
|
753
|
+
{ "_id": "object-1", "status": "active", "score": 42 },
|
|
754
|
+
{ "_id": "object-2", "status": "archived", "score": 10 }
|
|
755
|
+
]
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
**Response**
|
|
759
|
+
|
|
760
|
+
```json
|
|
761
|
+
{
|
|
762
|
+
"inserted": 2,
|
|
763
|
+
"items": [
|
|
764
|
+
{ "_id": "object-1", "status": "active", "score": 42 },
|
|
765
|
+
{ "_id": "object-2", "status": "archived", "score": 10 }
|
|
766
|
+
]
|
|
767
|
+
}
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
### Insert Many With Minimal Response
|
|
771
|
+
|
|
772
|
+
`POST /api/{db}/{collection}/object`
|
|
773
|
+
|
|
774
|
+
**Request**
|
|
775
|
+
|
|
776
|
+
```http
|
|
777
|
+
POST {URL}/api/{db}/{collection}/object
|
|
778
|
+
Authorization: Bearer <accessToken>
|
|
779
|
+
Content-Type: application/json
|
|
780
|
+
Prefer: return=minimal
|
|
781
|
+
|
|
782
|
+
[
|
|
783
|
+
{ "_id": "object-1", "status": "active" },
|
|
784
|
+
{ "_id": "object-2", "status": "archived" }
|
|
785
|
+
]
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
**Response**
|
|
789
|
+
|
|
790
|
+
```json
|
|
791
|
+
{
|
|
792
|
+
"inserted": 2
|
|
793
|
+
}
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
### Replace Many
|
|
797
|
+
|
|
798
|
+
`PUT /api/{db}/{collection}/object`
|
|
799
|
+
|
|
800
|
+
**Request**
|
|
801
|
+
|
|
802
|
+
```http
|
|
803
|
+
PUT {URL}/api/{db}/{collection}/object
|
|
804
|
+
Authorization: Bearer <accessToken>
|
|
805
|
+
Content-Type: application/json
|
|
806
|
+
|
|
807
|
+
[
|
|
808
|
+
{ "_id": "object-1", "status": "active", "score": 50 },
|
|
809
|
+
{ "_id": "object-2", "status": "archived", "score": 12 }
|
|
810
|
+
]
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
**Response**
|
|
814
|
+
|
|
815
|
+
```json
|
|
816
|
+
{
|
|
817
|
+
"replaced": 2,
|
|
818
|
+
"items": [
|
|
819
|
+
{ "_id": "object-1", "status": "active", "score": 50 },
|
|
820
|
+
{ "_id": "object-2", "status": "archived", "score": 12 }
|
|
821
|
+
]
|
|
822
|
+
}
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
### Patch Many
|
|
826
|
+
|
|
827
|
+
`PATCH /api/{db}/{collection}/object`
|
|
828
|
+
|
|
829
|
+
**Request**
|
|
830
|
+
|
|
831
|
+
```http
|
|
832
|
+
PATCH {URL}/api/{db}/{collection}/object
|
|
833
|
+
Authorization: Bearer <accessToken>
|
|
834
|
+
Content-Type: application/json
|
|
835
|
+
|
|
836
|
+
[
|
|
837
|
+
{ "_id": "object-1", "$set": { "score": 51 }, "$unset": ["legacyFlag"] },
|
|
838
|
+
{ "_id": "object-2", "$set": { "score": 13 } }
|
|
839
|
+
]
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
**Response**
|
|
843
|
+
|
|
844
|
+
```json
|
|
845
|
+
{
|
|
846
|
+
"updated": 2,
|
|
847
|
+
"skipped": [],
|
|
848
|
+
"items": [
|
|
849
|
+
{ "_id": "object-1", "status": "active", "score": 51 },
|
|
850
|
+
{ "_id": "object-2", "status": "archived", "score": 13 }
|
|
851
|
+
]
|
|
852
|
+
}
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
### Delete Many
|
|
856
|
+
|
|
857
|
+
`DELETE /api/{db}/{collection}/object`
|
|
858
|
+
|
|
859
|
+
**Request**
|
|
860
|
+
|
|
861
|
+
```http
|
|
862
|
+
DELETE {URL}/api/{db}/{collection}/object
|
|
863
|
+
Authorization: Bearer <accessToken>
|
|
864
|
+
Content-Type: application/json
|
|
865
|
+
|
|
866
|
+
{
|
|
867
|
+
"ids": ["object-1", "object-2"]
|
|
868
|
+
}
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
**Response**
|
|
872
|
+
|
|
873
|
+
```json
|
|
874
|
+
{
|
|
875
|
+
"deleted": 2,
|
|
876
|
+
"ids": ["object-1", "object-2"],
|
|
877
|
+
"skipped": []
|
|
878
|
+
}
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
The delete body may also be a plain array of ids.
|
|
882
|
+
|
|
883
|
+
### Streamed Bulk Writes
|
|
884
|
+
|
|
885
|
+
Large imports can use NDJSON so the server does not need to hold one giant JSON array in memory. Each line is one JSON value. Internally, OBJY DB buffers lines into bounded chunks and then uses the same bulk storage, routing, replication, durability, permission, and index paths as normal bulk writes.
|
|
886
|
+
|
|
887
|
+
Streamed writes require minimal responses:
|
|
888
|
+
|
|
889
|
+
```http
|
|
890
|
+
Content-Type: application/x-ndjson
|
|
891
|
+
Prefer: return=minimal
|
|
892
|
+
```
|
|
893
|
+
|
|
894
|
+
If a streamed request fails after earlier chunks were written, those earlier chunks stay committed. Use stable `_id` values when clients retry large streams. Omitted `_id` values are allowed on insert, but generated ids are not returned by minimal streamed responses.
|
|
895
|
+
|
|
896
|
+
#### Stream Insert
|
|
897
|
+
|
|
898
|
+
`POST /api/{db}/{collection}/object`
|
|
899
|
+
|
|
900
|
+
**Request**
|
|
901
|
+
|
|
902
|
+
```http
|
|
903
|
+
POST {URL}/api/{db}/{collection}/object
|
|
904
|
+
Authorization: Bearer <accessToken>
|
|
905
|
+
Content-Type: application/x-ndjson
|
|
906
|
+
Prefer: return=minimal
|
|
907
|
+
|
|
908
|
+
{"_id":"object-1","status":"active","score":42}
|
|
909
|
+
{"_id":"object-2","status":"archived","score":10}
|
|
910
|
+
{"_id":"object-3","status":"generated","score":3}
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
**Response**
|
|
914
|
+
|
|
915
|
+
```json
|
|
916
|
+
{
|
|
917
|
+
"inserted": 3
|
|
918
|
+
}
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
#### Stream Replace
|
|
922
|
+
|
|
923
|
+
`PUT /api/{db}/{collection}/object`
|
|
924
|
+
|
|
925
|
+
**Request**
|
|
926
|
+
|
|
927
|
+
```http
|
|
928
|
+
PUT {URL}/api/{db}/{collection}/object
|
|
929
|
+
Authorization: Bearer <accessToken>
|
|
930
|
+
Content-Type: application/x-ndjson
|
|
931
|
+
Prefer: return=minimal
|
|
932
|
+
|
|
933
|
+
{"_id":"object-1","status":"active","score":50}
|
|
934
|
+
{"_id":"object-2","status":"archived","score":12}
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
**Response**
|
|
938
|
+
|
|
939
|
+
```json
|
|
940
|
+
{
|
|
941
|
+
"replaced": 2
|
|
942
|
+
}
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
#### Stream Patch
|
|
946
|
+
|
|
947
|
+
`PATCH /api/{db}/{collection}/object`
|
|
948
|
+
|
|
949
|
+
**Request**
|
|
950
|
+
|
|
951
|
+
```http
|
|
952
|
+
PATCH {URL}/api/{db}/{collection}/object
|
|
953
|
+
Authorization: Bearer <accessToken>
|
|
954
|
+
Content-Type: application/x-ndjson
|
|
955
|
+
Prefer: return=minimal
|
|
956
|
+
|
|
957
|
+
{"_id":"object-1","$set":{"score":51},"$unset":["legacyFlag"]}
|
|
958
|
+
{"_id":"object-2","$set":{"score":13}}
|
|
959
|
+
{"_id":"missing","$set":{"score":99}}
|
|
960
|
+
```
|
|
961
|
+
|
|
962
|
+
**Response**
|
|
963
|
+
|
|
964
|
+
```json
|
|
965
|
+
{
|
|
966
|
+
"updated": 2,
|
|
967
|
+
"skipped": 1
|
|
968
|
+
}
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
#### Stream Delete
|
|
972
|
+
|
|
973
|
+
`DELETE /api/{db}/{collection}/object`
|
|
974
|
+
|
|
975
|
+
**Request**
|
|
976
|
+
|
|
977
|
+
```http
|
|
978
|
+
DELETE {URL}/api/{db}/{collection}/object
|
|
979
|
+
Authorization: Bearer <accessToken>
|
|
980
|
+
Content-Type: application/x-ndjson
|
|
981
|
+
Prefer: return=minimal
|
|
982
|
+
|
|
983
|
+
{"_id":"object-1"}
|
|
984
|
+
{"_id":"object-2"}
|
|
985
|
+
{"_id":"missing"}
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
**Response**
|
|
989
|
+
|
|
990
|
+
```json
|
|
991
|
+
{
|
|
992
|
+
"deleted": 2,
|
|
993
|
+
"skipped": 1
|
|
994
|
+
}
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
## Query
|
|
998
|
+
|
|
999
|
+
Query responses are plain arrays. Use GET for small URL-safe queries and POST `/search` for larger payloads. Normal object queries default to `250` rows and cap each page at `250`; use `offset` / `$skip` or `$page` to read more.
|
|
1000
|
+
|
|
1001
|
+
### GET Query
|
|
1002
|
+
|
|
1003
|
+
`GET /api/{db}/{collection}/object`
|
|
1004
|
+
|
|
1005
|
+
`q` is URL-encoded JSON. The decoded query in this example is `{"status":"active","score":{"$gte":10}}`.
|
|
1006
|
+
|
|
1007
|
+
**Request**
|
|
1008
|
+
|
|
1009
|
+
```http
|
|
1010
|
+
GET {URL}/api/{db}/{collection}/object?q=%7B%22status%22%3A%22active%22%2C%22score%22%3A%7B%22%24gte%22%3A10%7D%7D&sortBy=score&sort=desc&limit=50&offset=0
|
|
1011
|
+
Authorization: Bearer <accessToken>
|
|
1012
|
+
```
|
|
1013
|
+
|
|
1014
|
+
**Response**
|
|
1015
|
+
|
|
1016
|
+
```json
|
|
1017
|
+
[
|
|
1018
|
+
{ "_id": "object-1", "status": "active", "score": 42 },
|
|
1019
|
+
{ "_id": "object-9", "status": "active", "score": 31 }
|
|
1020
|
+
]
|
|
1021
|
+
```
|
|
1022
|
+
|
|
1023
|
+
### POST Search
|
|
1024
|
+
|
|
1025
|
+
`POST /api/{db}/{collection}/search`
|
|
1026
|
+
|
|
1027
|
+
**Request**
|
|
1028
|
+
|
|
1029
|
+
```http
|
|
1030
|
+
POST {URL}/api/{db}/{collection}/search
|
|
1031
|
+
Authorization: Bearer <accessToken>
|
|
1032
|
+
Content-Type: application/json
|
|
1033
|
+
|
|
1034
|
+
{
|
|
1035
|
+
"status": "active",
|
|
1036
|
+
"$sort": { "score": -1 },
|
|
1037
|
+
"$page": 1,
|
|
1038
|
+
"$pageSize": 50,
|
|
1039
|
+
"$refDepth": 1
|
|
1040
|
+
}
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
You may also use `$limit` and `$skip` in the POST body. URL `limit` / `offset` parameters take precedence when both are present. Page size is capped at `250`.
|
|
1044
|
+
|
|
1045
|
+
**Response**
|
|
1046
|
+
|
|
1047
|
+
```json
|
|
1048
|
+
[
|
|
1049
|
+
{ "_id": "object-1", "status": "active", "score": 42 }
|
|
1050
|
+
]
|
|
1051
|
+
```
|
|
1052
|
+
|
|
1053
|
+
### Operator Query
|
|
1054
|
+
|
|
1055
|
+
`POST /api/{db}/{collection}/search`
|
|
1056
|
+
|
|
1057
|
+
**Request**
|
|
1058
|
+
|
|
1059
|
+
```http
|
|
1060
|
+
POST {URL}/api/{db}/{collection}/search
|
|
1061
|
+
Authorization: Bearer <accessToken>
|
|
1062
|
+
Content-Type: application/json
|
|
1063
|
+
|
|
1064
|
+
{
|
|
1065
|
+
"status": { "$in": ["active", "queued"] },
|
|
1066
|
+
"score": { "$gte": 10, "$lt": 100 },
|
|
1067
|
+
"tags": { "$contains": "paid" },
|
|
1068
|
+
"$or": [
|
|
1069
|
+
{ "status": "active" },
|
|
1070
|
+
{ "score": { "$gt": 1000 } }
|
|
1071
|
+
]
|
|
1072
|
+
}
|
|
1073
|
+
```
|
|
1074
|
+
|
|
1075
|
+
**Response**
|
|
1076
|
+
|
|
1077
|
+
```json
|
|
1078
|
+
[
|
|
1079
|
+
{ "_id": "object-1", "status": "active", "score": 42, "tags": ["paid"] }
|
|
1080
|
+
]
|
|
1081
|
+
```
|
|
1082
|
+
|
|
1083
|
+
Also supported: `$eq`, `$equal`, `$deepEquals`, `$ne`, `$in`, `$nin`, `$regex`, `$like`, `$likeI`, `$startsWith`, `$endsWith`, `$contains`, `$elemMatch`, `$exists`, `$null`, `$type`, `$size`, `$mod`, `$between`, `$before`, `$after`, `$and`, `$or`, `$nor`, `$not`, `$all`, and `$any`.
|
|
1084
|
+
|
|
1085
|
+
### Query Path
|
|
1086
|
+
|
|
1087
|
+
Query, vector-search, and aggregation responses include the planner decision in:
|
|
1088
|
+
|
|
1089
|
+
```http
|
|
1090
|
+
x-objydb-query-path: index-full
|
|
1091
|
+
```
|
|
1092
|
+
|
|
1093
|
+
<details>
|
|
1094
|
+
<summary><strong>Planner path values</strong></summary>
|
|
1095
|
+
|
|
1096
|
+
Read paths include `scan`, `index-full`, `index-partial`, and `ordered-index`. Vector paths are `vector-index` and `vector-filter`. Aggregate paths include `aggregate-column-summary`, `aggregate-column-scan`, `aggregate-group-index`, `aggregate-group-forward-index`, `aggregate-group-column-scan`, and `aggregate-scan`.
|
|
1097
|
+
|
|
1098
|
+
Permission-denied reads report `permission-denied`; reference queries add `reference-` or `aggregate-reference-`. A sharded response may contain multiple comma-separated paths when shards use different plans.
|
|
1099
|
+
|
|
1100
|
+
</details>
|
|
1101
|
+
|
|
1102
|
+
## Aggregation
|
|
1103
|
+
|
|
1104
|
+
Aggregation is supplied as `$aggregate` inside the query object. Supported operators are `$sum`, `$avg`, `$min`, `$max`, and `$count`.
|
|
1105
|
+
|
|
1106
|
+
`POST /api/{db}/{collection}/search`
|
|
1107
|
+
|
|
1108
|
+
**Request**
|
|
1109
|
+
|
|
1110
|
+
```http
|
|
1111
|
+
POST {URL}/api/{db}/{collection}/search
|
|
1112
|
+
Authorization: Bearer <accessToken>
|
|
1113
|
+
Content-Type: application/json
|
|
1114
|
+
|
|
1115
|
+
{
|
|
1116
|
+
"status": "active",
|
|
1117
|
+
"$aggregate": {
|
|
1118
|
+
"groupBy": "status",
|
|
1119
|
+
"fields": {
|
|
1120
|
+
"totalScore": { "$sum": "score" },
|
|
1121
|
+
"avgScore": { "$avg": "score" },
|
|
1122
|
+
"count": { "$count": true }
|
|
1123
|
+
},
|
|
1124
|
+
"$sort": { "totalScore": -1 },
|
|
1125
|
+
"$page": 1,
|
|
1126
|
+
"$pageSize": 20
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
```
|
|
1130
|
+
|
|
1131
|
+
**Response**
|
|
1132
|
+
|
|
1133
|
+
```json
|
|
1134
|
+
[
|
|
1135
|
+
{ "status": "active", "totalScore": 1234, "avgScore": 38.56, "count": 32 }
|
|
1136
|
+
]
|
|
1137
|
+
```
|
|
1138
|
+
|
|
1139
|
+
## Vector Search
|
|
1140
|
+
|
|
1141
|
+
Store embeddings on `_vec`. The vector is indexed separately and hidden from normal read responses.
|
|
1142
|
+
|
|
1143
|
+
### Insert Vector Object
|
|
1144
|
+
|
|
1145
|
+
`POST /api/{db}/{collection}/object`
|
|
1146
|
+
|
|
1147
|
+
**Request**
|
|
1148
|
+
|
|
1149
|
+
```http
|
|
1150
|
+
POST {URL}/api/{db}/{collection}/object
|
|
1151
|
+
Authorization: Bearer <accessToken>
|
|
1152
|
+
Content-Type: application/json
|
|
1153
|
+
|
|
1154
|
+
{
|
|
1155
|
+
"_id": "doc-1",
|
|
1156
|
+
"title": "Hello",
|
|
1157
|
+
"_vec": [0.1, 0.2, 0.3]
|
|
1158
|
+
}
|
|
1159
|
+
```
|
|
1160
|
+
|
|
1161
|
+
**Response**
|
|
1162
|
+
|
|
1163
|
+
```json
|
|
1164
|
+
{
|
|
1165
|
+
"_id": "doc-1",
|
|
1166
|
+
"title": "Hello"
|
|
1167
|
+
}
|
|
1168
|
+
```
|
|
1169
|
+
|
|
1170
|
+
### Search Vectors
|
|
1171
|
+
|
|
1172
|
+
`POST /api/{db}/{collection}/search`
|
|
1173
|
+
|
|
1174
|
+
**Request**
|
|
1175
|
+
|
|
1176
|
+
```http
|
|
1177
|
+
POST {URL}/api/{db}/{collection}/search
|
|
1178
|
+
Authorization: Bearer <accessToken>
|
|
1179
|
+
Content-Type: application/json
|
|
1180
|
+
|
|
1181
|
+
{
|
|
1182
|
+
"$vector": {
|
|
1183
|
+
"vector": [0.1, 0.2, 0.3],
|
|
1184
|
+
"limit": 10,
|
|
1185
|
+
"ef": 64
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
```
|
|
1189
|
+
|
|
1190
|
+
**Response**
|
|
1191
|
+
|
|
1192
|
+
```json
|
|
1193
|
+
[
|
|
1194
|
+
{ "_id": "doc-1", "title": "Hello", "_score": 0.98 }
|
|
1195
|
+
]
|
|
1196
|
+
```
|
|
1197
|
+
|
|
1198
|
+
If the needed vector index is still warming:
|
|
1199
|
+
|
|
1200
|
+
```json
|
|
1201
|
+
{
|
|
1202
|
+
"error": "vector index warming",
|
|
1203
|
+
"indexKey": "all",
|
|
1204
|
+
"indexStatus": "pending"
|
|
1205
|
+
}
|
|
1206
|
+
```
|
|
1207
|
+
|
|
1208
|
+
## Live References
|
|
1209
|
+
|
|
1210
|
+
Reference descriptors point to another object without embedding a stored copy. `database` is optional; if omitted, the current database is used. Reads expand references by default with depth `1`; use `$refDepth: 0` to retain the descriptor only.
|
|
1211
|
+
|
|
1212
|
+
### Store Reference
|
|
1213
|
+
|
|
1214
|
+
`POST /api/{db}/{collection}/object`
|
|
1215
|
+
|
|
1216
|
+
**Request**
|
|
1217
|
+
|
|
1218
|
+
```http
|
|
1219
|
+
POST {URL}/api/{db}/{collection}/object?$refDepth=0
|
|
1220
|
+
Authorization: Bearer <accessToken>
|
|
1221
|
+
Content-Type: application/json
|
|
1222
|
+
|
|
1223
|
+
{
|
|
1224
|
+
"_id": "post-1",
|
|
1225
|
+
"title": "Hello",
|
|
1226
|
+
"author": {
|
|
1227
|
+
"type": "reference",
|
|
1228
|
+
"collection": "users",
|
|
1229
|
+
"value": "user-1"
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
```
|
|
1233
|
+
|
|
1234
|
+
**Response**
|
|
1235
|
+
|
|
1236
|
+
```json
|
|
1237
|
+
{
|
|
1238
|
+
"_id": "post-1",
|
|
1239
|
+
"title": "Hello",
|
|
1240
|
+
"author": {
|
|
1241
|
+
"type": "reference",
|
|
1242
|
+
"collection": "users",
|
|
1243
|
+
"value": "user-1"
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
```
|
|
1247
|
+
|
|
1248
|
+
### Read Expanded Reference
|
|
1249
|
+
|
|
1250
|
+
`GET /api/{db}/{collection}/object/{_id}`
|
|
1251
|
+
|
|
1252
|
+
Reads expand references by default with depth `1`.
|
|
1253
|
+
|
|
1254
|
+
**Request**
|
|
1255
|
+
|
|
1256
|
+
```http
|
|
1257
|
+
GET {URL}/api/{db}/{collection}/object/post-1
|
|
1258
|
+
Authorization: Bearer <accessToken>
|
|
1259
|
+
```
|
|
1260
|
+
|
|
1261
|
+
**Response**
|
|
1262
|
+
|
|
1263
|
+
```json
|
|
1264
|
+
{
|
|
1265
|
+
"_id": "post-1",
|
|
1266
|
+
"title": "Hello",
|
|
1267
|
+
"author": {
|
|
1268
|
+
"type": "reference",
|
|
1269
|
+
"collection": "users",
|
|
1270
|
+
"value": "user-1",
|
|
1271
|
+
"object": { "_id": "user-1", "username": "alice" }
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
```
|
|
1275
|
+
|
|
1276
|
+
### Read Unexpanded Reference
|
|
1277
|
+
|
|
1278
|
+
`GET /api/{db}/{collection}/object/{_id}`
|
|
1279
|
+
|
|
1280
|
+
**Request**
|
|
1281
|
+
|
|
1282
|
+
```http
|
|
1283
|
+
GET {URL}/api/{db}/{collection}/object/post-1?$refDepth=0
|
|
1284
|
+
Authorization: Bearer <accessToken>
|
|
1285
|
+
```
|
|
1286
|
+
|
|
1287
|
+
**Response**
|
|
1288
|
+
|
|
1289
|
+
```json
|
|
1290
|
+
{
|
|
1291
|
+
"_id": "post-1",
|
|
1292
|
+
"title": "Hello",
|
|
1293
|
+
"author": {
|
|
1294
|
+
"type": "reference",
|
|
1295
|
+
"collection": "users",
|
|
1296
|
+
"value": "user-1"
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
```
|
|
1300
|
+
|
|
1301
|
+
### Query Reference Path
|
|
1302
|
+
|
|
1303
|
+
`POST /api/{db}/{collection}/search`
|
|
1304
|
+
|
|
1305
|
+
**Request**
|
|
1306
|
+
|
|
1307
|
+
```http
|
|
1308
|
+
POST {URL}/api/{db}/{collection}/search
|
|
1309
|
+
Authorization: Bearer <accessToken>
|
|
1310
|
+
Content-Type: application/json
|
|
1311
|
+
|
|
1312
|
+
{
|
|
1313
|
+
"author.object.username": "alice"
|
|
1314
|
+
}
|
|
1315
|
+
```
|
|
1316
|
+
|
|
1317
|
+
**Response**
|
|
1318
|
+
|
|
1319
|
+
```json
|
|
1320
|
+
[
|
|
1321
|
+
{
|
|
1322
|
+
"_id": "post-1",
|
|
1323
|
+
"title": "Hello",
|
|
1324
|
+
"author": {
|
|
1325
|
+
"type": "reference",
|
|
1326
|
+
"collection": "users",
|
|
1327
|
+
"value": "user-1",
|
|
1328
|
+
"object": { "_id": "user-1", "username": "alice" }
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
]
|
|
1332
|
+
```
|
|
1333
|
+
|
|
1334
|
+
Vector search cannot currently be combined with reference object paths.
|
|
1335
|
+
|
|
1336
|
+
## Health and Cluster Status
|
|
1337
|
+
|
|
1338
|
+
### Health
|
|
1339
|
+
|
|
1340
|
+
The health route does not require authentication. During restore it remains available while data routes return `503` with `Retry-After: 5`.
|
|
1341
|
+
|
|
1342
|
+
`GET /health`
|
|
1343
|
+
|
|
1344
|
+
**Request**
|
|
1345
|
+
|
|
1346
|
+
```http
|
|
1347
|
+
GET {URL}/health
|
|
1348
|
+
```
|
|
1349
|
+
|
|
1350
|
+
**Response**
|
|
1351
|
+
|
|
1352
|
+
```json
|
|
1353
|
+
{
|
|
1354
|
+
"ok": true,
|
|
1355
|
+
"node": "node-8080",
|
|
1356
|
+
"restore": null
|
|
1357
|
+
}
|
|
1358
|
+
```
|
|
1359
|
+
|
|
1360
|
+
While restoring, the response also has `"restoring": true` and `restore` contains current progress.
|
|
1361
|
+
|
|
1362
|
+
### Public Cluster Status
|
|
1363
|
+
|
|
1364
|
+
`/api/config` is the compact public status endpoint for the UI and operators. It requires a global admin token. Cluster collection is cached for one second so concurrent status requests reuse the same peer fan-out.
|
|
1365
|
+
|
|
1366
|
+
`GET /api/config`
|
|
1367
|
+
|
|
1368
|
+
**Request**
|
|
1369
|
+
|
|
1370
|
+
```http
|
|
1371
|
+
GET {URL}/api/config
|
|
1372
|
+
Authorization: Bearer <accessToken>
|
|
1373
|
+
```
|
|
1374
|
+
|
|
1375
|
+
<details>
|
|
1376
|
+
<summary><strong>Response example</strong></summary>
|
|
1377
|
+
|
|
1378
|
+
```json
|
|
1379
|
+
{
|
|
1380
|
+
"ok": true,
|
|
1381
|
+
"generatedAt": "2026-09-18T08:41:22.000Z",
|
|
1382
|
+
"node": { "id": "node-8080", "url": "{URL}" },
|
|
1383
|
+
"rss": {
|
|
1384
|
+
"cluster": { "rss": 2190999552, "nodeCount": 1, "onlineNodeCount": 1 },
|
|
1385
|
+
"logical": { "rss": 2190999552, "nodeCount": 1, "onlineNodeCount": 1 },
|
|
1386
|
+
"physical": { "rss": 2190999552, "nodeCount": 1, "onlineNodeCount": 1 }
|
|
1387
|
+
},
|
|
1388
|
+
"heap": {
|
|
1389
|
+
"cluster": { "used": 1701040128, "total": 4294967296, "percent": 39.6 },
|
|
1390
|
+
"logical": { "used": 1701040128, "total": 4294967296, "percent": 39.6 },
|
|
1391
|
+
"physical": { "used": 1701040128, "total": 4294967296, "percent": 39.6 }
|
|
1392
|
+
},
|
|
1393
|
+
"indexes": {
|
|
1394
|
+
"ready": 18,
|
|
1395
|
+
"building": 2,
|
|
1396
|
+
"error": 0,
|
|
1397
|
+
"activeTasks": 1
|
|
1398
|
+
},
|
|
1399
|
+
"vectors": {
|
|
1400
|
+
"storedVectors": 4200,
|
|
1401
|
+
"activeIndexes": 2,
|
|
1402
|
+
"warmingIndexes": 0,
|
|
1403
|
+
"errors": 0
|
|
1404
|
+
},
|
|
1405
|
+
"compression": {
|
|
1406
|
+
"compressedObjects": 120000,
|
|
1407
|
+
"uncompressedBytes": 2899102924,
|
|
1408
|
+
"compressedBytes": 577136230,
|
|
1409
|
+
"savedPercent": 80.1
|
|
1410
|
+
},
|
|
1411
|
+
"nodes": [
|
|
1412
|
+
{
|
|
1413
|
+
"node": { "id": "node-8080", "url": "{URL}" },
|
|
1414
|
+
"ownedShards": ["shard-0", "shard-3"],
|
|
1415
|
+
"memory": { "rss": 2190999552, "heapUsed": 1701040128 },
|
|
1416
|
+
"replication": {
|
|
1417
|
+
"healthy": true,
|
|
1418
|
+
"lastSuccessfulSyncAt": "2026-09-18T08:41:22.000Z",
|
|
1419
|
+
"pendingHints": 0,
|
|
1420
|
+
"inflightPulls": 0,
|
|
1421
|
+
"error": null
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
],
|
|
1425
|
+
"shards": [
|
|
1426
|
+
{
|
|
1427
|
+
"name": "shard-0",
|
|
1428
|
+
"peers": [
|
|
1429
|
+
{ "url": "{URL}", "online": true }
|
|
1430
|
+
]
|
|
1431
|
+
}
|
|
1432
|
+
]
|
|
1433
|
+
}
|
|
1434
|
+
```
|
|
1435
|
+
|
|
1436
|
+
</details>
|
|
1437
|
+
|
|
1438
|
+
The normal response contains one shard topology, lightweight node health, logical and physical heap/RSS totals, per-node memory-guard state, and aggregate index, vector, and logical compression information. Logical totals select one reachable replica per shard; physical totals count every process. The example abbreviates fields inside the memory summaries; the live response also includes byte/MiB totals, capacity percentages, and online/offline counts.
|
|
1439
|
+
|
|
1440
|
+
`indexes.activeTasks` counts active cooperative index, column, and vector maintenance tasks in the node process.
|
|
1441
|
+
|
|
1442
|
+
`replication.healthy` means the node has no known replication failure or queued/in-flight catch-up work. It is an operational health signal, not a full data-checksum comparison between replicas.
|
|
1443
|
+
|
|
1444
|
+
Detailed request:
|
|
1445
|
+
|
|
1446
|
+
```http
|
|
1447
|
+
GET {URL}/api/config?details=1
|
|
1448
|
+
Authorization: Bearer <accessToken>
|
|
1449
|
+
```
|
|
1450
|
+
|
|
1451
|
+
The detailed response adds per-collection index, vector, and compression state plus full replication diagnostics. The admin UI requests it only when the cluster-status modal is opened. Restore progress is included only while a peer is restoring.
|
|
1452
|
+
|
|
1453
|
+
## Responses, Errors, and Limits
|
|
1454
|
+
|
|
1455
|
+
OBJY DB keeps public responses close to the requested operation:
|
|
1456
|
+
|
|
1457
|
+
| Operation | Response shape |
|
|
1458
|
+
| --- | --- |
|
|
1459
|
+
| Single create, read, replace, or patch | One object. |
|
|
1460
|
+
| Query, search, vector search, or aggregation | An array. |
|
|
1461
|
+
| Bulk write | Count fields plus returned items unless minimal response was requested. |
|
|
1462
|
+
| Bulk delete | `{ deleted, ids, skipped }`. |
|
|
1463
|
+
| Successful delete | `204 No Content`. |
|
|
1464
|
+
|
|
1465
|
+
Errors use a JSON body containing at least `error`. Additional fields may include `code`, `operationStarted`, `writeMayHaveSucceeded`, committed import counts, restore progress, or index readiness.
|
|
1466
|
+
|
|
1467
|
+
Normal object queries default to 250 rows and cap a page at 250. JSON bodies, individual objects, bulk counts, bulk bytes, file uploads, and pending persistence pressure are bounded by server configuration. Oversized requests return `413`; temporary restore or pressure responses may include `Retry-After`.
|
|
1468
|
+
|
|
1469
|
+
NDJSON streams commit bounded batches as they are processed. A later malformed line or failed batch does not roll back earlier committed batches.
|