UNPKG

14.1 kBJavaScriptView Raw
1import { validate, validators as v } from 'easevalidation';
2import invariant from 'invariant';
3import merge from 'lodash/merge';
4import { MongoClient, ObjectID } from 'mongodb';
5import { Module } from 'oors';
6import Repository from './libs/Repository';
7import * as helpers from './libs/helpers';
8import * as decorators from './decorators';
9import MigrationRepository from './repositories/Migration';
10import Seeder from './libs/Seeder';
11import withLogger from './decorators/withLogger';
12import withTimestamps from './decorators/withTimestamps';
13import Migration from './libs/Migration';
14import GQLQueryParser from './graphql/GQLQueryParser';
15import Migrator from './libs/Migrator';
16
17class MongoDB extends Module {
18 static validateConfig = validate(
19 v.isSchema({
20 connections: [
21 v.isRequired(),
22 v.isArray(
23 v.isSchema({
24 name: [v.isRequired(), v.isString()],
25 database: v.isAny(v.isString(), v.isUndefined()),
26 url: [v.isRequired(), v.isString()],
27 options: [v.isDefault({}), v.isObject()],
28 }),
29 ),
30 v.isLength({
31 min: 1,
32 }),
33 ],
34 defaultConnection: v.isAny(v.isString(), v.isUndefined()),
35 migration: [
36 v.isDefault({}),
37 v.isSchema({
38 isEnabled: [v.isDefault(false), v.isBoolean()],
39 isSilent: [v.isDefault(false), v.isBoolean()],
40 }),
41 ],
42 logQueries: [v.isDefault(true), v.isBoolean()],
43 addTimestamps: [v.isDefault(true), v.isBoolean()],
44 seeding: [
45 v.isDefault({}),
46 v.isSchema({
47 isEnabled: [v.isDefault(false), v.isBoolean()],
48 }),
49 ],
50 transaction: [
51 v.isDefault({}),
52 v.isSchema({
53 isEnabled: [v.isDefault(false), v.isBoolean()],
54 }),
55 ],
56 moduleDefaultConfig: [
57 v.isDefault({}),
58 v.isSchema({
59 repositories: [
60 v.isDefault({}),
61 v.isSchema({
62 autoload: [v.isDefault(true), v.isBoolean()],
63 dir: [v.isDefault('repositories'), v.isString()],
64 prefix: [v.isDefault(''), v.isString()],
65 collectionPrefix: [v.isDefault(''), v.isString()],
66 }),
67 ],
68 migrations: [
69 v.isDefault({}),
70 v.isSchema({
71 autoload: [v.isDefault(true), v.isBoolean()],
72 dir: [v.isDefault('migrations'), v.isString()],
73 }),
74 ],
75 }),
76 ],
77 }),
78 );
79
80 static RELATION_TYPE = {
81 ONE: 'one',
82 MANY: 'many',
83 };
84
85 name = 'oors.mongodb';
86
87 connections = {};
88
89 relations = {};
90
91 repositories = {};
92
93 hooks = {
94 'oors.graphql.buildContext': ({ context }) => {
95 const { fromMongo, fromMongoCursor, fromMongoArray, toMongo } = helpers;
96
97 Object.assign(context, {
98 fromMongo,
99 fromMongoCursor,
100 fromMongoArray,
101 toMongo,
102 getRepository: this.getRepository,
103 toObjectId: this.toObjectId,
104 gqlQueryParser: this.gqlQueryParser,
105 });
106 },
107 };
108
109 initialize({ connections, defaultConnection, logQueries, addTimestamps }) {
110 this.defaultConnectionName = defaultConnection || connections[0].name;
111
112 const names = connections.map(({ name }) => name);
113
114 if (!names.includes(this.defaultConnectionName)) {
115 throw new Error(
116 `Default connection name - "(${this.defaultConnectionName})" - can't be found through the list of available connections (${names})`,
117 );
118 }
119
120 this.onModule(this.name, 'repository', ({ repository }) => {
121 if (logQueries) {
122 withLogger()(repository);
123 }
124
125 if (addTimestamps) {
126 withTimestamps()(repository);
127 }
128 });
129
130 this.addHook('oors.health', 'scan', async collector => {
131 const databases = {};
132
133 await Promise.all(
134 Object.keys(this.connections).map(async name => {
135 databases[name] = await this.connections[name].isConnected();
136 }),
137 );
138
139 Object.assign(collector, {
140 'oors.mongodb': {
141 databases,
142 },
143 });
144 });
145 }
146
147 async setup({ connections }) {
148 await this.loadDependencies(['oors.autoloader']);
149
150 await Promise.all(connections.map(this.createConnection));
151
152 if (this.getConfig('migration.isEnabled')) {
153 this.createMigrator();
154 }
155
156 await this.loadFromModules();
157
158 if (this.getConfig('seeding.isEnabled')) {
159 await this.setupSeeding();
160 }
161
162 this.gqlQueryParser = new GQLQueryParser(this);
163
164 this.onModule('oors.graphql', 'healthCheck', async () => {
165 await Promise.all(
166 Object.keys(this.connections).map(async name => {
167 const isConnected = await this.connections[name].isConnected();
168 if (!isConnected) {
169 throw new Error(`Connection closed - "${name}"!`);
170 }
171 }),
172 );
173 });
174
175 this.exportProperties([
176 'createConnection',
177 'closeConnection',
178 'getConnection',
179 'getConnectionDb',
180 'toObjectId',
181 'gqlQueryParser',
182 'transaction',
183 'backup',
184 'repositories',
185 'createRepository',
186 'getRepository',
187 'addRepository',
188 'bindRepository',
189 'relations',
190 'addRelation',
191 'relationToLookup',
192 ]);
193
194 this.export({
195 configureRelations: configure =>
196 configure({
197 add: this.addRelation,
198 relations: this.relations,
199 RELATION_TYPE: this.constructor.RELATION_TYPE,
200 getRepository: this.getRepository,
201 }),
202 });
203 }
204
205 teardown = () =>
206 Promise.all(
207 Object.keys(this.connections).map(connectionName => this.closeConnection(connectionName)),
208 );
209
210 loadFromModules = async () => {
211 await Promise.all([
212 this.runHook('loadRepositories', this.loadRepositoriesFromModule, {
213 createRepository: this.createRepository,
214 bindRepositories: this.bindRepository,
215 bindRepository: this.bindRepository,
216 }),
217 this.getConfig('migration.isEnabled')
218 ? this.runHook('loadMigrations', this.loadMigrationsFromModule, {
219 migrator: this.migrator,
220 })
221 : Promise.resolve(),
222 ]);
223
224 this.configureRepositories();
225 };
226
227 getModuleConfig = module =>
228 merge({}, this.getConfig('moduleDefaultConfig'), module.getConfig(this.name));
229
230 loadRepositoriesFromModule = async module => {
231 const config = this.getModuleConfig(module);
232
233 if (!config.repositories.autoload) {
234 return;
235 }
236
237 const { glob } = this.deps['oors.autoloader'].wrap(module);
238 const files = await glob(`${config.repositories.dir}/*.js`, {
239 nodir: true,
240 });
241
242 files.forEach(file => {
243 const ModuleRepository = require(file).default; // eslint-disable-line global-require, import/no-dynamic-require
244 const repository = new ModuleRepository();
245 if (config.repositories.collectionPrefix) {
246 repository.collectionName = `${config.repositories.collectionPrefix}${repository.collectionName}`;
247 }
248 repository.module = module;
249 const name = `${config.repositories.prefix || module.name}.${repository.name ||
250 repository.constructor.name}`;
251
252 this.addRepository(name, repository);
253
254 module.export(`repositories.${repository.name || repository.constructor.name}`, repository);
255 });
256 };
257
258 loadMigrationsFromModule = async module => {
259 const config = this.getModuleConfig(module);
260
261 if (!config.migrations.autoload) {
262 return;
263 }
264
265 const { glob } = this.deps['oors.autoloader'].wrap(module);
266 const files = await glob(`${config.migrations.dir}/*.js`, {
267 nodir: true,
268 });
269
270 this.migrator.files.push(...files);
271 };
272
273 createMigrator() {
274 const migrationRepository = this.addRepository('Migration', new MigrationRepository());
275
276 this.migrator = new Migrator({
277 context: {
278 modules: this.manager,
279 db: this.getConnectionDb(),
280 },
281 MigrationRepository: migrationRepository,
282 transaction: this.transaction,
283 backup: this.backup,
284 getRepository: this.getRepository,
285 silent: this.getConfig('migration.isSilent'),
286 });
287
288 this.export({
289 migrator: this.migrator,
290 migrate: this.migrator.run,
291 });
292 }
293
294 async setupSeeding() {
295 const seeder = new Seeder();
296 const seeds = {};
297
298 await Promise.all([
299 this.runHook('configureSeeder', () => {}, {
300 seeder,
301 getRepository: this.getRepository,
302 }),
303 this.runHook('loadSeedData', () => {}, {
304 seeds,
305 }),
306 ]);
307
308 if (Object.keys(seeds).length) {
309 await this.seed(seeds);
310 }
311
312 this.export({
313 seeder,
314 seed: seeder.load,
315 });
316 }
317
318 createConnection = async ({ name, url, options }) => {
319 this.connections[name] = await MongoClient.connect(url, {
320 ignoreUndefined: true,
321 ...options,
322 useNewUrlParser: true,
323 });
324 return this.connections[name];
325 };
326
327 getConnectionDb = (name = this.defaultConnectionName) => {
328 const connection = this.getConnection(name);
329 const { database, url } = this.getConfig('connections').find(
330 ({ name: _name }) => _name === name,
331 );
332 return connection.db(database || url.substr(url.lastIndexOf('/') + 1));
333 };
334
335 getConnection = name => {
336 if (!name) {
337 return this.connections[this.defaultConnectionName];
338 }
339
340 if (!this.connections[name]) {
341 throw new Error(`Unknown connection name - "${name}"!`);
342 }
343
344 return this.connections[name];
345 };
346
347 closeConnection = name => this.getConnection(name).close();
348
349 toObjectId = value => new ObjectID(value);
350
351 transaction = async (fn, options = {}, connectionName) => {
352 const db = this.getConnectionDb(connectionName);
353
354 if (!this.getConfig('transaction.isEnabled')) {
355 return fn(db, this);
356 }
357
358 return db.startSession(options).withTransaction(async () => fn(db, this));
359 };
360
361 // eslint-disable-next-line
362 backup = connectionName => {
363 // https://github.com/theycallmeswift/node-mongodb-s3-backup
364 // https://dzone.com/articles/auto-backup-mongodb-database-with-nodejs-on-server-1
365 };
366
367 extractCollectionName = relationNode =>
368 relationNode.collectionName ||
369 (relationNode.repositoryName &&
370 this.getRepository(relationNode.repositoryName).collectionName) ||
371 (relationNode.repository && relationNode.repository.collectionName);
372
373 addRelation = ({ type, inversedType, ...args }) => {
374 const from = {
375 ...args.from,
376 collectionName: this.extractCollectionName(args.from),
377 };
378 const to = {
379 ...args.to,
380 collectionName: this.extractCollectionName(args.to),
381 };
382
383 if (!this.relations[from.collectionName]) {
384 this.relations[from.collectionName] = {};
385 }
386
387 this.relations[from.collectionName][from.name] = {
388 collectionName: to.collectionName,
389 localField: from.field,
390 foreignField: to.field,
391 type,
392 };
393
394 if (inversedType && to.name) {
395 this.addRelation({
396 from: to,
397 to: from,
398 type: inversedType,
399 });
400 }
401 };
402
403 relationToLookup = (collectionName, name) => ({
404 from: this.relations[collectionName][name].collectionName,
405 localField: this.relations[collectionName][name].localField,
406 foreignField: this.relations[collectionName][name].foreignField,
407 as: name,
408 });
409
410 createRepository = ({ methods = {}, connectionName, ...options }) => {
411 const repository = new Repository(options);
412
413 Object.keys(methods).forEach(methodName => {
414 repository[methodName] = methods[methodName].bind(repository);
415 });
416
417 this.bindRepository(repository, connectionName);
418
419 return repository;
420 };
421
422 bindRepository = (repository, connectionName) => {
423 if (Array.isArray(repository)) {
424 return repository.map(repo => this.bind(repo, connectionName));
425 }
426
427 invariant(
428 repository.collectionName,
429 `Missing repository collection name - ${repository.constructor.name}!`,
430 );
431
432 Object.assign(repository, {
433 collection: !repository.hasCollection()
434 ? this.getConnectionDb(connectionName).collection(repository.collectionName)
435 : repository.collection,
436 getRepository: this.getRepository,
437 relationToLookup: (name, options = {}) => ({
438 ...this.relationToLookup(repository.collectionName, name),
439 ...options,
440 }),
441 getRelation: name => this.relations[repository.collectionName][name],
442 hasRelation: name => this.relations[repository.collectionName][name] !== undefined,
443 });
444
445 return repository;
446 };
447
448 addRepository = (key, repository, options = {}) => {
449 const payload = {
450 key,
451 repository,
452 options,
453 };
454
455 this.emit('repository', payload);
456
457 this.repositories[payload.key] = this.bindRepository(
458 payload.repository,
459 options.connectionName,
460 );
461
462 return this.repositories[payload.key];
463 };
464
465 addRepositories = repositories =>
466 Object.keys(repositories).reduce(
467 (acc, repositoryName) => ({
468 ...acc,
469 [repositoryName]: this.addRepository(repositoryName, repositories[repositoryName]),
470 }),
471 {},
472 );
473
474 getRepository = key => {
475 if (!this.repositories[key]) {
476 throw new Error(`Unable to find "${key}" repository!`);
477 }
478
479 return this.repositories[key];
480 };
481
482 configureRepositories = (repositories = this.repositories) => {
483 Object.keys(repositories).forEach(key => {
484 const repository = repositories[key];
485
486 repository.configure({
487 getRepository: this.getRepository,
488 });
489
490 Object.keys(repository.relations).forEach(relationName => {
491 const { localField, foreignField, type, ...restOptions } = repository.relations[
492 relationName
493 ];
494
495 this.addRelation({
496 from: {
497 repository,
498 field: localField,
499 name: relationName,
500 },
501 to: {
502 ...restOptions,
503 field: foreignField,
504 },
505 type,
506 });
507 });
508 });
509 };
510}
511
512export { MongoDB as default, Repository, helpers, decorators, Migration };