@owlmeans/mongo-resource 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/LICENSE +21 -0
- package/README.md +807 -0
- package/build/.gitkeep +0 -0
- package/build/consts.d.ts +3 -0
- package/build/consts.d.ts.map +1 -0
- package/build/consts.js +3 -0
- package/build/consts.js.map +1 -0
- package/build/helper.d.ts +3 -0
- package/build/helper.d.ts.map +1 -0
- package/build/helper.js +3 -0
- package/build/helper.js.map +1 -0
- package/build/index.d.ts +5 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +4 -0
- package/build/index.js.map +1 -0
- package/build/resource.d.ts +4 -0
- package/build/resource.d.ts.map +1 -0
- package/build/resource.js +268 -0
- package/build/resource.js.map +1 -0
- package/build/types.d.ts +20 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/build/utils/index.d.ts +5 -0
- package/build/utils/index.d.ts.map +1 -0
- package/build/utils/index.js +5 -0
- package/build/utils/index.js.map +1 -0
- package/build/utils/indexes.d.ts +5 -0
- package/build/utils/indexes.d.ts.map +1 -0
- package/build/utils/indexes.js +34 -0
- package/build/utils/indexes.js.map +1 -0
- package/build/utils/life-cycle.d.ts +7 -0
- package/build/utils/life-cycle.d.ts.map +1 -0
- package/build/utils/life-cycle.js +48 -0
- package/build/utils/life-cycle.js.map +1 -0
- package/build/utils/name.d.ts +4 -0
- package/build/utils/name.d.ts.map +1 -0
- package/build/utils/name.js +2 -0
- package/build/utils/name.js.map +1 -0
- package/build/utils/schema.d.ts +4 -0
- package/build/utils/schema.d.ts.map +1 -0
- package/build/utils/schema.js +63 -0
- package/build/utils/schema.js.map +1 -0
- package/package.json +41 -0
- package/src/consts.ts +4 -0
- package/src/helper.ts +5 -0
- package/src/index.ts +5 -0
- package/src/resource.ts +349 -0
- package/src/types.ts +17 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/indexes.ts +38 -0
- package/src/utils/life-cycle.ts +64 -0
- package/src/utils/name.ts +5 -0
- package/src/utils/schema.ts +82 -0
- package/tsconfig.json +14 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/utils/schema.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@owlmeans/mongo-resource",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "tsc -b",
|
|
7
|
+
"dev": "sleep 198 && nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
|
|
8
|
+
"watch": "tsc -b -w --preserveWatchOutput --pretty"
|
|
9
|
+
},
|
|
10
|
+
"main": "build/index.js",
|
|
11
|
+
"module": "build/index.js",
|
|
12
|
+
"types": "build/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"import": "./build/index.js",
|
|
16
|
+
"require": "./build/index.js",
|
|
17
|
+
"default": "./build/index.js",
|
|
18
|
+
"module": "./build/index.js",
|
|
19
|
+
"types": "./build/index.d.ts"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"ajv": "*",
|
|
24
|
+
"mongodb": "*"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@owlmeans/context": "^0.1.0",
|
|
28
|
+
"@owlmeans/resource": "^0.1.0",
|
|
29
|
+
"@owlmeans/server-context": "^0.1.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^22.7.8",
|
|
33
|
+
"nodemon": "^3.1.7",
|
|
34
|
+
"npm-check": "^6.0.1",
|
|
35
|
+
"typescript": "^5.6.3"
|
|
36
|
+
},
|
|
37
|
+
"private": false,
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/consts.ts
ADDED
package/src/helper.ts
ADDED
package/src/index.ts
ADDED
package/src/resource.ts
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { appendContextual, assertContext } from '@owlmeans/context'
|
|
2
|
+
import type { BasicContext, Contextual } from '@owlmeans/context'
|
|
3
|
+
import { DEFAULT_DB_ALIAS, DEFAULT_PAGE_SIZE } from './consts.js'
|
|
4
|
+
import type { ListCriteria, ResourceMaker, ResourceRecord } from '@owlmeans/resource'
|
|
5
|
+
import type { ServerConfig, ServerContext } from '@owlmeans/server-context'
|
|
6
|
+
import type { MongoDbService, MongoResource } from './types.js'
|
|
7
|
+
import { initializeCollection } from './utils/life-cycle.js'
|
|
8
|
+
import { ObjectId } from 'mongodb'
|
|
9
|
+
import { MisshapedRecord, RecordExists, UnknownRecordError, UnsupportedArgumentError, RecordUpdateFailed, prepareListOptions } from '@owlmeans/resource'
|
|
10
|
+
import type { JSONSchemaType } from 'ajv'
|
|
11
|
+
import { getSchemaSecureFeilds } from './helper.js'
|
|
12
|
+
|
|
13
|
+
type Config = ServerConfig
|
|
14
|
+
type Context<C extends Config = Config> = ServerContext<C>
|
|
15
|
+
|
|
16
|
+
export const makeMongoResource = <
|
|
17
|
+
R extends ResourceRecord, T extends MongoResource<R> = MongoResource<R>
|
|
18
|
+
>(
|
|
19
|
+
alias: string, dbAlias: string = DEFAULT_DB_ALIAS, serviceAlias: string = DEFAULT_DB_ALIAS,
|
|
20
|
+
makeCustomResource?: ResourceMaker<R, T>
|
|
21
|
+
): T => {
|
|
22
|
+
const location = `mongo-resource:${alias}`
|
|
23
|
+
|
|
24
|
+
const resource: T = appendContextual<T>(alias, {
|
|
25
|
+
get: async (id, field, opts) => {
|
|
26
|
+
const record = await resource.load(id, field, opts)
|
|
27
|
+
if (record == null) {
|
|
28
|
+
throw new UnknownRecordError(id)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return record
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
load: async (id, field, opts) => {
|
|
35
|
+
if (typeof field === 'object') {
|
|
36
|
+
opts = field
|
|
37
|
+
field = field.field
|
|
38
|
+
}
|
|
39
|
+
field = field ?? '_id'
|
|
40
|
+
const criteria = '_id' === field ? new ObjectId(id) : id
|
|
41
|
+
if (opts?.ttl != null) {
|
|
42
|
+
throw new UnsupportedArgumentError('ttl')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const record = await resource.collection.findOne({ [field]: criteria })
|
|
46
|
+
if (record != null) {
|
|
47
|
+
record.id = record._id instanceof ObjectId ? record._id.toString() : record._id
|
|
48
|
+
delete (record as any)._id
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return record
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
update: async (record, opts) => {
|
|
55
|
+
let field = '_id'
|
|
56
|
+
if (typeof opts === 'string') {
|
|
57
|
+
field = opts
|
|
58
|
+
} else if (typeof opts === 'object') {
|
|
59
|
+
field = opts.field ?? field
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (field !== '_id' && record[field as keyof typeof record] == null) {
|
|
63
|
+
throw new MisshapedRecord('no-field-value')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const id = field == '_id' ? record.id : record[field as keyof typeof record] as string
|
|
67
|
+
if (id == null) {
|
|
68
|
+
throw new MisshapedRecord('id')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const original = await resource.get(id, field)
|
|
72
|
+
|
|
73
|
+
const criteria = '_id' === field ? new ObjectId(id) : id
|
|
74
|
+
|
|
75
|
+
const replace = { ...record, _id: new ObjectId(original.id) }
|
|
76
|
+
if (replace.id != null) {
|
|
77
|
+
delete replace.id
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const result = await resource.collection.replaceOne(
|
|
81
|
+
{ [field]: criteria },
|
|
82
|
+
_prepareValues(replace, resource.schema as JSONSchemaType<any>)
|
|
83
|
+
)
|
|
84
|
+
if (!result.acknowledged) {
|
|
85
|
+
throw new RecordUpdateFailed(`${field}:${id}`)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return resource.get(id, opts)
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
save: async (record, opts) => {
|
|
92
|
+
const id = record.id
|
|
93
|
+
if (id != null || (
|
|
94
|
+
typeof opts === 'string' && record[opts as keyof typeof record] != null
|
|
95
|
+
) || (
|
|
96
|
+
typeof opts === 'object' && ("field" in opts)
|
|
97
|
+
&& record[opts.field as keyof typeof record] != null
|
|
98
|
+
)) {
|
|
99
|
+
return resource.update(record, opts)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return resource.create(record, typeof opts !== 'string' ? opts : undefined)
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
getDefaults: () => {
|
|
106
|
+
const schema: JSONSchemaType<unknown> | undefined = resource.schema as JSONSchemaType<unknown>
|
|
107
|
+
if (schema == null) {
|
|
108
|
+
return {}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return Object.entries(schema.properties ?? {}).reduce((defaults, [key, value]) => {
|
|
112
|
+
if ((value as JSONSchemaType<unknown>).default == null) {
|
|
113
|
+
return defaults
|
|
114
|
+
}
|
|
115
|
+
return { ...defaults, [key]: (value as JSONSchemaType<unknown>).default }
|
|
116
|
+
}, {})
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
create: async (record, opts) => {
|
|
120
|
+
if ("id" in record && record.id == null) {
|
|
121
|
+
delete record.id
|
|
122
|
+
}
|
|
123
|
+
if (record.id != null) {
|
|
124
|
+
throw new RecordExists('id-present')
|
|
125
|
+
}
|
|
126
|
+
if (opts != null && opts.ttl != null) {
|
|
127
|
+
throw new UnsupportedArgumentError('ttl')
|
|
128
|
+
}
|
|
129
|
+
const result = await resource.collection.insertOne({
|
|
130
|
+
...resource.getDefaults(),
|
|
131
|
+
..._prepareValues(record, resource.schema as JSONSchemaType<any>)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
if (!result.acknowledged) {
|
|
135
|
+
throw new RecordUpdateFailed(`creation`)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const id = result.insertedId
|
|
139
|
+
|
|
140
|
+
return resource.get(id.toString())
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
delete: async (id, opts) => {
|
|
144
|
+
let record: R | null = null
|
|
145
|
+
if (typeof id === 'object') {
|
|
146
|
+
if (id.id == null) {
|
|
147
|
+
throw new MisshapedRecord('id')
|
|
148
|
+
}
|
|
149
|
+
record = await resource.load(id.id)
|
|
150
|
+
} else {
|
|
151
|
+
record = await resource.load(id, opts)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (record == null) {
|
|
155
|
+
return null
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let field = '_id'
|
|
159
|
+
if (typeof opts === 'string') {
|
|
160
|
+
field = opts
|
|
161
|
+
opts = undefined
|
|
162
|
+
} else if (typeof opts === 'object') {
|
|
163
|
+
field = opts.field ?? field
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const _id = field == '_id' ? record.id : record[field as keyof typeof record] as string
|
|
167
|
+
if (id == null) {
|
|
168
|
+
throw new MisshapedRecord('id')
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const criteria = '_id' === field ? new ObjectId(_id) : _id
|
|
172
|
+
|
|
173
|
+
const result = await resource.collection.deleteOne({ [field]: criteria })
|
|
174
|
+
if (!result.acknowledged || result.deletedCount === 0) {
|
|
175
|
+
return null
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return record
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
pick: async (id, opts) => {
|
|
182
|
+
const record = await resource.delete(id, opts)
|
|
183
|
+
if (record == null) {
|
|
184
|
+
throw new UnknownRecordError(typeof id == 'string' ? id : (id.id ?? 'unknown'))
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return record
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
list: async (criteria, opts) => {
|
|
191
|
+
const options = prepareListOptions(DEFAULT_PAGE_SIZE, criteria, opts)
|
|
192
|
+
|
|
193
|
+
criteria = options.criteria
|
|
194
|
+
const pager = options.pager ?? {}
|
|
195
|
+
|
|
196
|
+
const size = pager?.size ?? DEFAULT_PAGE_SIZE
|
|
197
|
+
const total = await resource.collection.countDocuments(criteria as ListCriteria)
|
|
198
|
+
pager.total = total
|
|
199
|
+
|
|
200
|
+
const skip = (pager.page ?? 0) * size
|
|
201
|
+
if (total === 0 && skip >= total) {
|
|
202
|
+
return { items: [], pager }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let cursor = resource.collection.find(criteria as ListCriteria)
|
|
206
|
+
.skip(skip).limit(size)
|
|
207
|
+
|
|
208
|
+
if (pager.sort != null) {
|
|
209
|
+
cursor = cursor.sort(
|
|
210
|
+
typeof pager.sort === 'string'
|
|
211
|
+
? pager.sort
|
|
212
|
+
: pager.sort.reduce((sort, [field, order]) =>
|
|
213
|
+
({ ...sort, [field as keyof typeof sort]: order ? -1 : 1 }), {}
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const items = await cursor.toArray()
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
pager, items: items.map(item => {
|
|
222
|
+
const _item: R = { ...item } as any
|
|
223
|
+
_item.id = item._id.toString()
|
|
224
|
+
delete (_item as any)._id
|
|
225
|
+
|
|
226
|
+
return _item
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
lock: async (record, fields) => {
|
|
232
|
+
fields ??= getSchemaSecureFeilds(resource.schema ?? {})
|
|
233
|
+
if (fields == null || fields.length < 1) {
|
|
234
|
+
throw new SyntaxError(`No fields to lock: ${JSON.stringify(record)}`)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const context = assertContext<Config, Context>(resource.ctx as Context, location)
|
|
238
|
+
const mongo = context.service<MongoDbService>(serviceAlias ?? dbAlias)
|
|
239
|
+
|
|
240
|
+
return mongo.lock(dbAlias, record, fields)
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
unlock: async (record, fields) => {
|
|
244
|
+
fields ??= getSchemaSecureFeilds(resource.schema ?? {})
|
|
245
|
+
if (fields == null || fields.length < 1) {
|
|
246
|
+
throw new SyntaxError(`No fields to unlock: ${JSON.stringify(record)}`)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const context = assertContext<Config, Context>(resource.ctx as Context, location)
|
|
250
|
+
const mongo = context.service<MongoDbService>(serviceAlias ?? dbAlias)
|
|
251
|
+
|
|
252
|
+
return mongo.unlock(dbAlias, record, fields)
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
db: async () => {
|
|
256
|
+
const context = assertContext<Config, Context>(resource.ctx as Context, location)
|
|
257
|
+
const mongo = context.service<MongoDbService>(serviceAlias ?? dbAlias)
|
|
258
|
+
await mongo.ready()
|
|
259
|
+
return await mongo.db(dbAlias)
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
client: async () => {
|
|
263
|
+
const context = assertContext<Config, Context>(resource.ctx as Context, location)
|
|
264
|
+
const mongo = context.service<MongoDbService>(serviceAlias ?? dbAlias)
|
|
265
|
+
await mongo.ready()
|
|
266
|
+
return await mongo.client(dbAlias)
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
index: (name, index, options) => {
|
|
270
|
+
resource.indexes = resource.indexes ?? []
|
|
271
|
+
resource.indexes.push({ name, index, options })
|
|
272
|
+
return resource
|
|
273
|
+
}
|
|
274
|
+
} as Partial<T>)
|
|
275
|
+
|
|
276
|
+
resource.init = async () => {
|
|
277
|
+
const context = assertContext<Config, Context>(resource.ctx as Context, location)
|
|
278
|
+
const mongo = context.service<MongoDbService>(serviceAlias ?? dbAlias)
|
|
279
|
+
await mongo.ready()
|
|
280
|
+
const db = await mongo.db(dbAlias)
|
|
281
|
+
const config = mongo.config(dbAlias)
|
|
282
|
+
resource.collection = await initializeCollection(db, config, resource as unknown as MongoResource<ResourceRecord>)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
resource.reinitializeContext = <Type extends Contextual>(context: BasicContext<Config>) => {
|
|
286
|
+
const resource = (makeCustomResource?.(dbAlias, serviceAlias)
|
|
287
|
+
?? makeMongoResource<R, T>(alias, dbAlias, serviceAlias)) as unknown as Type
|
|
288
|
+
|
|
289
|
+
resource.ctx = context
|
|
290
|
+
|
|
291
|
+
return resource as Type
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return resource
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const _prepareValues = <T extends ResourceRecord>(obj: T, schema?: JSONSchemaType<T>): T => {
|
|
298
|
+
// @TODO Validate keys from additional properties in the root
|
|
299
|
+
return schema != null ? Object.fromEntries(Object.entries(obj).map(([key, value]) => {
|
|
300
|
+
// @TODO What if we reference another table by object id in another field?
|
|
301
|
+
// How to properly transform them?
|
|
302
|
+
// What if _id isn't an Object Id?
|
|
303
|
+
if (key === '_id') {
|
|
304
|
+
return [key, new ObjectId(value as string)]
|
|
305
|
+
}
|
|
306
|
+
const type = schema.properties?.[key]
|
|
307
|
+
|
|
308
|
+
if (type?.type === 'object') {
|
|
309
|
+
if (type.format === 'date-time') {
|
|
310
|
+
return [key, new Date(value as string)]
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (type.additionalProperties != null && type.additionalProperties.type === 'object') {
|
|
314
|
+
value = Object.fromEntries(Object.entries(value).map(([key, value]) => {
|
|
315
|
+
return [key, _prepareSingleValue(value, type.additionalProperties as JSONSchemaType<any>)]
|
|
316
|
+
}))
|
|
317
|
+
|
|
318
|
+
return [key, value]
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return [key, _prepareValues(value, type as JSONSchemaType<typeof value>)]
|
|
322
|
+
} else if (type?.type === 'array') {
|
|
323
|
+
return [key, (value as any[]).map(item => _prepareSingleValue(item, type.items as JSONSchemaType<any>))]
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return [key, _prepareSingleValue(value, type)]
|
|
327
|
+
})) as T : obj
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const _prepareSingleValue = <T>(value: T, schema?: JSONSchemaType<T>) => {
|
|
331
|
+
if (['object', 'array'].includes(schema?.type)) {
|
|
332
|
+
return _prepareValues(value as any, schema as JSONSchemaType<any>)
|
|
333
|
+
}
|
|
334
|
+
if (schema?.type === 'string' && value != null) {
|
|
335
|
+
return `${value}`
|
|
336
|
+
}
|
|
337
|
+
if (schema?.type === 'number' && value != null) {
|
|
338
|
+
return +value
|
|
339
|
+
}
|
|
340
|
+
if (schema?.type === 'boolean' && value != null) {
|
|
341
|
+
return !!value
|
|
342
|
+
}
|
|
343
|
+
if (value == null) {
|
|
344
|
+
return value
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// We sclarize value forcefully if we don't expect any tricks here
|
|
348
|
+
return `${value}`
|
|
349
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Resource, ResourceRecord, ResourceDbService, DbLocker, ResourceLocker } from '@owlmeans/resource'
|
|
2
|
+
import type { Collection, CreateIndexesOptions, Db, IndexSpecification, MongoClient } from 'mongodb'
|
|
3
|
+
import type { AnySchema } from 'ajv'
|
|
4
|
+
|
|
5
|
+
export interface MongoResource<T extends ResourceRecord> extends Resource<T>, ResourceLocker<T> {
|
|
6
|
+
name?: string
|
|
7
|
+
schema?: AnySchema
|
|
8
|
+
indexes?: Array<{ name: string, index: IndexSpecification, options?: CreateIndexesOptions }>
|
|
9
|
+
collection: Collection
|
|
10
|
+
db: () => Promise<Db>
|
|
11
|
+
client: () => Promise<MongoClient>
|
|
12
|
+
index: <Type extends MongoResource<T>>(name: string, index: IndexSpecification, options?: CreateIndexesOptions) => Type
|
|
13
|
+
getDefaults: () => Partial<T>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface MongoDbService extends ResourceDbService<Db, MongoClient>, DbLocker<ResourceRecord> {
|
|
17
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ResourceRecord } from '@owlmeans/resource'
|
|
2
|
+
import type { MongoResource } from '../types.js'
|
|
3
|
+
import type { Collection } from 'mongodb'
|
|
4
|
+
|
|
5
|
+
export const updateIndexes = async (collection: Collection, resource: MongoResource<ResourceRecord>) => {
|
|
6
|
+
if (resource.indexes != null) {
|
|
7
|
+
await Promise.all(resource.indexes.map(
|
|
8
|
+
async index => {
|
|
9
|
+
const _index = (await collection.indexes()).find(_index => _index.name === index.name)
|
|
10
|
+
if (_index != null) {
|
|
11
|
+
const existing = { ..._index }
|
|
12
|
+
if ('name' in existing) {
|
|
13
|
+
delete existing.name
|
|
14
|
+
}
|
|
15
|
+
if ('v' in existing) {
|
|
16
|
+
delete existing.v
|
|
17
|
+
}
|
|
18
|
+
const propsed = { key: index.index, ...index.options }
|
|
19
|
+
// We do not recreate text indexes
|
|
20
|
+
if (JSON.stringify(existing) !== JSON.stringify(propsed) && !('weights' in existing)) {
|
|
21
|
+
console.info(JSON.stringify(existing))
|
|
22
|
+
console.info(JSON.stringify(propsed))
|
|
23
|
+
console.debug(`Recreate index ${resource.alias} : ${index.name}`)
|
|
24
|
+
await collection.dropIndex(index.name)
|
|
25
|
+
await collection.createIndex(index.index, {
|
|
26
|
+
name: index.name, ...((index.options != null) ? index.options : {})
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
} else {
|
|
30
|
+
console.debug(`Create index ${resource.alias} : ${index.name}`)
|
|
31
|
+
await collection.createIndex(index.index, {
|
|
32
|
+
name: index.name, ...((index.options != null) ? index.options : {})
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
))
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { DbConfig, ResourceRecord } from '@owlmeans/resource'
|
|
2
|
+
import type { MongoResource } from '../types.js'
|
|
3
|
+
import type { Db, Collection, Document } from 'mongodb'
|
|
4
|
+
import { mongoCollectionName } from './name.js'
|
|
5
|
+
import { schemaToMongoSchema } from './schema.js'
|
|
6
|
+
import { updateIndexes } from './indexes.js'
|
|
7
|
+
|
|
8
|
+
export const initializeCollection = async (
|
|
9
|
+
db: Db, config: DbConfig, resource: MongoResource<ResourceRecord>
|
|
10
|
+
): Promise<Collection> => {
|
|
11
|
+
let _collection: Collection
|
|
12
|
+
|
|
13
|
+
const name = mongoCollectionName(config, resource)
|
|
14
|
+
const cursor = db.listCollections({ name })
|
|
15
|
+
if (!await cursor.hasNext()) {
|
|
16
|
+
_collection = await createCollection(db, name, resource)
|
|
17
|
+
} else {
|
|
18
|
+
_collection = await updateCollection(db, name, resource)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return _collection
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const createCollection = async (db: Db, name: string, resource: MongoResource<ResourceRecord>): Promise<Collection> => {
|
|
25
|
+
const collection = await db.createCollection(name, {
|
|
26
|
+
...(resource.schema != null ? {
|
|
27
|
+
validator: {
|
|
28
|
+
$jsonSchema: patchJsonSchema(schemaToMongoSchema(resource.schema))
|
|
29
|
+
}
|
|
30
|
+
} : {})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
if (resource.indexes != null) {
|
|
34
|
+
await Promise.all(resource.indexes.map(
|
|
35
|
+
async index => await collection.createIndex(index.index, {
|
|
36
|
+
name: index.name, ...((index.options != null) ? index.options : {})
|
|
37
|
+
})
|
|
38
|
+
))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return collection
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const updateCollection = async (db: Db, name: string, resource: MongoResource<ResourceRecord>): Promise<Collection> => {
|
|
45
|
+
if (resource.schema != null) {
|
|
46
|
+
const $jsonSchema = patchJsonSchema(schemaToMongoSchema(resource.schema))
|
|
47
|
+
await db.command({ collMod: name, validator: { $jsonSchema } })
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const collection = db.collection(name)
|
|
51
|
+
|
|
52
|
+
await updateIndexes(collection, resource)
|
|
53
|
+
|
|
54
|
+
return collection
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const patchJsonSchema = (schema: Document): Document => {
|
|
58
|
+
if (schema.properties != null) {
|
|
59
|
+
if (schema.properties._id == null) {
|
|
60
|
+
schema.properties._id = { bsonType: 'objectId' }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return schema
|
|
64
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { DbConfig, ResourceRecord } from '@owlmeans/resource'
|
|
2
|
+
import type { MongoResource } from '../types.js'
|
|
3
|
+
|
|
4
|
+
export const mongoCollectionName = (config: DbConfig, resource: MongoResource<ResourceRecord>): string =>
|
|
5
|
+
`${config.resourcePrefix ?? ''}${resource.name ?? resource.alias}`
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { AnySchema, JSONSchemaType } from 'ajv'
|
|
2
|
+
import type { Document } from 'mongodb'
|
|
3
|
+
|
|
4
|
+
export const schemaToMongoSchema = (schema: AnySchema): Document => {
|
|
5
|
+
const _schema = schema as JSONSchemaType<unknown>
|
|
6
|
+
|
|
7
|
+
if (_schema.type !== 'object') {
|
|
8
|
+
throw new SyntaxError('Only object schemas can be converted to mongo schema')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const mongoSchems: Document = {
|
|
12
|
+
bsonType: 'object',
|
|
13
|
+
properties: _schema.properties != null
|
|
14
|
+
? 'allOf' in _schema.properties
|
|
15
|
+
? covertAllOfProperties(_schema.properties.allOf) :
|
|
16
|
+
convertProperties(_schema.properties ?? {})
|
|
17
|
+
: undefined,
|
|
18
|
+
additionalProperties:
|
|
19
|
+
_schema.additionalProperties != null
|
|
20
|
+
? convertAdditionalProperties(_schema.additionalProperties)
|
|
21
|
+
: false,
|
|
22
|
+
...(_schema.required != null ? { required: _schema.required } : {}),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if ("properties" in mongoSchems && mongoSchems.properties == null) {
|
|
26
|
+
delete mongoSchems.properties
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if ("required" in mongoSchems && mongoSchems.required == null
|
|
30
|
+
|| Array.isArray(mongoSchems.required) && mongoSchems.required.length === 0) {
|
|
31
|
+
delete mongoSchems.required
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return mongoSchems
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const convertAdditionalProperties = (additionalProperties: boolean | AnySchema): Document | boolean => {
|
|
38
|
+
if (typeof additionalProperties === 'boolean') {
|
|
39
|
+
return additionalProperties
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return convertToBsonType(additionalProperties as JSONSchemaType<unknown>)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const convertProperties = (properties: Record<string, AnySchema>): Document =>
|
|
46
|
+
Object.fromEntries(Object.entries(properties).map(([key, value]) => {
|
|
47
|
+
if (key === 'oneOf') {
|
|
48
|
+
throw new SyntaxError('We dont support schemas with oneof properties definition')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return [key, convertToBsonType(value as JSONSchemaType<unknown>)]
|
|
52
|
+
}))
|
|
53
|
+
|
|
54
|
+
const covertAllOfProperties = (allOf: Record<string, JSONSchemaType<unknown>>[]): Document =>
|
|
55
|
+
allOf.map(schema => convertProperties(schema.properties ?? {}))
|
|
56
|
+
.reduce((properties, schema) => ({ ...properties, ...schema }), {})
|
|
57
|
+
|
|
58
|
+
const prepareScalarType = (value: JSONSchemaType<unknown>): string | string[] => {
|
|
59
|
+
const type = (Array.isArray(value.type) ? value.type : [value.type])
|
|
60
|
+
.map(type => type === 'boolean' ? 'bool' : type)
|
|
61
|
+
if (value.nullable) {
|
|
62
|
+
type.push('null')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return type.length === 1 ? type[0] : type
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const convertToBsonType = (value: JSONSchemaType<unknown>): Document => {
|
|
69
|
+
if (Array.isArray(value.type)) {
|
|
70
|
+
if ('properties' in value || 'additionalProperties' in value) {
|
|
71
|
+
return {
|
|
72
|
+
...schemaToMongoSchema({ ...value, type: 'object' }),
|
|
73
|
+
bsonType: prepareScalarType(value)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { bsonType: prepareScalarType(value) }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return value.type === 'object'
|
|
80
|
+
? value.format === 'date-time' ? { bsonType: 'date' } : schemaToMongoSchema(value)
|
|
81
|
+
: { bsonType: prepareScalarType(value) }
|
|
82
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": [
|
|
3
|
+
"../tsconfig.default.json",
|
|
4
|
+
],
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"rootDir": "./src/", /* Specify the root folder within your source files. */
|
|
7
|
+
"outDir": "./build/", /* Specify an output folder for all emitted files. */
|
|
8
|
+
},
|
|
9
|
+
"exclude": [
|
|
10
|
+
"./dist/**/*",
|
|
11
|
+
"./build/**/*",
|
|
12
|
+
"./*.ts"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["./src/consts.ts","./src/helper.ts","./src/index.ts","./src/resource.ts","./src/types.ts","./src/utils/index.ts","./src/utils/indexes.ts","./src/utils/life-cycle.ts","./src/utils/name.ts","./src/utils/schema.ts"],"version":"5.6.3"}
|