-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmongo-collections.js
217 lines (209 loc) · 8.04 KB
/
mongo-collections.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
const Mongodb = require('mongodb')
class Mongo {
static clients = []
static dbs = []
static db
static client
static onConnectHandlers = []
static ObjectId = Mongodb.ObjectId
static async connect(
uri = process.env?.MONGODB_URI || 'mongodb://localhost:27017',
options = {},
connectionName = 'default'
) {
if (Mongo.clients[connectionName]) {
throw Error('connection by the name', connectionName, 'already exists')
}
const client = await Mongodb.MongoClient.connect(uri, options)
Mongo.clients[connectionName] = client
Mongo.dbs[connectionName] = client.db()
if (connectionName === 'default') {
Mongo.client = client
Mongo.db = Mongo.dbs[connectionName]
}
if (Mongo.onConnectHandlers[connectionName]) {
for await (const handler of Mongo.onConnectHandlers[connectionName]) {
await handler()
}
}
return client.db
}
static disconnect(connectionName) {
function disc(name) {
if (Mongo.dbs[name]) {
delete Mongo.dbs[name]
Mongo.clients[name].close()
if (name === 'default') {
Mongo.db = undefined
Mongo.client = undefined
}
}
}
if (!connectionName) Object.keys(Mongo.clients).forEach(name => disc(name))
else disc(connectionName)
}
}
module.exports.Mongo = Mongo
module.exports.default = Mongo
// for the following - kudos to https://stackoverflow.com/a/30158566/6262595
function prototypeProperties(obj) {
const p = []
for (; obj != null; obj = Object.getPrototypeOf(obj)) {
const ops = Object.getOwnPropertyNames(obj)
for (const op of ops) if (p.indexOf(op) == -1) p.push(op)
}
return p
}
class Collection {
static connectionName = 'default'
static collectionOptions
static collectionIndexes
static ObjectId = Mongodb.ObjectId
static initialDocs
static async onConnect() {
// if there are creatOptions it must be done before db.collection(name) is ever called
try {
if (this.collectionOptions) {
const collections = await Mongo.dbs[this.connectionName]
.listCollections({ name: this.collectionName })
.toArray()
if (!(collections && collections.length === 1)) {
console.info('Collection.onConnect creating collection', this.collectionName)
var result = await Mongo.dbs[this.connectionName].createCollection(
this.collectionName,
this.collectionOptions
)
if (!result) console.error('Collection.onConnect result failed')
}
}
} catch (err) {
console.error('onConnect createCollection error:', err)
throw err
}
// now that the db is open, apply all the properties of the collection to the prototype for this class for they are part of new Collection
const collection = Mongo.dbs[this.connectionName].collection(this.collectionName)
this.collection = collection
const keys = prototypeProperties(collection)
for (const key of keys) {
if (key in this) continue
Object.defineProperty(this, key, {
get() {
return collection[key]
},
enumerable: true,
configurable: true,
})
// the line below applies the collection methods to the prototype for future use of new Collection() instances
// but that doesn't seem so useful because you still have to pass the new doc to the method like
// class User extends Collection
// const doc=new User(); doc.insertOne(doc). It would be clearer to says User.insertOne(doc)
Object.defineProperty(this.prototype, key, {
get() {
return collection[key]
},
enumerable: true,
configurable: true,
})
}
try {
if (this.collectionIndexes && this.collectionIndexes.length)
await this.collection.createIndexes(this.collectionIndexes)
} catch (err) {
console.error('createIndexes error:', err)
throw err
}
try {
var count = await this.collection.count()
if (this.initialDocs && (process.env.NODE_ENV !== 'production' || !count)) {
// if development, or if production but nothing in the database
await this._write_docs(this.initialDocs)
delete this.initialDocs
}
} catch (err) {
console.error('Collection.createIndexes error:', err)
throw err
}
}
static async setCollectionProps() {
const connectionName = this.connectionName
if (Mongo.dbs[connectionName]) {
await this.onConnect()
} else if (Mongo.onConnectHandlers[connectionName])
Mongo.onConnectHandlers[connectionName].push(this.onConnect.bind(this))
else Mongo.onConnectHandlers[connectionName] = [this.onConnect.bind(this)]
}
static async preload(docs) {
// this will mutate the docs so that the _id's are ObjectIds
let idCheck = {}
// convert object _id's to objects
docs.forEach(doc => {
if (doc._id instanceof Mongodb.ObjectId) {
idCheck[doc._id] = doc
return // nothing to do here
}
if (!doc._id) {
throw new Error("Document doesn't have an id:", doc)
}
const _idString = doc._id?.$oid || doc._id
if (!_idString) {
throw new Error("Document _id field doesn't look like ObjectId", doc)
}
if (idCheck[_idString]) {
throw new Error('_write_load duplicate id found. Replacing:\n', idCheck[_idString], '\nwith\n', doc)
}
idCheck[_idString] = doc
doc._id = new Mongodb.ObjectId(_idString)
})
if (this.initialDocs) this.initialDocs = this.initialDocs.concat(docs)
else if (this.collection) {
await this._write_docs(docs)
} else this.initialDocs = docs
}
static async _write_docs(docs) {
for await (const doc of docs) {
try {
const result = await this.collection.replaceOne({ _id: doc._id }, doc, { upsert: true })
if (
typeof result !== 'object' ||
!result.acknowledged ||
result.modifiedCount + result.upsertedCount + result.matchedCount < 1
) {
console.error('_write_load result not ok', result, 'for', doc)
// don't throw errors here- keep going
}
} catch (err) {
console.error('_write_load caught error trying to replaceOne for', err, 'doc was', doc)
// don't through errors here - just keep going
}
}
}
constructor(doc) {
if (this.constructor.validate) {
const result = this.constructor.validate(doc) || { value: doc }
if (result.error) {
throw result.error
}
Object.assign(this, result.value)
} else {
Object.assign(this, doc)
}
}
}
/*
Collection.setCollectionProps(
'docs',
'default',
{ capped: true,
size: publicConfig.MongoLogsCappedSize,
},
[
{ key: { path: 1 }, name: 'path', unique: true, partialFilterExpression: { path: { $exists: true } } },
{
key: { parentId: 1, 'component.component': 1, _id: -1 },
name: 'children',
partialFilterExpression: { parentId: { $exists: true }, 'component.component': { $exists: true } },
},
]
)
*/
module.exports.Collection = Collection