@owlmeans/mongo-resource 0.1.18-rc.8 → 0.1.18
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 +232 -71
- package/agent-meta/manifest.json +3 -3
- package/agent-meta/skills/mongo-resource/SKILL.md +89 -44
- package/build/consts.d.ts +8 -4
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +8 -4
- package/build/consts.js.map +1 -1
- package/build/declarations/types.d.ts +12 -0
- package/build/declarations/types.d.ts.map +1 -0
- package/build/declarations/types.js +2 -0
- package/build/declarations/types.js.map +1 -0
- package/build/declarations.d.ts +4 -9
- package/build/declarations.d.ts.map +1 -1
- package/build/declarations.js +28 -21
- package/build/declarations.js.map +1 -1
- package/build/index.d.ts +4 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/resource.d.ts +2 -2
- package/build/resource.d.ts.map +1 -1
- package/build/resource.js +100 -130
- package/build/resource.js.map +1 -1
- package/build/types.d.ts +11 -5
- package/build/types.d.ts.map +1 -1
- package/build/types.local.d.ts +4 -0
- package/build/types.local.d.ts.map +1 -0
- package/build/types.local.js +2 -0
- package/build/types.local.js.map +1 -0
- package/build/utils/consts.local.d.ts +25 -0
- package/build/utils/consts.local.d.ts.map +1 -0
- package/build/utils/consts.local.js +31 -0
- package/build/utils/consts.local.js.map +1 -0
- package/build/utils/criteria/types.d.ts +27 -0
- package/build/utils/criteria/types.d.ts.map +1 -0
- package/build/utils/criteria/types.js +2 -0
- package/build/utils/criteria/types.js.map +1 -0
- package/build/utils/criteria.d.ts +9 -0
- package/build/utils/criteria.d.ts.map +1 -0
- package/build/utils/criteria.js +204 -0
- package/build/utils/criteria.js.map +1 -0
- package/build/utils/index.d.ts +1 -0
- package/build/utils/index.d.ts.map +1 -1
- package/build/utils/index.js +1 -0
- package/build/utils/index.js.map +1 -1
- package/build/utils/indexes/types.d.ts +19 -0
- package/build/utils/indexes/types.d.ts.map +1 -0
- package/build/utils/indexes/types.js +2 -0
- package/build/utils/indexes/types.js.map +1 -0
- package/build/utils/indexes.d.ts +3 -4
- package/build/utils/indexes.d.ts.map +1 -1
- package/build/utils/indexes.js +44 -28
- package/build/utils/indexes.js.map +1 -1
- package/build/utils/life-cycle/types.d.ts +36 -0
- package/build/utils/life-cycle/types.d.ts.map +1 -0
- package/build/utils/life-cycle/types.js +2 -0
- package/build/utils/life-cycle/types.js.map +1 -0
- package/build/utils/life-cycle.d.ts +3 -30
- package/build/utils/life-cycle.d.ts.map +1 -1
- package/build/utils/life-cycle.js +96 -112
- package/build/utils/life-cycle.js.map +1 -1
- package/build/utils/migrations.d.ts.map +1 -1
- package/build/utils/migrations.js +1 -2
- package/build/utils/migrations.js.map +1 -1
- package/build/utils/name.d.ts.map +1 -1
- package/build/utils/name.js +1 -5
- package/build/utils/name.js.map +1 -1
- package/build/utils/refs/types.d.ts +78 -0
- package/build/utils/refs/types.d.ts.map +1 -0
- package/build/utils/refs/types.js +2 -0
- package/build/utils/refs/types.js.map +1 -0
- package/build/utils/refs.d.ts +13 -67
- package/build/utils/refs.d.ts.map +1 -1
- package/build/utils/refs.js +115 -158
- package/build/utils/refs.js.map +1 -1
- package/build/utils/schema/types.d.ts +16 -0
- package/build/utils/schema/types.d.ts.map +1 -0
- package/build/utils/schema/types.js +2 -0
- package/build/utils/schema/types.js.map +1 -0
- package/build/utils/schema.d.ts +3 -11
- package/build/utils/schema.d.ts.map +1 -1
- package/build/utils/schema.js +86 -88
- package/build/utils/schema.js.map +1 -1
- package/build/utils/types.local.d.ts +13 -0
- package/build/utils/types.local.d.ts.map +1 -0
- package/build/utils/types.local.js +2 -0
- package/build/utils/types.local.js.map +1 -0
- package/package.json +10 -9
- package/src/consts.ts +8 -4
- package/src/declarations/types.ts +12 -0
- package/src/declarations.ts +32 -28
- package/src/index.ts +4 -0
- package/src/resource.ts +121 -163
- package/src/types.local.ts +5 -0
- package/src/types.ts +13 -7
- package/src/utils/consts.local.ts +35 -0
- package/src/utils/criteria/types.ts +28 -0
- package/src/utils/criteria.ts +235 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/indexes/types.ts +20 -0
- package/src/utils/indexes.ts +48 -28
- package/src/utils/life-cycle/types.ts +39 -0
- package/src/utils/life-cycle.ts +120 -134
- package/src/utils/migrations.ts +3 -17
- package/src/utils/name.ts +1 -6
- package/src/utils/refs/types.ts +79 -0
- package/src/utils/refs.ts +147 -189
- package/src/utils/schema/types.ts +17 -0
- package/src/utils/schema.ts +92 -91
- package/src/utils/types.local.ts +13 -0
- package/tests/context.ts +3 -3
- package/tests/criteria.spec.ts +116 -0
- package/tests/crud.spec.ts +2 -2
- package/tests/indexes.spec.ts +42 -0
- package/tests/refs.spec.ts +5 -3
- package/build/.gitkeep +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/mongo-resource",
|
|
3
|
-
"version": "0.1.18
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -26,16 +26,17 @@
|
|
|
26
26
|
"mongodb": "*"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@owlmeans/context": "^0.1.18
|
|
30
|
-
"@owlmeans/
|
|
31
|
-
"@owlmeans/
|
|
29
|
+
"@owlmeans/context": "^0.1.18",
|
|
30
|
+
"@owlmeans/log": "^0.1.18",
|
|
31
|
+
"@owlmeans/resource": "^0.1.18",
|
|
32
|
+
"@owlmeans/server-context": "^0.1.18"
|
|
32
33
|
},
|
|
33
34
|
"devDependencies": {
|
|
34
|
-
"@owlmeans/dep-config": "
|
|
35
|
-
"@owlmeans/test-integration": "^0.1.18
|
|
36
|
-
"@types/bun": "^1.4.
|
|
37
|
-
"@types/node": "^26.
|
|
38
|
-
"mongodb": "^7.
|
|
35
|
+
"@owlmeans/dep-config": "^0.1.18",
|
|
36
|
+
"@owlmeans/test-integration": "^0.1.18",
|
|
37
|
+
"@types/bun": "^1.4.2",
|
|
38
|
+
"@types/node": "^26.6.4",
|
|
39
|
+
"mongodb": "^7.7.0",
|
|
39
40
|
"nodemon": "^3.1.14",
|
|
40
41
|
"typescript": "^7.0.2"
|
|
41
42
|
},
|
package/src/consts.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
|
|
2
2
|
export const DEFAULT_DB_ALIAS = 'mongo'
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Page size a `list` call gets when it asks for none. Mongo cannot afford an unbounded read
|
|
6
|
+
* of a collection by omission, so paging is the default here — `list(where, { size: 0 })`
|
|
7
|
+
* asks for the whole result set, explicitly and greppably.
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_PAGE_SIZE = 100
|
|
5
10
|
|
|
6
11
|
/**
|
|
7
12
|
* Collection that records which code-registered migrations have already been applied.
|
|
8
13
|
*
|
|
9
|
-
* One ledger per database, which is the right boundary:
|
|
10
|
-
*
|
|
11
|
-
* data and dropping the database drops the ledger with it.
|
|
14
|
+
* One ledger per database, which is the right boundary: it sits beside the data its
|
|
15
|
+
* migrations changed, so dropping the database drops the ledger with it.
|
|
12
16
|
*/
|
|
13
17
|
export const DEF_MIGRATIONS_COLLECTION = '_owlmeans_migrations'
|
|
14
18
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { MongoDeclaration } from '../types.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The per-alias declaration store every mongo resource maker reads and extends — migrations and
|
|
5
|
+
* declared references, keyed by resource alias.
|
|
6
|
+
*/
|
|
7
|
+
export interface MongoDeclarationHelper {
|
|
8
|
+
/** The declaration of an alias, created empty on first use. */
|
|
9
|
+
getDeclaration: (alias: string) => MongoDeclaration
|
|
10
|
+
/** Testing seam — drops every declaration so a spec can redeclare a resource from scratch. */
|
|
11
|
+
resetDeclarations: (alias?: string) => void
|
|
12
|
+
}
|
package/src/declarations.ts
CHANGED
|
@@ -1,42 +1,46 @@
|
|
|
1
1
|
import { createMigrationRegistry } from '@owlmeans/resource'
|
|
2
|
-
import type { MigrationRegistry } from '@owlmeans/resource'
|
|
3
2
|
|
|
4
|
-
import type {
|
|
5
|
-
|
|
6
|
-
export interface MongoDeclaration {
|
|
7
|
-
migrations: MigrationRegistry<MongoTx>
|
|
8
|
-
/** Declared ObjectId references, keyed by field. Registered via `resource.reference()`. */
|
|
9
|
-
references: Map<string, MongoReference>
|
|
10
|
-
}
|
|
3
|
+
import type { MongoDeclarationHelper } from './declarations/types.js'
|
|
4
|
+
import type { MongoTx, MongoDeclaration } from './types.js'
|
|
11
5
|
|
|
12
6
|
/**
|
|
13
7
|
* Per-alias migration store, held at module scope rather than on the resource object.
|
|
14
8
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
9
|
+
* A maker may run more than once for the same alias — a custom maker wrapping the built-in
|
|
10
|
+
* one, a maker called again by an app or a spec. Keying the declarations by alias makes
|
|
11
|
+
* that a no-op: every run reads and extends the same registry, so nothing a caller declared
|
|
12
|
+
* by chaining onto an earlier resource object is lost. Losing a migration is silent — the
|
|
13
|
+
* data transformation simply never runs — which is why the store cannot live on the object.
|
|
14
|
+
*
|
|
15
|
+
* Process-wide for the same reason: it stays outside the factory, so every helper built from it
|
|
16
|
+
* reads the one store.
|
|
21
17
|
*/
|
|
22
18
|
const declarations: Map<string, MongoDeclaration> = new Map()
|
|
23
19
|
|
|
24
|
-
export const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
20
|
+
export const createMongoDeclarationHelper = (): MongoDeclarationHelper => {
|
|
21
|
+
const getDeclaration = (alias: string): MongoDeclaration => {
|
|
22
|
+
let declaration = declarations.get(alias)
|
|
23
|
+
if (declaration == null) {
|
|
24
|
+
declaration = { migrations: createMigrationRegistry<MongoTx>(), references: new Map() }
|
|
25
|
+
declarations.set(alias, declaration)
|
|
26
|
+
}
|
|
30
27
|
|
|
31
|
-
|
|
32
|
-
}
|
|
28
|
+
return declaration
|
|
29
|
+
}
|
|
33
30
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
declarations.clear()
|
|
31
|
+
const resetDeclarations = (alias?: string): void => {
|
|
32
|
+
if (alias == null) {
|
|
33
|
+
declarations.clear()
|
|
38
34
|
|
|
39
|
-
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
declarations.delete(alias)
|
|
40
38
|
}
|
|
41
|
-
|
|
39
|
+
|
|
40
|
+
return { getDeclaration, resetDeclarations }
|
|
42
41
|
}
|
|
42
|
+
|
|
43
|
+
export const mongoDeclarationHelper = createMongoDeclarationHelper()
|
|
44
|
+
|
|
45
|
+
/** @deprecated compat:factory-refactor — use `mongoDeclarationHelper.resetDeclarations(…)` */
|
|
46
|
+
export const resetDeclarations = (alias?: string): void => mongoDeclarationHelper.resetDeclarations(alias)
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
export type * from './types.js'
|
|
3
3
|
export * from './consts.js'
|
|
4
4
|
export * from './declarations.js'
|
|
5
|
+
export type * from './declarations/types.js'
|
|
5
6
|
export * from './utils/migrations.js'
|
|
6
7
|
export * from './utils/refs.js'
|
|
8
|
+
export type * from './utils/refs/types.js'
|
|
9
|
+
export * from './utils/criteria.js'
|
|
10
|
+
export type * from './utils/criteria/types.js'
|
|
7
11
|
export * from './resource.js'
|
|
8
12
|
export * from './helper.js'
|
package/src/resource.ts
CHANGED
|
@@ -1,120 +1,144 @@
|
|
|
1
1
|
import { appendContextual, assertContext } from '@owlmeans/context'
|
|
2
|
-
import type { BasicContext, Contextual } from '@owlmeans/context'
|
|
3
2
|
import { DEFAULT_DB_ALIAS, DEFAULT_PAGE_SIZE } from './consts.js'
|
|
4
|
-
import { MigrationStage } from '@owlmeans/resource'
|
|
5
|
-
import type { ListCriteria, ResourceMaker, ResourceRecord } from '@owlmeans/resource'
|
|
6
|
-
import type { ServerConfig, ServerContext } from '@owlmeans/server-context'
|
|
3
|
+
import { MigrationStage, type Criteria, type FirstOptions, type ListOptions, type ListResult, type ResourceRecord, type WriteOptions, MisshapedRecord, RecordExists, UnknownRecordError, UnsupportedArgumentError, RecordUpdateFailed } from '@owlmeans/resource'
|
|
7
4
|
import type { MongoDbService, MongoReference, MongoRefOptions, MongoResource, MongoTx } from './types.js'
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { ObjectId } from 'mongodb'
|
|
11
|
-
import { MisshapedRecord, RecordExists, UnknownRecordError, UnsupportedArgumentError, RecordUpdateFailed, prepareListOptions } from '@owlmeans/resource'
|
|
5
|
+
import { makeMongoLifeCycleUtils } from './utils/life-cycle.js'
|
|
6
|
+
import { mongoDeclarationHelper } from './declarations.js'
|
|
7
|
+
import { ObjectId, type CreateIndexesOptions, type Document, type IndexSpecification } from 'mongodb'
|
|
12
8
|
import type { JSONSchemaType } from 'ajv'
|
|
13
9
|
import { getSchemaSecureFeilds } from './helper.js'
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
} from './utils/refs.js'
|
|
10
|
+
import { mongoCriteriaHelper } from './utils/criteria.js'
|
|
11
|
+
import { makeRefMigration, mongoRefHelper } from './utils/refs.js'
|
|
12
|
+
import type { Config, Context } from './types.local.js'
|
|
18
13
|
|
|
19
|
-
type Config = ServerConfig
|
|
20
|
-
type Context<C extends Config = Config> = ServerContext<C>
|
|
21
14
|
|
|
22
15
|
export const makeMongoResource = <
|
|
23
16
|
R extends ResourceRecord, T extends MongoResource<R> = MongoResource<R>
|
|
24
17
|
>(
|
|
25
18
|
alias: string, dbAlias: string = DEFAULT_DB_ALIAS, serviceAlias: string = DEFAULT_DB_ALIAS,
|
|
26
|
-
|
|
19
|
+
collectionName?: string
|
|
27
20
|
): T => {
|
|
28
21
|
const location = `mongo-resource:${alias}`
|
|
29
22
|
|
|
30
23
|
/**
|
|
31
24
|
* Live view — references may be declared after the resource is built, and the
|
|
32
|
-
* declarations
|
|
25
|
+
* declarations live at module scope keyed by alias, so a second maker run for the
|
|
26
|
+
* same alias sees everything already declared for it.
|
|
33
27
|
*/
|
|
34
|
-
const refs = (): Map<string, MongoReference> => getDeclaration(alias).references
|
|
28
|
+
const refs = (): Map<string, MongoReference> => mongoDeclarationHelper.getDeclaration(alias).references
|
|
35
29
|
|
|
36
30
|
const demarshal = <Type extends ResourceRecord>(record: Type & { _id?: unknown }): Type => {
|
|
37
31
|
record.id = record._id instanceof ObjectId ? record._id.toString() : record._id as string
|
|
38
32
|
delete record._id
|
|
39
|
-
return demarshalRefs(record, refs())
|
|
33
|
+
return mongoRefHelper.demarshalRefs(record, refs())
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A read addressed either way. An id is matched tolerantly: a string that is not a mongo id
|
|
38
|
+
* finds nothing, where handing it to `ObjectId` would raise a driver error at a call site
|
|
39
|
+
* that only asked whether the record exists.
|
|
40
|
+
*/
|
|
41
|
+
const filterOf = (idOrWhere: string | Criteria<R>): Document =>
|
|
42
|
+
typeof idOrWhere === 'string'
|
|
43
|
+
? mongoRefHelper.identityCriteria('id', idOrWhere, refs())
|
|
44
|
+
: mongoCriteriaHelper.criteriaToFilter(idOrWhere, refs())
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The single implementation both overloads of `load` and `get` stand on — an id and a
|
|
48
|
+
* criteria object differ only in how the filter is built.
|
|
49
|
+
*/
|
|
50
|
+
const loadOne = async (
|
|
51
|
+
idOrWhere: string | Criteria<R>, opts?: FirstOptions<R>
|
|
52
|
+
): Promise<R | null> => {
|
|
53
|
+
const sort = mongoCriteriaHelper.sortToMongo(opts?.sort)
|
|
54
|
+
const record = await resource.collection.findOne(
|
|
55
|
+
filterOf(idOrWhere), sort != null ? { sort } : {}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return record != null ? demarshal(record as unknown as R) : null
|
|
40
59
|
}
|
|
41
60
|
|
|
42
61
|
const resource: T = appendContextual<T>(alias, {
|
|
43
|
-
get: async (
|
|
44
|
-
const record = await
|
|
62
|
+
get: (async (idOrWhere: string | Criteria<R>, opts?: FirstOptions<R>): Promise<R> => {
|
|
63
|
+
const record = await loadOne(idOrWhere, opts)
|
|
45
64
|
if (record == null) {
|
|
46
|
-
throw new UnknownRecordError(
|
|
65
|
+
throw new UnknownRecordError(
|
|
66
|
+
typeof idOrWhere === 'string' ? idOrWhere : JSON.stringify(idOrWhere)
|
|
67
|
+
)
|
|
47
68
|
}
|
|
48
69
|
|
|
49
70
|
return record
|
|
50
|
-
},
|
|
71
|
+
}) as T['get'],
|
|
72
|
+
|
|
73
|
+
load: loadOne as T['load'],
|
|
51
74
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
75
|
+
list: async (where?: Criteria<R>, opts?: ListOptions<R>): Promise<ListResult<R>> => {
|
|
76
|
+
const filter = mongoCriteriaHelper.criteriaToFilter(where, refs())
|
|
77
|
+
const total = await resource.collection.countDocuments(filter)
|
|
78
|
+
|
|
79
|
+
/** `size: 0` is the explicit ask for everything; an omitted size pages by default. */
|
|
80
|
+
const size = opts?.size ?? DEFAULT_PAGE_SIZE
|
|
81
|
+
const page = opts?.page ?? 0
|
|
82
|
+
if (size < 1 && page !== 0) {
|
|
83
|
+
/** Nothing to page through — answering page 0 instead would be a silent wrong answer. */
|
|
84
|
+
throw new UnsupportedArgumentError('page-without-size')
|
|
56
85
|
}
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
|
|
86
|
+
|
|
87
|
+
if (total === 0) {
|
|
88
|
+
return size > 0 ? { items: [], total, page, size } : { items: [], total }
|
|
60
89
|
}
|
|
61
90
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
91
|
+
let cursor = resource.collection.find(filter)
|
|
92
|
+
const sort = mongoCriteriaHelper.sortToMongo(opts?.sort)
|
|
93
|
+
if (sort != null) {
|
|
94
|
+
cursor = cursor.sort(sort)
|
|
95
|
+
}
|
|
96
|
+
if (size > 0) {
|
|
97
|
+
cursor = cursor.skip(page * size).limit(size)
|
|
65
98
|
}
|
|
66
99
|
|
|
67
|
-
|
|
100
|
+
const items = (await cursor.toArray()).map(item => demarshal({ ...item } as unknown as R))
|
|
101
|
+
|
|
102
|
+
return size > 0 ? { items, total, page, size } : { items, total }
|
|
68
103
|
},
|
|
69
104
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (typeof opts === 'string') {
|
|
73
|
-
field = opts
|
|
74
|
-
} else if (typeof opts === 'object') {
|
|
75
|
-
field = opts.field ?? field
|
|
76
|
-
}
|
|
105
|
+
count: async (where?: Criteria<R>): Promise<number> =>
|
|
106
|
+
await resource.collection.countDocuments(mongoCriteriaHelper.criteriaToFilter(where, refs())),
|
|
77
107
|
|
|
78
|
-
|
|
79
|
-
|
|
108
|
+
update: async (record: Partial<R>, opts?: WriteOptions): Promise<R> => {
|
|
109
|
+
if (opts?.ttl != null) {
|
|
110
|
+
throw new UnsupportedArgumentError('ttl')
|
|
80
111
|
}
|
|
81
|
-
|
|
82
|
-
const id = field == '_id' ? record.id : record[field as keyof typeof record] as string
|
|
112
|
+
const id = record.id
|
|
83
113
|
if (id == null) {
|
|
84
114
|
throw new MisshapedRecord('id')
|
|
85
115
|
}
|
|
86
116
|
|
|
87
|
-
|
|
117
|
+
/** Absence is an error, not a silent no-op — `replaceOne` would just match nothing. */
|
|
118
|
+
const original = await resource.get(id)
|
|
88
119
|
|
|
89
|
-
const replace = { ...record
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
120
|
+
const replace: Document = { ...record }
|
|
121
|
+
/**
|
|
122
|
+
* Documents never store `id`, and a replacement that omits `_id` keeps the one the
|
|
123
|
+
* document already carries — so the whole record is replaced without touching its key.
|
|
124
|
+
*/
|
|
125
|
+
delete replace.id
|
|
93
126
|
|
|
94
127
|
const result = await resource.collection.replaceOne(
|
|
95
|
-
identityCriteria(
|
|
128
|
+
mongoRefHelper.identityCriteria('id', original.id as string, refs()),
|
|
96
129
|
_prepareValues(replace, resource.schema as JSONSchemaType<any>, refs())
|
|
97
130
|
)
|
|
98
131
|
if (!result.acknowledged) {
|
|
99
|
-
throw new RecordUpdateFailed(
|
|
132
|
+
throw new RecordUpdateFailed(`id:${id}`)
|
|
100
133
|
}
|
|
101
134
|
|
|
102
|
-
return resource.get(id
|
|
135
|
+
return await resource.get(original.id as string)
|
|
103
136
|
},
|
|
104
137
|
|
|
105
|
-
save: async (record
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
) || (
|
|
110
|
-
typeof opts === 'object' && ("field" in opts)
|
|
111
|
-
&& record[opts.field as keyof typeof record] != null
|
|
112
|
-
)) {
|
|
113
|
-
return resource.update(record, opts)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return resource.create(record, typeof opts !== 'string' ? opts : undefined)
|
|
117
|
-
},
|
|
138
|
+
save: async (record: Partial<R>, opts?: WriteOptions): Promise<R> =>
|
|
139
|
+
record.id != null
|
|
140
|
+
? await resource.update(record, opts)
|
|
141
|
+
: await resource.create(record, opts),
|
|
118
142
|
|
|
119
143
|
getDefaults: () => {
|
|
120
144
|
const schema: JSONSchemaType<unknown> | undefined = resource.schema as JSONSchemaType<unknown>
|
|
@@ -130,14 +154,14 @@ export const makeMongoResource = <
|
|
|
130
154
|
}, {})
|
|
131
155
|
},
|
|
132
156
|
|
|
133
|
-
create: async (record
|
|
157
|
+
create: async (record: Partial<R>, opts?: WriteOptions): Promise<R> => {
|
|
134
158
|
if ("id" in record && record.id == null) {
|
|
135
159
|
delete record.id
|
|
136
160
|
}
|
|
137
161
|
if (record.id != null) {
|
|
138
162
|
throw new RecordExists('id-present')
|
|
139
163
|
}
|
|
140
|
-
if (opts
|
|
164
|
+
if (opts?.ttl != null) {
|
|
141
165
|
throw new UnsupportedArgumentError('ttl')
|
|
142
166
|
}
|
|
143
167
|
const result = await resource.collection.insertOne({
|
|
@@ -149,89 +173,33 @@ export const makeMongoResource = <
|
|
|
149
173
|
throw new RecordUpdateFailed(`creation`)
|
|
150
174
|
}
|
|
151
175
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
return resource.get(id.toString())
|
|
176
|
+
return await resource.get(result.insertedId.toString())
|
|
155
177
|
},
|
|
156
178
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (id.id == null) {
|
|
161
|
-
throw new MisshapedRecord('id')
|
|
162
|
-
}
|
|
163
|
-
record = await resource.load(id.id)
|
|
164
|
-
} else {
|
|
165
|
-
record = await resource.load(id, opts)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (record == null) {
|
|
169
|
-
return null
|
|
170
|
-
}
|
|
179
|
+
/** Atomic: the record is handed back by the very operation that removed it. */
|
|
180
|
+
delete: async (id: string): Promise<R | null> => {
|
|
181
|
+
const record = await resource.collection.findOneAndDelete(mongoRefHelper.identityCriteria('id', id, refs()))
|
|
171
182
|
|
|
172
|
-
|
|
173
|
-
if (typeof opts === 'string') {
|
|
174
|
-
field = opts
|
|
175
|
-
opts = undefined
|
|
176
|
-
} else if (typeof opts === 'object') {
|
|
177
|
-
field = opts.field ?? field
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const _id = field == '_id' ? record.id : record[field as keyof typeof record] as string
|
|
181
|
-
if (id == null) {
|
|
182
|
-
throw new MisshapedRecord('id')
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const result = await resource.collection.deleteOne(identityCriteria(field, _id as string, refs()))
|
|
186
|
-
if (!result.acknowledged || result.deletedCount === 0) {
|
|
187
|
-
return null
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return record
|
|
183
|
+
return record != null ? demarshal(record as unknown as R) : null
|
|
191
184
|
},
|
|
192
185
|
|
|
193
|
-
|
|
194
|
-
const record = await resource.delete(id
|
|
186
|
+
take: async (id: string): Promise<R> => {
|
|
187
|
+
const record = await resource.delete(id)
|
|
195
188
|
if (record == null) {
|
|
196
|
-
throw new UnknownRecordError(
|
|
189
|
+
throw new UnknownRecordError(id)
|
|
197
190
|
}
|
|
198
191
|
|
|
199
192
|
return record
|
|
200
193
|
},
|
|
201
194
|
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const size = pager?.size ?? DEFAULT_PAGE_SIZE
|
|
209
|
-
const total = await resource.collection.countDocuments(criteria as ListCriteria)
|
|
210
|
-
pager.total = total
|
|
211
|
-
|
|
212
|
-
const skip = (pager.page ?? 0) * size
|
|
213
|
-
if (total === 0 && skip >= total) {
|
|
214
|
-
return { items: [], pager }
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
let cursor = resource.collection.find(criteria as ListCriteria)
|
|
218
|
-
.skip(skip).limit(size)
|
|
219
|
-
|
|
220
|
-
if (pager.sort != null) {
|
|
221
|
-
cursor = cursor.sort(
|
|
222
|
-
typeof pager.sort === 'string'
|
|
223
|
-
? pager.sort
|
|
224
|
-
: pager.sort.reduce((sort, [field, order]) =>
|
|
225
|
-
({ ...sort, [field as keyof typeof sort]: order ? -1 : 1 }), {}
|
|
226
|
-
)
|
|
227
|
-
)
|
|
195
|
+
purge: async (where: Criteria<R>): Promise<number> => {
|
|
196
|
+
const filter = mongoCriteriaHelper.criteriaToFilter(where, refs())
|
|
197
|
+
if (Object.keys(filter).length < 1) {
|
|
198
|
+
/** An empty filter here would empty the collection. */
|
|
199
|
+
throw new UnsupportedArgumentError('purge:no-criteria')
|
|
228
200
|
}
|
|
229
201
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
return {
|
|
233
|
-
pager, items: items.map(item => demarshal({ ...item } as unknown as R))
|
|
234
|
-
}
|
|
202
|
+
return (await resource.collection.deleteMany(filter)).deletedCount
|
|
235
203
|
},
|
|
236
204
|
|
|
237
205
|
lock: async (record, fields) => {
|
|
@@ -272,26 +240,26 @@ export const makeMongoResource = <
|
|
|
272
240
|
return await mongo.client(dbAlias)
|
|
273
241
|
},
|
|
274
242
|
|
|
275
|
-
|
|
243
|
+
/**
|
|
244
|
+
* `index`, `migration` and `reference` hand the resource itself back so declarations
|
|
245
|
+
* chain — a return an object literal can't express, hence the member level casts: each
|
|
246
|
+
* implementation returns the closed over `resource`, which is that very object.
|
|
247
|
+
*/
|
|
248
|
+
index: ((name: string, index: IndexSpecification, options?: CreateIndexesOptions) => {
|
|
276
249
|
resource.indexes = resource.indexes ?? []
|
|
277
250
|
resource.indexes.push({ name, index, options })
|
|
278
251
|
return resource
|
|
279
|
-
},
|
|
252
|
+
}) as T['index'],
|
|
280
253
|
|
|
281
|
-
/**
|
|
282
|
-
* `migration` and `reference` are `this`-returning in the interface, which an object
|
|
283
|
-
* literal can't express — hence the member level casts: the implementations return
|
|
284
|
-
* the closed over `resource`, which is that very object.
|
|
285
|
-
*/
|
|
286
254
|
migration: ((name: string, apply: (tx: MongoTx) => Promise<void>, stage?: MigrationStage) => {
|
|
287
|
-
getDeclaration(alias).migrations.register(name, apply, stage)
|
|
255
|
+
mongoDeclarationHelper.getDeclaration(alias).migrations.register(name, apply, stage)
|
|
288
256
|
return resource
|
|
289
257
|
}) as T['migration'],
|
|
290
258
|
|
|
291
|
-
migrations: () => getDeclaration(alias).migrations,
|
|
259
|
+
migrations: () => mongoDeclarationHelper.getDeclaration(alias).migrations,
|
|
292
260
|
|
|
293
261
|
reference: ((field: string, opts?: string | MongoRefOptions) => {
|
|
294
|
-
const declaration = getDeclaration(alias)
|
|
262
|
+
const declaration = mongoDeclarationHelper.getDeclaration(alias)
|
|
295
263
|
const options = typeof opts === 'string' ? { resource: opts } : opts ?? {}
|
|
296
264
|
declaration.references.set(field, { field, resource: options.resource, noIndex: options.noIndex })
|
|
297
265
|
/**
|
|
@@ -299,17 +267,16 @@ export const makeMongoResource = <
|
|
|
299
267
|
* here rather than at init so it precedes migrations the app declares after its
|
|
300
268
|
* `reference()` calls — the field's type contract is the foundation those build on.
|
|
301
269
|
*/
|
|
302
|
-
declaration.migrations.register(refMigrationName(field), makeRefMigration(field), MigrationStage.Pre)
|
|
270
|
+
declaration.migrations.register(mongoRefHelper.refMigrationName(field), makeRefMigration(field), MigrationStage.Pre)
|
|
303
271
|
|
|
304
272
|
return resource
|
|
305
273
|
}) as T['reference'],
|
|
306
274
|
|
|
307
|
-
references: () => [...getDeclaration(alias).references.values()]
|
|
275
|
+
references: () => [...mongoDeclarationHelper.getDeclaration(alias).references.values()]
|
|
308
276
|
} as Partial<T>)
|
|
309
277
|
|
|
310
278
|
// Explicit collection name override (decoupled from the registration alias, which may
|
|
311
|
-
// contain characters that aren't valid in a collection name).
|
|
312
|
-
// because it's threaded back into the recursive makeMongoResource call below.
|
|
279
|
+
// contain characters that aren't valid in a collection name).
|
|
313
280
|
if (collectionName != null) {
|
|
314
281
|
resource.name = collectionName
|
|
315
282
|
}
|
|
@@ -322,20 +289,11 @@ export const makeMongoResource = <
|
|
|
322
289
|
await mongo.ready()
|
|
323
290
|
const db = await mongo.db(dbAlias)
|
|
324
291
|
const config = mongo.config(dbAlias)
|
|
325
|
-
resource.collection = await initializeCollection(
|
|
326
|
-
|
|
292
|
+
resource.collection = await makeMongoLifeCycleUtils(db).initializeCollection(
|
|
293
|
+
config, resource as unknown as MongoResource<ResourceRecord>, context
|
|
327
294
|
)
|
|
328
295
|
}
|
|
329
296
|
|
|
330
|
-
resource.reinitializeContext = <Type extends Contextual>(context: BasicContext<Config>) => {
|
|
331
|
-
const resource = (makeCustomResource?.(dbAlias, serviceAlias)
|
|
332
|
-
?? makeMongoResource<R, T>(alias, dbAlias, serviceAlias, makeCustomResource, collectionName)) as unknown as Type
|
|
333
|
-
|
|
334
|
-
resource.ctx = context
|
|
335
|
-
|
|
336
|
-
return resource as Type
|
|
337
|
-
}
|
|
338
|
-
|
|
339
297
|
return resource
|
|
340
298
|
}
|
|
341
299
|
|
|
@@ -349,7 +307,7 @@ const _prepareValues = <T extends ResourceRecord>(
|
|
|
349
307
|
*/
|
|
350
308
|
if (refs != null && refs.size > 0) {
|
|
351
309
|
obj = Object.fromEntries(Object.entries(obj).map(([key, value]) =>
|
|
352
|
-
refs.has(key) ? [key, marshalReference(key, value)] : [key, value]
|
|
310
|
+
refs.has(key) ? [key, mongoRefHelper.marshalReference(key, value)] : [key, value]
|
|
353
311
|
)) as T
|
|
354
312
|
}
|
|
355
313
|
// @TODO Validate keys from additional properties in the root
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
Resource, ResourceRecord, ResourceDbService, DbLocker, ResourceLocker, MigratableResource
|
|
3
|
-
} from '@owlmeans/resource'
|
|
1
|
+
import type { Resource, ResourceRecord, ResourceDbService, DbLocker, LockableResource, MigratableResource, MigrationRegistry } from '@owlmeans/resource'
|
|
4
2
|
import type { Collection, CreateIndexesOptions, Db, IndexSpecification, MongoClient } from 'mongodb'
|
|
5
3
|
import type { AnySchema } from 'ajv'
|
|
6
4
|
|
|
@@ -44,7 +42,8 @@ export interface MongoRefOptions {
|
|
|
44
42
|
noIndex?: boolean
|
|
45
43
|
}
|
|
46
44
|
|
|
47
|
-
export interface MongoResource<T extends ResourceRecord> extends Resource<T>,
|
|
45
|
+
export interface MongoResource<T extends ResourceRecord> extends Resource<T>, LockableResource<T>,
|
|
46
|
+
MigratableResource<MongoTx, MongoResource<T>> {
|
|
48
47
|
name?: string
|
|
49
48
|
/**
|
|
50
49
|
* The db-config alias this resource was registered against.
|
|
@@ -61,13 +60,14 @@ export interface MongoResource<T extends ResourceRecord> extends Resource<T>, Re
|
|
|
61
60
|
collection: Collection
|
|
62
61
|
db: () => Promise<Db>
|
|
63
62
|
client: () => Promise<MongoClient>
|
|
64
|
-
index:
|
|
63
|
+
index: (name: string, index: IndexSpecification, options?: CreateIndexesOptions) => this
|
|
65
64
|
/**
|
|
66
65
|
* Declare that a field stores another record's id.
|
|
67
66
|
*
|
|
68
67
|
* Chainable and idempotent like {@link MigratableResource.migration}, and stored the same
|
|
69
|
-
* way — per alias at module scope —
|
|
70
|
-
*
|
|
68
|
+
* way — per alias at module scope — so a maker that runs more than once for the same alias
|
|
69
|
+
* re-declares the same entry rather than losing it. A lost declaration would silently stop
|
|
70
|
+
* the string/ObjectId conversion for the field.
|
|
71
71
|
*
|
|
72
72
|
* Declare only fields whose values really are mongo ids (assigned from another record's
|
|
73
73
|
* `id`). Composite keys, external provider ids, DIDs and business slugs must stay
|
|
@@ -81,3 +81,9 @@ export interface MongoResource<T extends ResourceRecord> extends Resource<T>, Re
|
|
|
81
81
|
|
|
82
82
|
export interface MongoDbService extends ResourceDbService<Db, MongoClient>, DbLocker<ResourceRecord> {
|
|
83
83
|
}
|
|
84
|
+
|
|
85
|
+
export interface MongoDeclaration {
|
|
86
|
+
migrations: MigrationRegistry<MongoTx>
|
|
87
|
+
/** Declared ObjectId references, keyed by field. Registered via `resource.reference()`. */
|
|
88
|
+
references: Map<string, MongoReference>
|
|
89
|
+
}
|