@owlmeans/mongo 0.1.18-rc.3 → 0.1.18-rc.30
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 +13 -9
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/mongo/SKILL.md +52 -19
- package/build/service.d.ts.map +1 -1
- package/build/service.js +1 -16
- package/build/service.js.map +1 -1
- package/package.json +8 -8
- package/src/service.ts +1 -22
- package/tests/crud.spec.ts +153 -0
- package/tests/references.spec.ts +8 -6
package/README.md
CHANGED
|
@@ -6,13 +6,13 @@ MongoDB service for OwlMeans server contexts — connection management with repl
|
|
|
6
6
|
|
|
7
7
|
- `makeMongoDbService(alias?)` — creates a MongoDB connection service
|
|
8
8
|
- `appendMongo(context, alias?)` — registers the service in the context
|
|
9
|
-
- Reads connection config from `
|
|
9
|
+
- Reads connection config from the `dbs` entry whose alias matches (supports `kluster:` directives)
|
|
10
10
|
- Used as the database provider for `@owlmeans/mongo-resource`
|
|
11
11
|
|
|
12
12
|
## Installation
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
bun add @owlmeans/mongo
|
|
15
|
+
bun add @owlmeans/mongo@^0.1.18-rc.30
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
## Usage
|
|
@@ -24,16 +24,20 @@ import { appendMongo, DEFAULT_ALIAS as MONGO_SERVICE } from '@owlmeans/mongo'
|
|
|
24
24
|
appendMongo<C, T>(context)
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
Config (`config.json`)
|
|
27
|
+
Config (`config.json`) — `dbs` is a list, and `schema` names the database every resource on this
|
|
28
|
+
connection reads and writes (falling back to the config `alias`, then the service alias):
|
|
28
29
|
|
|
29
30
|
```json
|
|
30
31
|
{
|
|
31
|
-
"dbs":
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
"
|
|
32
|
+
"dbs": [
|
|
33
|
+
{
|
|
34
|
+
"service": "mongo",
|
|
35
|
+
"alias": "mongo",
|
|
36
|
+
"host": "localhost",
|
|
37
|
+
"port": 27017,
|
|
38
|
+
"schema": "myapp"
|
|
35
39
|
}
|
|
36
|
-
|
|
40
|
+
]
|
|
37
41
|
}
|
|
38
42
|
```
|
|
39
43
|
|
|
@@ -66,7 +70,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
66
70
|
your project's skill store (`.agents/skills/`):
|
|
67
71
|
|
|
68
72
|
```sh
|
|
69
|
-
npx @owlmeans/agent-skills
|
|
73
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.32
|
|
70
74
|
```
|
|
71
75
|
|
|
72
76
|
The embedded files are version-matched to this package release. Do not edit them
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/mongo",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-
|
|
4
|
+
"version": "0.1.18-rc.30",
|
|
5
|
+
"generatedAt": "2026-09-22T12:36:21.843Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mongo
|
|
3
|
-
description: How to use @owlmeans/mongo — MongoDB connection service (makeMongoDbService / appendMongo) registered on a server context; cluster setup,
|
|
3
|
+
description: How to use @owlmeans/mongo — MongoDB connection service (makeMongoDbService / appendMongo) registered on a server context; cluster setup, field encryption backend. Auto-invoked when wiring MongoDB into a server app.
|
|
4
4
|
user-invocable: false
|
|
5
5
|
---
|
|
6
6
|
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
@@ -8,7 +8,7 @@ user-invocable: false
|
|
|
8
8
|
# @owlmeans/mongo
|
|
9
9
|
|
|
10
10
|
**Layer:** Infra
|
|
11
|
-
**Install:** `"@owlmeans/mongo": "^0.1.18-rc.
|
|
11
|
+
**Install:** `"@owlmeans/mongo": "^0.1.18-rc.30"` in `dependencies`
|
|
12
12
|
|
|
13
13
|
## Key Exports
|
|
14
14
|
|
|
@@ -16,7 +16,8 @@ user-invocable: false
|
|
|
16
16
|
|--------|-------------|
|
|
17
17
|
| `makeMongoDbService(alias?)` | Factory for the MongoDB connection service (implements `MongoDbService` from [[mongo-resource]]). |
|
|
18
18
|
| `appendMongo(context, alias?)` | Register the service on a server context. Default alias `'mongo'` (`DEFAULT_ALIAS`). |
|
|
19
|
-
| `DEFAULT_ALIAS` | `'mongo'
|
|
19
|
+
| `DEFAULT_ALIAS`, `DEF_REPLSET` | `'mongo'`; `'rs-main'`, the replica set an array `host` bootstraps. |
|
|
20
|
+
| `MongoMeta` | The `DbConfig.meta` shape this package reads — `{ replicaSet }`. |
|
|
20
21
|
|
|
21
22
|
## Usage
|
|
22
23
|
|
|
@@ -31,38 +32,70 @@ cfg.dbs = [{
|
|
|
31
32
|
host: '127.0.0.1', // or string[] for a cluster — triggers replica set setup
|
|
32
33
|
port: 27017,
|
|
33
34
|
user: 'admin', secret: '...',
|
|
34
|
-
schema: 'my-app', // the DATABASE name
|
|
35
|
+
schema: 'my-app', // the DATABASE name
|
|
35
36
|
encryptionKey: '...', // enables lock()/unlock() field encryption
|
|
36
|
-
|
|
37
|
+
meta: { replicaSet: 'rs-main' }, // only read for an array host
|
|
37
38
|
}]
|
|
38
39
|
```
|
|
39
40
|
|
|
40
|
-
- The service lazily creates one `MongoClient` per config alias
|
|
41
|
-
replica
|
|
41
|
+
- The service lazily creates one `MongoClient` per config alias. An array `host` bootstraps the
|
|
42
|
+
replica set first — `replSetInitiate` when the set was never configured, a forced
|
|
43
|
+
`replSetReconfig` when the members' addresses moved, up to three attempts — then reconnects
|
|
44
|
+
through `?replicaSet=<meta.replicaSet ?? DEF_REPLSET>`.
|
|
42
45
|
- `lock`/`unlock` encrypt/decrypt record fields with `encryptionKey` via
|
|
43
46
|
`@owlmeans/basic-keys` — the backend behind `MongoResource.lock()`.
|
|
44
|
-
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
- **`user` decides how the connection string is assembled.** With `user` set, a single `host`
|
|
48
|
+
becomes `mongodb://<host>[:<port>]` and the client gets `directConnection: true`, which is what a
|
|
49
|
+
standalone node needs: advertising a replica set instead would block the driver on server
|
|
50
|
+
selection looking for a primary that never appears. `meta.replicaSet` is read only on the
|
|
51
|
+
multi-host path.
|
|
52
|
+
- **An entry with no `user` is passed through verbatim.** No scheme is prepended, `port` is not
|
|
53
|
+
appended and no client options are set at all — so such an entry must spell the whole connection
|
|
54
|
+
string in `host` (`mongodb://127.0.0.1:27017/?directConnection=true`). A bare `127.0.0.1` reaches
|
|
55
|
+
the driver as-is and is rejected for having no scheme.
|
|
56
|
+
- The database name is `config.schema ?? config.alias ?? service.alias`, taken as given — the
|
|
57
|
+
service's `name(alias?)` returns it. One database per config entry, each carrying its own data
|
|
58
|
+
**and its own migration ledger**; a second database is a second `cfg.dbs` entry.
|
|
47
59
|
|
|
48
60
|
## Tests
|
|
49
61
|
|
|
50
62
|
This package hosts the integration suites for the whole Mongo pair (a devDependency in the
|
|
51
|
-
other direction would be a cycle): `tests/
|
|
52
|
-
`tests/
|
|
53
|
-
repair). Gated on `MONGO_URL` — see
|
|
54
|
-
port-forward to the cluster mongo
|
|
63
|
+
other direction would be a cycle): `tests/crud.spec.ts` (the `Resource` contract against a real
|
|
64
|
+
collection), `tests/migration.spec.ts` (ledger end to end), `tests/references.spec.ts` (ObjectId
|
|
65
|
+
reference conversion, system `$ref:` migration, drift repair). Gated on `MONGO_URL` — see
|
|
66
|
+
[[testing-integration]]; a dev port-forward to the cluster mongo fills the `MONGO_URL` the
|
|
67
|
+
repo's `.env.example` describes.
|
|
55
68
|
|
|
56
69
|
## Depends On
|
|
57
70
|
|
|
58
71
|
- `@owlmeans/resource` (`createDbService`) · `@owlmeans/mongo-resource` (service contract)
|
|
72
|
+
- `@owlmeans/context` · `@owlmeans/server-context` — the service lifecycle and the context it asserts
|
|
59
73
|
- `@owlmeans/basic-keys` — field encryption
|
|
60
|
-
-
|
|
74
|
+
- `mongodb` (`^7.5.0`) — a direct dependency, not a peer: the driver is resolved here and the
|
|
75
|
+
resource package takes it as a peer
|
|
61
76
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
77
|
+
## Runtime floor
|
|
78
|
+
|
|
79
|
+
On Bun, loading this package requires **Bun 1.4.0 or newer**, and that floor is the whole
|
|
80
|
+
requirement — `bson` then resolves freely inside the driver's own `^7.2.0` range and needs no
|
|
81
|
+
override or pin. `bson@7.3.x` runs a static initializer that calls
|
|
82
|
+
`process.getBuiltinModule('v8').startupSnapshot.isBuildingSnapshot()`, which Bun implements from
|
|
83
|
+
1.4.0 on. Below that, `import 'mongodb'` throws `NotImplementedError: node:v8 isBuildingSnapshot is
|
|
84
|
+
not yet implemented in Bun` before any application code runs, so the symptom is a process that
|
|
85
|
+
never starts rather than a query that misbehaves. Every runtime that loads the driver counts: the
|
|
86
|
+
local shell, CI, and the container image.
|
|
87
|
+
|
|
88
|
+
An app that cannot raise its runtime pins the older bson in its own root manifest instead:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{ "overrides": { "bson": "7.2.0" } }
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Check the runtime, not the lockfile, before lowering one:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
bun -e "import('mongodb').then(() => console.log('OK')).catch(e => console.log(e.message))"
|
|
98
|
+
```
|
|
66
99
|
|
|
67
100
|
## Related
|
|
68
101
|
|
package/build/service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAK3E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;AAI9D,KAAK,MAAM,GAAG,YAAY,CAAA;AAC1B,UAAU,OAAO,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,CAAE,SAAQ,aAAa,CAAC,CAAC,CAAC;CAAI;AAEzE,eAAO,MAAM,kBAAkB,WAAW,MAAM,KAAmB,cAuGlE,CAAA;AAED,eAAO,MAAM,WAAW,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,WACpE,CAAC,UAAS,MAAM,KACxB,CAMF,CAAA"}
|
package/build/service.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertContext
|
|
1
|
+
import { assertContext } from '@owlmeans/context';
|
|
2
2
|
import { DEFAULT_ALIAS } from './consts.js';
|
|
3
3
|
import { MongoClient } from 'mongodb';
|
|
4
4
|
import { prepareConfig } from './utils/config.js';
|
|
@@ -19,15 +19,6 @@ export const makeMongoDbService = (alias = DEFAULT_ALIAS) => {
|
|
|
19
19
|
if (service.clients[configAlias] != null) {
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
22
|
-
if (service.layers == null) {
|
|
23
|
-
service.layers = [Layer.Global];
|
|
24
|
-
}
|
|
25
|
-
if (config.serviceSensitive && service.layers.includes(Layer.Service)) {
|
|
26
|
-
service.layers.push(Layer.Service);
|
|
27
|
-
}
|
|
28
|
-
if (config.entitySensitive && service.layers.includes(Layer.Entity)) {
|
|
29
|
-
service.layers.push(Layer.Entity);
|
|
30
|
-
}
|
|
31
22
|
let [url, options] = prepareConfig(config);
|
|
32
23
|
let client = new MongoClient(url, options);
|
|
33
24
|
if (Array.isArray(config.host)) {
|
|
@@ -78,12 +69,6 @@ export const makeMongoDbService = (alias = DEFAULT_ALIAS) => {
|
|
|
78
69
|
field,
|
|
79
70
|
fields.includes(field) ? await key.decrypt(value).catch(() => value) : value
|
|
80
71
|
])));
|
|
81
|
-
},
|
|
82
|
-
reinitializeContext: (context) => {
|
|
83
|
-
const _service = makeMongoDbService(alias);
|
|
84
|
-
_service.ctx = context;
|
|
85
|
-
_service.layers = service.layers;
|
|
86
|
-
return _service;
|
|
87
72
|
}
|
|
88
73
|
}, service => async () => {
|
|
89
74
|
const context = assertContext(service.ctx, location);
|
package/build/service.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,
|
|
1
|
+
{"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/mongo",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.30",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -23,19 +23,19 @@
|
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@owlmeans/dep-config": "workspace:*",
|
|
26
|
-
"@owlmeans/test-integration": "^0.1.18-rc.
|
|
27
|
-
"@types/bun": "^1.
|
|
26
|
+
"@owlmeans/test-integration": "^0.1.18-rc.25",
|
|
27
|
+
"@types/bun": "^1.4.0",
|
|
28
28
|
"@types/node": "^26.1.0",
|
|
29
29
|
"nodemon": "^3.1.14",
|
|
30
30
|
"typescript": "^7.0.2"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@noble/hashes": "^1.5.0",
|
|
34
|
-
"@owlmeans/basic-keys": "^0.1.18-rc.
|
|
35
|
-
"@owlmeans/context": "^0.1.18-rc.
|
|
36
|
-
"@owlmeans/mongo-resource": "^0.1.18-rc.
|
|
37
|
-
"@owlmeans/resource": "^0.1.18-rc.
|
|
38
|
-
"@owlmeans/server-context": "^0.1.18-rc.
|
|
34
|
+
"@owlmeans/basic-keys": "^0.1.18-rc.33",
|
|
35
|
+
"@owlmeans/context": "^0.1.18-rc.28",
|
|
36
|
+
"@owlmeans/mongo-resource": "^0.1.18-rc.31",
|
|
37
|
+
"@owlmeans/resource": "^0.1.18-rc.30",
|
|
38
|
+
"@owlmeans/server-context": "^0.1.18-rc.31",
|
|
39
39
|
"@scure/base": "^2.3.0",
|
|
40
40
|
"mongodb": "^7.5.0"
|
|
41
41
|
},
|
package/src/service.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { assertContext
|
|
2
|
-
import type { BasicContext } from '@owlmeans/context'
|
|
1
|
+
import { assertContext } from '@owlmeans/context'
|
|
3
2
|
import { DEFAULT_ALIAS } from './consts.js'
|
|
4
3
|
import type { ServerContext, ServerConfig } from '@owlmeans/server-context'
|
|
5
4
|
import { MongoClient } from 'mongodb'
|
|
@@ -33,16 +32,6 @@ export const makeMongoDbService = (alias: string = DEFAULT_ALIAS): MongoDbServic
|
|
|
33
32
|
return
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
if (service.layers == null) {
|
|
37
|
-
service.layers = [Layer.Global]
|
|
38
|
-
}
|
|
39
|
-
if (config.serviceSensitive && service.layers.includes(Layer.Service)) {
|
|
40
|
-
service.layers.push(Layer.Service)
|
|
41
|
-
}
|
|
42
|
-
if (config.entitySensitive && service.layers.includes(Layer.Entity)) {
|
|
43
|
-
service.layers.push(Layer.Entity)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
35
|
let [url, options] = prepareConfig(config)
|
|
47
36
|
|
|
48
37
|
let client = new MongoClient(url, options)
|
|
@@ -112,16 +101,6 @@ export const makeMongoDbService = (alias: string = DEFAULT_ALIAS): MongoDbServic
|
|
|
112
101
|
]
|
|
113
102
|
))
|
|
114
103
|
)
|
|
115
|
-
},
|
|
116
|
-
|
|
117
|
-
reinitializeContext: <T>(context: BasicContext<ServerConfig>) => {
|
|
118
|
-
const _service = makeMongoDbService(alias)
|
|
119
|
-
|
|
120
|
-
_service.ctx = context
|
|
121
|
-
|
|
122
|
-
_service.layers = service.layers
|
|
123
|
-
|
|
124
|
-
return _service as T
|
|
125
104
|
}
|
|
126
105
|
}, service => async () => {
|
|
127
106
|
const context = assertContext<Config, Context>(service.ctx as Context, location)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { afterAll, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { makeMongoResource } from '@owlmeans/mongo-resource'
|
|
3
|
+
import { UnknownRecordError, UnsupportedArgumentError } from '@owlmeans/resource'
|
|
4
|
+
|
|
5
|
+
import { gate, makeSuite } from './context.js'
|
|
6
|
+
import type { MongoResource, Note } from './context.js'
|
|
7
|
+
|
|
8
|
+
const noteSchema = {
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: {
|
|
11
|
+
title: { type: 'string' },
|
|
12
|
+
slug: { type: 'string', nullable: true }
|
|
13
|
+
},
|
|
14
|
+
required: ['title']
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const suite = makeSuite('crud')
|
|
18
|
+
const it = gate.skip ? test.skip : test
|
|
19
|
+
|
|
20
|
+
/** One alias — and so one collection — per test, so the sets never interfere. */
|
|
21
|
+
const notes = async (alias: string): Promise<MongoResource<Note>> => {
|
|
22
|
+
const resource = makeMongoResource<Note, MongoResource<Note>>(alias)
|
|
23
|
+
resource.schema = noteSchema as never
|
|
24
|
+
const { context } = await suite.boot({ resources: [resource] })
|
|
25
|
+
|
|
26
|
+
return context.resource<MongoResource<Note>>(alias)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('@owlmeans/mongo — the resource contract', () => {
|
|
30
|
+
if (gate.skip) {
|
|
31
|
+
test.skip(gate.reason ?? 'mongo gate closed', () => {})
|
|
32
|
+
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
afterAll(async () => {
|
|
37
|
+
await suite.teardown()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('reads one record by id and by criteria', async () => {
|
|
41
|
+
const res = await notes('crud-read')
|
|
42
|
+
const note = await res.create({ title: 'first', slug: 'one' })
|
|
43
|
+
expect(typeof note.id).toBe('string')
|
|
44
|
+
|
|
45
|
+
expect((await res.get(note.id!)).title).toBe('first')
|
|
46
|
+
/** Several fields at once, without listing and taking the first element. */
|
|
47
|
+
expect((await res.get({ title: 'first', slug: 'one' })).id).toBe(note.id!)
|
|
48
|
+
expect(await res.load({ slug: 'nope' })).toBeNull()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('answers an id that is not a mongo id rather than throwing a driver error', async () => {
|
|
52
|
+
const res = await notes('crud-badid')
|
|
53
|
+
|
|
54
|
+
expect(await res.load('not-an-object-id')).toBeNull()
|
|
55
|
+
await expect(res.get('not-an-object-id')).rejects.toThrow(UnknownRecordError)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('pages and sorts, and always reports the total', async () => {
|
|
59
|
+
const res = await notes('crud-page')
|
|
60
|
+
for (const title of ['c', 'a', 'd', 'b']) {
|
|
61
|
+
await res.create({ title })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const first = await res.list({}, { size: 2, sort: ['title'] })
|
|
65
|
+
expect(first.items.map(note => note.title)).toEqual(['a', 'b'])
|
|
66
|
+
expect(first.total).toBe(4)
|
|
67
|
+
expect(first.page).toBe(0)
|
|
68
|
+
expect(first.size).toBe(2)
|
|
69
|
+
|
|
70
|
+
const second = await res.list({}, { page: 1, size: 2, sort: ['title'] })
|
|
71
|
+
expect(second.items.map(note => note.title)).toEqual(['c', 'd'])
|
|
72
|
+
expect(second.total).toBe(4)
|
|
73
|
+
|
|
74
|
+
const newest = await res.list(undefined, { size: 1, sort: [{ field: 'title', order: 'desc' }] })
|
|
75
|
+
expect(newest.items.map(note => note.title)).toEqual(['d'])
|
|
76
|
+
|
|
77
|
+
/** `size: 0` is the explicit ask for the whole result set. */
|
|
78
|
+
const all = await res.list({}, { size: 0, sort: ['title'] })
|
|
79
|
+
expect(all.items).toHaveLength(4)
|
|
80
|
+
expect(all.total).toBe(4)
|
|
81
|
+
expect(all.size).toBeUndefined()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('counts without reading the records', async () => {
|
|
85
|
+
const res = await notes('crud-count')
|
|
86
|
+
await res.create({ title: 'x', slug: 'keep' })
|
|
87
|
+
await res.create({ title: 'y', slug: 'keep' })
|
|
88
|
+
await res.create({ title: 'z', slug: 'drop' })
|
|
89
|
+
|
|
90
|
+
expect(await res.count()).toBe(3)
|
|
91
|
+
expect(await res.count({ slug: 'keep' })).toBe(2)
|
|
92
|
+
expect(await res.count({ slug: 'absent' })).toBe(0)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('answers the shared operators the same way the other stores do', async () => {
|
|
96
|
+
const res = await notes('crud-ops')
|
|
97
|
+
await res.create({ title: 'Alpha', slug: 'a-1' })
|
|
98
|
+
await res.create({ title: 'beta', slug: 'b-2' })
|
|
99
|
+
await res.create({ title: 'gamma' })
|
|
100
|
+
|
|
101
|
+
expect((await res.list({ title: { $ilike: 'al%' } })).items.map(note => note.title))
|
|
102
|
+
.toEqual(['Alpha'])
|
|
103
|
+
expect((await res.list({ slug: { $startsWith: 'b-' } })).items.map(note => note.title))
|
|
104
|
+
.toEqual(['beta'])
|
|
105
|
+
expect((await res.list({ slug: { $null: true } })).items.map(note => note.title))
|
|
106
|
+
.toEqual(['gamma'])
|
|
107
|
+
expect((await res.list({ title: ['Alpha', 'beta'] })).total).toBe(2)
|
|
108
|
+
expect((await res.list({ $or: [{ title: 'Alpha' }, { slug: 'b-2' }] })).total).toBe(2)
|
|
109
|
+
expect((await res.list({ $not: { title: 'gamma' } })).total).toBe(2)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('purges by criteria and refuses to empty the collection', async () => {
|
|
113
|
+
const res = await notes('crud-purge')
|
|
114
|
+
await res.create({ title: 'x', slug: 'drop' })
|
|
115
|
+
await res.create({ title: 'y', slug: 'drop' })
|
|
116
|
+
await res.create({ title: 'z', slug: 'keep' })
|
|
117
|
+
|
|
118
|
+
/** An empty criteria object would delete everything — one call replacing a page loop. */
|
|
119
|
+
await expect(res.purge({})).rejects.toThrow(UnsupportedArgumentError)
|
|
120
|
+
|
|
121
|
+
expect(await res.purge({ slug: 'drop' })).toBe(2)
|
|
122
|
+
expect(await res.count()).toBe(1)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('takes a record out and hands it back, where delete tolerates absence', async () => {
|
|
126
|
+
const res = await notes('crud-take')
|
|
127
|
+
const note = await res.create({ title: 'once' })
|
|
128
|
+
|
|
129
|
+
expect((await res.take(note.id!)).title).toBe('once')
|
|
130
|
+
await expect(res.take(note.id!)).rejects.toThrow(UnknownRecordError)
|
|
131
|
+
expect(await res.delete(note.id!)).toBeNull()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('creates without an id, replaces with one', async () => {
|
|
135
|
+
const res = await notes('crud-save')
|
|
136
|
+
const created = await res.save({ title: 'draft', slug: 'kept' })
|
|
137
|
+
expect(typeof created.id).toBe('string')
|
|
138
|
+
|
|
139
|
+
const replaced = await res.save({ id: created.id, title: 'final' })
|
|
140
|
+
expect(replaced.id).toBe(created.id!)
|
|
141
|
+
expect(replaced.title).toBe('final')
|
|
142
|
+
/** A replace, not a patch — the field the new record omits is gone from the document. */
|
|
143
|
+
expect(replaced.slug).toBeUndefined()
|
|
144
|
+
expect(await res.count()).toBe(1)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('refuses an expiry it cannot honour', async () => {
|
|
148
|
+
const res = await notes('crud-ttl')
|
|
149
|
+
|
|
150
|
+
await expect(res.create({ title: 'ephemeral' }, { ttl: 60 })).rejects
|
|
151
|
+
.toThrow(UnsupportedArgumentError)
|
|
152
|
+
})
|
|
153
|
+
})
|
package/tests/references.spec.ts
CHANGED
|
@@ -108,7 +108,7 @@ describe('@owlmeans/mongo — ObjectId references', () => {
|
|
|
108
108
|
expect(listed.items.map(i => i.title)).toEqual(['a'])
|
|
109
109
|
expect(listed.items[0].ownerId).toBe(owner.id!)
|
|
110
110
|
|
|
111
|
-
const viaIn = await items.list({ ownerId: { $in: [owner.id!, other.id!] } }
|
|
111
|
+
const viaIn = await items.list({ ownerId: { $in: [owner.id!, other.id!] } })
|
|
112
112
|
expect(viaIn.items).toHaveLength(2)
|
|
113
113
|
|
|
114
114
|
/** `id` criteria address `_id` — documents never store an `id` field. */
|
|
@@ -119,7 +119,8 @@ describe('@owlmeans/mongo — ObjectId references', () => {
|
|
|
119
119
|
const none = await items.list({ ownerId: 'ext:not-an-id' })
|
|
120
120
|
expect(none.items).toHaveLength(0)
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
/** A criteria read addresses the reference field directly — no list to take a first from. */
|
|
123
|
+
const loaded = await items.load({ ownerId: owner.id! })
|
|
123
124
|
expect(loaded?.title).toBe('a')
|
|
124
125
|
})
|
|
125
126
|
|
|
@@ -150,7 +151,7 @@ describe('@owlmeans/mongo — ObjectId references', () => {
|
|
|
150
151
|
expect((stored?.ownerIds as unknown[]).every(v => v instanceof ObjectId)).toBe(true)
|
|
151
152
|
|
|
152
153
|
/** Multikey criteria hit the converted elements. */
|
|
153
|
-
const listed = await items.list({ ownerIds: second.id! })
|
|
154
|
+
const listed = await items.list({ ownerIds: { $contains: [second.id!] } })
|
|
154
155
|
expect(listed.items.map(i => i.title)).toEqual(['multi'])
|
|
155
156
|
})
|
|
156
157
|
|
|
@@ -281,15 +282,16 @@ describe('@owlmeans/mongo — ObjectId references', () => {
|
|
|
281
282
|
expect(updated.ownerId).toBe(next.id!)
|
|
282
283
|
|
|
283
284
|
/** Addressing the record BY its reference field converts the lookup value too. */
|
|
284
|
-
const byRef = await items.update({ ...
|
|
285
|
+
const byRef = await items.update({ ...await items.get({ ownerId: next.id! }), title: 'by-ref' })
|
|
285
286
|
expect(byRef.title).toBe('by-ref')
|
|
286
287
|
|
|
287
288
|
const stored = await raw(async client =>
|
|
288
289
|
await client.db(suite.database).collection('ref-upd').findOne({ _id: new ObjectId(item.id!) }))
|
|
289
290
|
expect(stored?.ownerId).toBeInstanceOf(ObjectId)
|
|
290
291
|
|
|
291
|
-
|
|
292
|
-
|
|
292
|
+
/** And the delete-and-return reached through the same converted lookup. */
|
|
293
|
+
const gone = await items.take((await items.get({ ownerId: next.id! })).id!)
|
|
294
|
+
expect(gone.title).toBe('by-ref')
|
|
293
295
|
})
|
|
294
296
|
|
|
295
297
|
it('survives redeclaration and reboot without conflicts', async () => {
|