@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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +807 -0
  3. package/build/.gitkeep +0 -0
  4. package/build/consts.d.ts +3 -0
  5. package/build/consts.d.ts.map +1 -0
  6. package/build/consts.js +3 -0
  7. package/build/consts.js.map +1 -0
  8. package/build/helper.d.ts +3 -0
  9. package/build/helper.d.ts.map +1 -0
  10. package/build/helper.js +3 -0
  11. package/build/helper.js.map +1 -0
  12. package/build/index.d.ts +5 -0
  13. package/build/index.d.ts.map +1 -0
  14. package/build/index.js +4 -0
  15. package/build/index.js.map +1 -0
  16. package/build/resource.d.ts +4 -0
  17. package/build/resource.d.ts.map +1 -0
  18. package/build/resource.js +268 -0
  19. package/build/resource.js.map +1 -0
  20. package/build/types.d.ts +20 -0
  21. package/build/types.d.ts.map +1 -0
  22. package/build/types.js +2 -0
  23. package/build/types.js.map +1 -0
  24. package/build/utils/index.d.ts +5 -0
  25. package/build/utils/index.d.ts.map +1 -0
  26. package/build/utils/index.js +5 -0
  27. package/build/utils/index.js.map +1 -0
  28. package/build/utils/indexes.d.ts +5 -0
  29. package/build/utils/indexes.d.ts.map +1 -0
  30. package/build/utils/indexes.js +34 -0
  31. package/build/utils/indexes.js.map +1 -0
  32. package/build/utils/life-cycle.d.ts +7 -0
  33. package/build/utils/life-cycle.d.ts.map +1 -0
  34. package/build/utils/life-cycle.js +48 -0
  35. package/build/utils/life-cycle.js.map +1 -0
  36. package/build/utils/name.d.ts +4 -0
  37. package/build/utils/name.d.ts.map +1 -0
  38. package/build/utils/name.js +2 -0
  39. package/build/utils/name.js.map +1 -0
  40. package/build/utils/schema.d.ts +4 -0
  41. package/build/utils/schema.d.ts.map +1 -0
  42. package/build/utils/schema.js +63 -0
  43. package/build/utils/schema.js.map +1 -0
  44. package/package.json +41 -0
  45. package/src/consts.ts +4 -0
  46. package/src/helper.ts +5 -0
  47. package/src/index.ts +5 -0
  48. package/src/resource.ts +349 -0
  49. package/src/types.ts +17 -0
  50. package/src/utils/index.ts +5 -0
  51. package/src/utils/indexes.ts +38 -0
  52. package/src/utils/life-cycle.ts +64 -0
  53. package/src/utils/name.ts +5 -0
  54. package/src/utils/schema.ts +82 -0
  55. package/tsconfig.json +14 -0
  56. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/utils/schema.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,MAAiB,EAAY,EAAE;IACjE,MAAM,OAAO,GAAG,MAAiC,CAAA;IAEjD,IAAI,OAAO,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,IAAI,WAAW,CAAC,sDAAsD,CAAC,CAAA;IAC/E,CAAC;IAED,MAAM,WAAW,GAAa;QAC5B,QAAQ,EAAE,QAAQ;QAClB,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,IAAI;YACpC,CAAC,CAAC,OAAO,IAAI,OAAO,CAAC,UAAU;gBAC7B,CAAC,CAAC,qBAAqB,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC;gBACnD,iBAAiB,CAAC,OAAO,CAAC,UAAU,IAAI,EAAE,CAAC;YAC7C,CAAC,CAAC,SAAS;QACb,oBAAoB,EAClB,OAAO,CAAC,oBAAoB,IAAI,IAAI;YAClC,CAAC,CAAC,2BAA2B,CAAC,OAAO,CAAC,oBAAoB,CAAC;YAC3D,CAAC,CAAC,KAAK;QACX,GAAG,CAAC,OAAO,CAAC,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACpE,CAAA;IAED,IAAI,YAAY,IAAI,WAAW,IAAI,WAAW,CAAC,UAAU,IAAI,IAAI,EAAE,CAAC;QAClE,OAAO,WAAW,CAAC,UAAU,CAAA;IAC/B,CAAC;IAED,IAAI,UAAU,IAAI,WAAW,IAAI,WAAW,CAAC,QAAQ,IAAI,IAAI;WACxD,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,IAAI,WAAW,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9E,OAAO,WAAW,CAAC,QAAQ,CAAA;IAC7B,CAAC;IAED,OAAO,WAAW,CAAA;AACpB,CAAC,CAAA;AAED,MAAM,2BAA2B,GAAG,CAAC,oBAAyC,EAAsB,EAAE;IACpG,IAAI,OAAO,oBAAoB,KAAK,SAAS,EAAE,CAAC;QAC9C,OAAO,oBAAoB,CAAA;IAC7B,CAAC;IAED,OAAO,iBAAiB,CAAC,oBAA+C,CAAC,CAAA;AAC3E,CAAC,CAAA;AAED,MAAM,iBAAiB,GAAG,CAAC,UAAqC,EAAY,EAAE,CAC5E,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;IACjE,IAAI,GAAG,KAAK,OAAO,EAAE,CAAC;QACpB,MAAM,IAAI,WAAW,CAAC,0DAA0D,CAAC,CAAA;IACnF,CAAC;IAED,OAAO,CAAC,GAAG,EAAE,iBAAiB,CAAC,KAAgC,CAAC,CAAC,CAAA;AACnE,CAAC,CAAC,CAAC,CAAA;AAEL,MAAM,qBAAqB,GAAG,CAAC,KAAgD,EAAY,EAAE,CAC3F,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,iBAAiB,CAAC,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC;KAC5D,MAAM,CAAC,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,UAAU,EAAE,GAAG,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,CAAA;AAEvE,MAAM,iBAAiB,GAAG,CAAC,KAA8B,EAAqB,EAAE;IAC9E,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;SACjE,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;IAClD,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;QACnB,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IACnB,CAAC;IAED,OAAO,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;AAC3C,CAAC,CAAA;AAED,MAAM,iBAAiB,GAAG,CAAC,KAA8B,EAAY,EAAE;IACrE,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9B,IAAI,YAAY,IAAI,KAAK,IAAI,sBAAsB,IAAI,KAAK,EAAE,CAAC;YAC7D,OAAO;gBACL,GAAG,mBAAmB,CAAC,EAAE,GAAG,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;gBACpD,QAAQ,EAAE,iBAAiB,CAAC,KAAK,CAAC;aACnC,CAAA;QACH,CAAC;QACD,OAAO,EAAE,QAAQ,EAAE,iBAAiB,CAAC,KAAK,CAAC,EAAE,CAAA;IAC/C,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ;QAC5B,CAAC,CAAC,KAAK,CAAC,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,mBAAmB,CAAC,KAAK,CAAC;QAClF,CAAC,CAAC,EAAE,QAAQ,EAAE,iBAAiB,CAAC,KAAK,CAAC,EAAE,CAAA;AAC5C,CAAC,CAAA"}
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
@@ -0,0 +1,4 @@
1
+
2
+ export const DEFAULT_DB_ALIAS = 'mongo'
3
+
4
+ export const DEFAULT_PAGE_SIZE = 10
package/src/helper.ts ADDED
@@ -0,0 +1,5 @@
1
+ import type { AnySchema, JSONSchemaType } from "ajv"
2
+
3
+ export const getSchemaSecureFeilds = (schema: AnySchema): string[] =>
4
+ Object.entries((schema as JSONSchemaType<any>)?.properties)
5
+ .filter(([, prop]) => (prop as any).secure === true).map(([key]) => key)
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+
2
+ export type * from './types.js'
3
+ export * from './consts.js'
4
+ export * from './resource.js'
5
+ export * from './helper.js'
@@ -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,5 @@
1
+
2
+ export * from './indexes.js'
3
+ export * from './schema.js'
4
+ export * from './name.js'
5
+ export * from './life-cycle.js'
@@ -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"}