@owlmeans/mongo 0.1.18-rc.1 → 0.1.18-rc.11
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 +11 -7
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/mongo/SKILL.md +11 -11
- package/build/service.d.ts.map +1 -1
- package/build/service.js +1 -16
- package/build/service.js.map +1 -1
- package/build/utils/cluster.d.ts.map +1 -1
- package/build/utils/config.d.ts.map +1 -1
- package/build/utils/config.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,7 +6,7 @@ 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
|
|
@@ -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
|
|
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.11",
|
|
5
|
+
"generatedAt": "2026-09-01T16:28:56.992Z",
|
|
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.11"` in `dependencies` (peer `mongodb`)
|
|
12
12
|
|
|
13
13
|
## Key Exports
|
|
14
14
|
|
|
@@ -31,9 +31,8 @@ cfg.dbs = [{
|
|
|
31
31
|
host: '127.0.0.1', // or string[] for a cluster — triggers replica set setup
|
|
32
32
|
port: 27017,
|
|
33
33
|
user: 'admin', secret: '...',
|
|
34
|
-
schema: 'my-app', // the DATABASE name
|
|
34
|
+
schema: 'my-app', // the DATABASE name
|
|
35
35
|
encryptionKey: '...', // enables lock()/unlock() field encryption
|
|
36
|
-
entitySensitive: true, // per-Entity-layer databases
|
|
37
36
|
}]
|
|
38
37
|
```
|
|
39
38
|
|
|
@@ -41,9 +40,9 @@ cfg.dbs = [{
|
|
|
41
40
|
replica-set bootstrap (`setUpCluster`) first.
|
|
42
41
|
- `lock`/`unlock` encrypt/decrypt record fields with `encryptionKey` via
|
|
43
42
|
`@owlmeans/basic-keys` — the backend behind `MongoResource.lock()`.
|
|
44
|
-
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
- The database name is `dbName()` — `config.schema ?? config.alias ?? service.alias`, taken as
|
|
44
|
+
given. One database per config entry, each carrying its own data **and its own migration
|
|
45
|
+
ledger**; a second database is a second `cfg.dbs` entry.
|
|
47
46
|
|
|
48
47
|
## Tests
|
|
49
48
|
|
|
@@ -59,10 +58,11 @@ port-forward to the cluster mongo.
|
|
|
59
58
|
- `@owlmeans/basic-keys` — field encryption
|
|
60
59
|
- peer `mongodb` (currently `^7.5.0`)
|
|
61
60
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
the
|
|
65
|
-
|
|
61
|
+
This package requires **Bun 1.4.0 or newer**. On anything older, `bson >= 7.3.0` throws
|
|
62
|
+
`ERR_NOT_IMPLEMENTED` (`v8.startupSnapshot.isBuildingSnapshot`) the moment `mongodb` is imported and
|
|
63
|
+
the server never starts; 1.4.0 implements the call, so `bson` is left to resolve freely inside the
|
|
64
|
+
driver's declared `^7.2.0` range. An app that must run an older Bun pins `"bson": "7.2.0"` in its
|
|
65
|
+
own root `overrides` instead. Details in the `bun` skill.
|
|
66
66
|
|
|
67
67
|
## Related
|
|
68
68
|
|
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,GAAI,QAAO,MAAsB,KAAG,cAuGlE,CAAA;AAED,eAAO,MAAM,WAAW,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,EAC7E,SAAS,CAAC,EAAE,QAAO,MAAsB,KACxC,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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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cluster.d.ts","sourceRoot":"","sources":["../../src/utils/cluster.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAE1C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAMlD,eAAO,MAAM,YAAY,
|
|
1
|
+
{"version":3,"file":"cluster.d.ts","sourceRoot":"","sources":["../../src/utils/cluster.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAE1C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAMlD,eAAO,MAAM,YAAY,GAAU,QAAQ,WAAW,EAAE,QAAQ,QAAQ,KAAG,OAAO,CAAC,OAAO,CA6DzF,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/utils/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;AAEjD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAIlD,eAAO,MAAM,aAAa,
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/utils/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;AAEjD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAIlD,eAAO,MAAM,aAAa,GAAI,QAAQ,QAAQ,EAAE,SAAQ,OAAc,KAAG,CAAC,MAAM,EAAE,kBAAkB,CAuBnG,CAAA;AAED,eAAO,MAAM,IAAI,GAAI,MAAM,MAAM,EAAE,QAAQ,QAAQ,KAAG,MACC,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/utils/config.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE1C,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,MAAgB,EAAE,
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/utils/config.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE1C,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,MAAgB,EAAE,SAAkB,IAAI,EAAgC,EAAE;IACtG,IAAI,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAA;IACpE,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IAEtE,IAAI,MAAM,CAAC,IAAI,IAAI,IAAI,EAAE,CAAC;QACxB,IAAI,GAAG,aAAa,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAA;IACrG,CAAC;IAED,MAAM,IAAI,GAA0B,MAAM,CAAC,IAAI,CAAA;IAE/C,2EAA2E;IAC3E,6EAA6E;IAC7E,6EAA6E;IAC7E,0EAA0E;IAC1E,gFAAgF;IAChF,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,IAAI,GAAG,GAAG,IAAI,gBAAgB,IAAI,EAAE,UAAU,IAAI,WAAW,EAAE,CAAA;IACjE,CAAC;IAED,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC;YAClC,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE;YACxD,gBAAgB,EAAE,MAAM;SACzB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;AACT,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,MAAgB,EAAU,EAAE,CAC7D,MAAM,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,MAAM,CAAC,IAAI,EAAE,CAAA"}
|
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.11",
|
|
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.7",
|
|
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.10",
|
|
35
|
+
"@owlmeans/context": "^0.1.18-rc.7",
|
|
36
|
+
"@owlmeans/mongo-resource": "^0.1.18-rc.10",
|
|
37
|
+
"@owlmeans/resource": "^0.1.18-rc.8",
|
|
38
|
+
"@owlmeans/server-context": "^0.1.18-rc.10",
|
|
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 () => {
|