mongodb-adapter.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825
  1. /* eslint no-unused-vars: 0 */
  2. import {ObjectId} from 'mongodb';
  3. import {MongoClient} from 'mongodb';
  4. import {isObjectId} from './utils/index.js';
  5. import {Adapter} from '@e22m4u/js-repository';
  6. import {DataType} from '@e22m4u/js-repository';
  7. import {isIsoDate} from './utils/is-iso-date.js';
  8. import {capitalize} from '@e22m4u/js-repository';
  9. import {createMongodbUrl} from './utils/index.js';
  10. import {ServiceContainer} from '@e22m4u/js-service';
  11. import {transformValuesDeep} from './utils/index.js';
  12. import {stringToRegexp} from '@e22m4u/js-repository';
  13. import {selectObjectKeys} from '@e22m4u/js-repository';
  14. import {ModelDefinitionUtils} from '@e22m4u/js-repository';
  15. import {InvalidArgumentError} from '@e22m4u/js-repository';
  16. import {InvalidOperatorValueError} from '@e22m4u/js-repository';
  17. /**
  18. * Mongodb option names.
  19. * 5.8.1
  20. *
  21. * @type {string[]}
  22. */
  23. const MONGODB_OPTION_NAMES = [
  24. 'appname',
  25. 'authMechanism',
  26. 'authMechanismProperties',
  27. 'authSource',
  28. 'compressors',
  29. 'connectTimeoutMS',
  30. 'directConnection',
  31. 'heartbeatFrequencyMS',
  32. 'journal',
  33. 'loadBalanced',
  34. 'localThresholdMS',
  35. 'maxIdleTimeMS',
  36. 'maxPoolSize',
  37. 'maxConnecting',
  38. 'maxStalenessSeconds',
  39. 'minPoolSize',
  40. 'proxyHost',
  41. 'proxyPort',
  42. 'proxyUsername',
  43. 'proxyPassword',
  44. 'readConcernLevel',
  45. 'readPreference',
  46. 'readPreferenceTags',
  47. 'replicaSet',
  48. 'retryReads',
  49. 'retryWrites',
  50. 'serverSelectionTimeoutMS',
  51. 'serverSelectionTryOnce',
  52. 'socketTimeoutMS',
  53. 'srvMaxHosts',
  54. 'srvServiceName',
  55. 'ssl',
  56. 'timeoutMS',
  57. 'tls',
  58. 'tlsAllowInvalidCertificates',
  59. 'tlsAllowInvalidHostnames',
  60. 'tlsCAFile',
  61. 'tlsCertificateKeyFile',
  62. 'tlsCertificateKeyFilePassword',
  63. 'tlsInsecure',
  64. 'w',
  65. 'waitQueueTimeoutMS',
  66. 'wTimeoutMS',
  67. 'zlibCompressionLevel',
  68. ];
  69. /**
  70. * Default settings.
  71. *
  72. * @type {{connectTimeoutMS: number}}
  73. */
  74. const DEFAULT_SETTINGS = {
  75. reconnectInterval: 2000, // adapter specific option
  76. connectTimeoutMS: 2000,
  77. serverSelectionTimeoutMS: 2000,
  78. };
  79. /**
  80. * Mongodb adapter.
  81. */
  82. export class MongodbAdapter extends Adapter {
  83. /**
  84. * Mongodb instance.
  85. *
  86. * @private
  87. */
  88. _client;
  89. /**
  90. * Collections.
  91. *
  92. * @type {Map<any, any>}
  93. * @private
  94. */
  95. _collections = new Map();
  96. /**
  97. * Connected.
  98. *
  99. * @type {boolean}
  100. * @private
  101. */
  102. _connected = false;
  103. /**
  104. * Connected.
  105. *
  106. * @return {boolean}
  107. */
  108. get connected() {
  109. return this._connected;
  110. }
  111. /**
  112. * Connecting.
  113. *
  114. * @type {boolean}
  115. * @private
  116. */
  117. _connecting = false;
  118. /**
  119. * Connecting.
  120. *
  121. * @return {boolean}
  122. */
  123. get connecting() {
  124. return this._connecting;
  125. }
  126. /**
  127. * Constructor.
  128. *
  129. * @param {ServiceContainer} container
  130. * @param settings
  131. */
  132. constructor(container, settings) {
  133. settings = Object.assign({}, DEFAULT_SETTINGS, settings || {});
  134. super(container, settings);
  135. settings.protocol = settings.protocol || 'mongodb';
  136. settings.hostname = settings.hostname || settings.host || '127.0.0.1';
  137. settings.port = settings.port || 27017;
  138. settings.database = settings.database || settings.db || 'test';
  139. }
  140. /**
  141. * Connect.
  142. *
  143. * @return {Promise<*|undefined>}
  144. * @private
  145. */
  146. async connect() {
  147. if (this._connecting) {
  148. const tryAgainAfter = 500;
  149. await new Promise(r => setTimeout(() => r(), tryAgainAfter));
  150. return this.connect();
  151. }
  152. if (this._connected) return;
  153. this._connecting = true;
  154. const options = selectObjectKeys(this.settings, MONGODB_OPTION_NAMES);
  155. const url = createMongodbUrl(this.settings);
  156. // console.log(`Connecting to ${url}`);
  157. this._client = new MongoClient(url, options);
  158. const {reconnectInterval} = this.settings;
  159. const connectFn = async () => {
  160. try {
  161. await this._client.connect();
  162. } catch (e) {
  163. console.error(e);
  164. // console.log('MongoDB connection failed!');
  165. // console.log(`Reconnecting after ${reconnectInterval} ms.`);
  166. await new Promise(r => setTimeout(() => r(), reconnectInterval));
  167. return connectFn();
  168. }
  169. // console.log('MongoDB is connected.');
  170. this._connected = true;
  171. this._connecting = false;
  172. };
  173. await connectFn();
  174. this._client.once('serverClosed', event => {
  175. if (this._connected) {
  176. this._connected = false;
  177. // console.log('MongoDB lost connection!');
  178. console.log(event);
  179. // console.log(`Reconnecting after ${reconnectInterval} ms.`);
  180. setTimeout(() => connectFn(), reconnectInterval);
  181. } else {
  182. // console.log('MongoDB connection closed.');
  183. }
  184. });
  185. }
  186. /**
  187. * Disconnect.
  188. *
  189. * @return {Promise<*|undefined>}
  190. */
  191. async disconnect() {
  192. if (this._connecting) {
  193. const tryAgainAfter = 500;
  194. await new Promise(r => setTimeout(() => r(), tryAgainAfter));
  195. return this.disconnect();
  196. }
  197. if (!this._connected) return;
  198. this._connected = false;
  199. if (this._client) await this._client.close();
  200. }
  201. /**
  202. * Get id prop name.
  203. *
  204. * @param modelName
  205. */
  206. _getIdPropName(modelName) {
  207. return this.getService(ModelDefinitionUtils).getPrimaryKeyAsPropertyName(
  208. modelName,
  209. );
  210. }
  211. /**
  212. * Get id col name.
  213. *
  214. * @param modelName
  215. */
  216. _getIdColName(modelName) {
  217. return this.getService(ModelDefinitionUtils).getPrimaryKeyAsColumnName(
  218. modelName,
  219. );
  220. }
  221. /**
  222. * Coerce id.
  223. *
  224. * @param value
  225. * @return {ObjectId|*}
  226. * @private
  227. */
  228. _coerceId(value) {
  229. if (value == null) return value;
  230. if (isObjectId(value)) return new ObjectId(value);
  231. return value;
  232. }
  233. /**
  234. * Coerce iso date.
  235. *
  236. * @param value
  237. * @return {*|Date}
  238. * @private
  239. */
  240. _coerceIsoDate(value) {
  241. if (value === null) return value;
  242. if (isIsoDate(value)) return new Date(value);
  243. return value;
  244. }
  245. /**
  246. * To database.
  247. *
  248. * @param {string} modelName
  249. * @param {object} modelData
  250. * @return {object}
  251. * @private
  252. */
  253. _toDatabase(modelName, modelData) {
  254. const tableData = this.getService(
  255. ModelDefinitionUtils,
  256. ).convertPropertyNamesToColumnNames(modelName, modelData);
  257. const idColName = this._getIdColName(modelName);
  258. if (idColName !== 'id' && idColName !== '_id')
  259. throw new InvalidArgumentError(
  260. 'MongoDB is not supporting custom names of the primary key. ' +
  261. 'Do use "id" as a primary key instead of %v.',
  262. idColName,
  263. );
  264. if (idColName in tableData && idColName !== '_id') {
  265. tableData._id = tableData[idColName];
  266. delete tableData[idColName];
  267. }
  268. return transformValuesDeep(tableData, value => {
  269. if (value instanceof ObjectId) return value;
  270. if (value instanceof Date) return value;
  271. if (isObjectId(value)) return new ObjectId(value);
  272. if (isIsoDate(value)) return new Date(value);
  273. return value;
  274. });
  275. }
  276. /**
  277. * From database.
  278. *
  279. * @param {string} modelName
  280. * @param {object} tableData
  281. * @return {object}
  282. * @private
  283. */
  284. _fromDatabase(modelName, tableData) {
  285. if ('_id' in tableData) {
  286. const idColName = this._getIdColName(modelName);
  287. if (idColName !== 'id' && idColName !== '_id')
  288. throw new InvalidArgumentError(
  289. 'MongoDB is not supporting custom names of the primary key. ' +
  290. 'Do use "id" as a primary key instead of %v.',
  291. idColName,
  292. );
  293. if (idColName !== '_id') {
  294. tableData[idColName] = tableData._id;
  295. delete tableData._id;
  296. }
  297. }
  298. const modelData = this.getService(
  299. ModelDefinitionUtils,
  300. ).convertColumnNamesToPropertyNames(modelName, tableData);
  301. return transformValuesDeep(modelData, value => {
  302. if (value instanceof ObjectId) return String(value);
  303. if (value instanceof Date) return value.toISOString();
  304. return value;
  305. });
  306. }
  307. /**
  308. * Get collection.
  309. *
  310. * @param {string} modelName
  311. * @return {*}
  312. * @private
  313. */
  314. _getCollection(modelName) {
  315. let collection = this._collections.get(modelName);
  316. if (collection) return collection;
  317. const tableName =
  318. this.getService(ModelDefinitionUtils).getTableNameByModelName(modelName);
  319. collection = this._client.db(this.settings.database).collection(tableName);
  320. this._collections.set(modelName, collection);
  321. return collection;
  322. }
  323. /**
  324. * Get id type.
  325. *
  326. * @param modelName
  327. * @return {string|*}
  328. * @private
  329. */
  330. _getIdType(modelName) {
  331. const utils = this.getService(ModelDefinitionUtils);
  332. const pkPropName = utils.getPrimaryKeyAsPropertyName(modelName);
  333. return utils.getDataTypeByPropertyName(modelName, pkPropName);
  334. }
  335. /**
  336. * Build projection.
  337. *
  338. * @param {string} modelName
  339. * @param {string|string[]} fields
  340. * @return {Record<string, number>|undefined}
  341. * @private
  342. */
  343. _buildProjection(modelName, fields) {
  344. if (!fields) return;
  345. fields = Array.isArray(fields) ? fields : [fields];
  346. if (!fields.length) return;
  347. if (fields.indexOf('_id') === -1) fields.push('_id');
  348. return fields.reduce((acc, field) => {
  349. if (!field || typeof field !== 'string')
  350. throw new InvalidArgumentError(
  351. 'A field name must be a non-empty String, but %v given.',
  352. field,
  353. );
  354. let colName = this._getColName(modelName, field);
  355. acc[colName] = 1;
  356. return acc;
  357. }, {});
  358. }
  359. /**
  360. * Get col name.
  361. *
  362. * @param {string} modelName
  363. * @param {string} propName
  364. * @return {string}
  365. * @private
  366. */
  367. _getColName(modelName, propName) {
  368. if (!propName || typeof propName !== 'string')
  369. throw new InvalidArgumentError(
  370. 'A property name must be a non-empty String, but %v given.',
  371. propName,
  372. );
  373. const utils = this.getService(ModelDefinitionUtils);
  374. let colName = propName;
  375. try {
  376. colName = utils.getColumnNameByPropertyName(modelName, propName);
  377. } catch (error) {
  378. if (
  379. !(error instanceof InvalidArgumentError) ||
  380. error.message.indexOf('does not have the property') === -1
  381. ) {
  382. throw error;
  383. }
  384. }
  385. return colName;
  386. }
  387. /**
  388. * Build sort.
  389. *
  390. * @param {string} modelName
  391. * @param {string|string[]} clause
  392. * @return {object|undefined}
  393. * @private
  394. */
  395. _buildSort(modelName, clause) {
  396. if (!clause) return;
  397. clause = Array.isArray(clause) ? clause : [clause];
  398. if (!clause.length) return;
  399. const utils = this.getService(ModelDefinitionUtils);
  400. const idPropName = this._getIdPropName(modelName);
  401. return clause.reduce((acc, order) => {
  402. if (!order || typeof order !== 'string')
  403. throw new InvalidArgumentError(
  404. 'A field order must be a non-empty String, but %v given.',
  405. order,
  406. );
  407. const direction = order.match(/\s+(A|DE)SC$/);
  408. let key = order.replace(/\s+(A|DE)SC$/, '').trim();
  409. if (key === idPropName) {
  410. key = '_id';
  411. } else {
  412. try {
  413. key = utils.getColumnNameByPropertyName(modelName, key);
  414. } catch (error) {
  415. if (
  416. !(error instanceof InvalidArgumentError) ||
  417. error.message.indexOf('does not have the property') === -1
  418. ) {
  419. throw error;
  420. }
  421. }
  422. }
  423. acc[key] = direction && direction[1] === 'DE' ? -1 : 1;
  424. return acc;
  425. }, {});
  426. }
  427. /**
  428. * Build query.
  429. *
  430. * @param {string} modelName
  431. * @param {object} clause
  432. * @return {object}
  433. * @private
  434. */
  435. _buildQuery(modelName, clause) {
  436. const query = {};
  437. if (!clause || typeof clause !== 'object') return query;
  438. const idPropName = this._getIdPropName(modelName);
  439. Object.keys(clause).forEach(key => {
  440. let cond = clause[key];
  441. // and/or/nor clause
  442. if (key === 'and' || key === 'or' || key === 'nor') {
  443. if (Array.isArray(cond))
  444. cond = cond.map(c => this._buildQuery(modelName, c));
  445. query['$' + key] = cond;
  446. return;
  447. }
  448. // id
  449. if (key === idPropName) {
  450. key = '_id';
  451. } else {
  452. key = this._getColName(modelName, key);
  453. }
  454. // string
  455. if (typeof cond === 'string') {
  456. query[key] = this._coerceId(cond);
  457. return;
  458. }
  459. // ObjectId
  460. if (cond instanceof ObjectId) {
  461. query[key] = cond;
  462. return;
  463. }
  464. // operator
  465. if (cond && cond.constructor && cond.constructor.name === 'Object') {
  466. // eq
  467. if ('eq' in cond) {
  468. query[key] = this._coerceId(cond.eq);
  469. }
  470. // neq
  471. if ('neq' in cond) {
  472. query[key] = {$ne: this._coerceId(cond.neq)};
  473. }
  474. // gt
  475. if ('gt' in cond) {
  476. query[key] = {$gt: cond.gt};
  477. }
  478. // lt
  479. if ('lt' in cond) {
  480. query[key] = {$lt: cond.lt};
  481. }
  482. // gte
  483. if ('gte' in cond) {
  484. query[key] = {$gte: cond.gte};
  485. }
  486. // lte
  487. if ('lte' in cond) {
  488. query[key] = {$lte: cond.lte};
  489. }
  490. // inq
  491. if ('inq' in cond) {
  492. if (!cond.inq || !Array.isArray(cond.inq))
  493. throw new InvalidOperatorValueError(
  494. 'inq',
  495. 'an Array of possible values',
  496. cond.inq,
  497. );
  498. query[key] = {$in: cond.inq.map(v => this._coerceId(v))};
  499. }
  500. // nin
  501. if ('nin' in cond) {
  502. if (!cond.nin || !Array.isArray(cond.nin))
  503. throw new InvalidOperatorValueError(
  504. 'nin',
  505. 'an Array of possible values',
  506. cond,
  507. );
  508. query[key] = {$nin: cond.nin.map(v => this._coerceId(v))};
  509. }
  510. // between
  511. if ('between' in cond) {
  512. if (!Array.isArray(cond.between) || cond.between.length !== 2)
  513. throw new InvalidOperatorValueError(
  514. 'between',
  515. 'an Array of 2 elements',
  516. cond.between,
  517. );
  518. query[key] = {$gte: cond.between[0], $lte: cond.between[1]};
  519. }
  520. // exists
  521. if ('exists' in cond) {
  522. if (typeof cond.exists !== 'boolean')
  523. throw new InvalidOperatorValueError(
  524. 'exists',
  525. 'a Boolean',
  526. cond.exists,
  527. );
  528. query[key] = {$exists: cond.exists};
  529. }
  530. // like
  531. if ('like' in cond) {
  532. if (typeof cond.like !== 'string' && !(cond.like instanceof RegExp))
  533. throw new InvalidOperatorValueError(
  534. 'like',
  535. 'a String or RegExp',
  536. cond.like,
  537. );
  538. query[key] = {$regex: stringToRegexp(cond.like)};
  539. }
  540. // nlike
  541. if ('nlike' in cond) {
  542. if (typeof cond.nlike !== 'string' && !(cond.nlike instanceof RegExp))
  543. throw new InvalidOperatorValueError(
  544. 'nlike',
  545. 'a String or RegExp',
  546. cond.nlike,
  547. );
  548. query[key] = {$not: stringToRegexp(cond.nlike)};
  549. }
  550. // ilike
  551. if ('ilike' in cond) {
  552. if (typeof cond.ilike !== 'string' && !(cond.ilike instanceof RegExp))
  553. throw new InvalidOperatorValueError(
  554. 'ilike',
  555. 'a String or RegExp',
  556. cond.ilike,
  557. );
  558. query[key] = {$regex: stringToRegexp(cond.ilike, 'i')};
  559. }
  560. // nilike
  561. if ('nilike' in cond) {
  562. if (
  563. typeof cond.nilike !== 'string' &&
  564. !(cond.nilike instanceof RegExp)
  565. ) {
  566. throw new InvalidOperatorValueError(
  567. 'nilike',
  568. 'a String or RegExp',
  569. cond.nilike,
  570. );
  571. }
  572. query[key] = {$not: stringToRegexp(cond.nilike, 'i')};
  573. }
  574. // regexp and flags (optional)
  575. if ('regexp' in cond) {
  576. if (
  577. typeof cond.regexp !== 'string' &&
  578. !(cond.regexp instanceof RegExp)
  579. ) {
  580. throw new InvalidOperatorValueError(
  581. 'regexp',
  582. 'a String or RegExp',
  583. cond.regexp,
  584. );
  585. }
  586. const flags = cond.flags || undefined;
  587. if (flags && typeof flags !== 'string')
  588. throw new InvalidArgumentError(
  589. 'RegExp flags must be a String, but %v given.',
  590. cond.flags,
  591. );
  592. query[key] = {$regex: stringToRegexp(cond.regexp, flags)};
  593. }
  594. return;
  595. }
  596. // unknown
  597. query[key] = cond;
  598. });
  599. return query;
  600. }
  601. /**
  602. * Create.
  603. *
  604. * @param {string} modelName
  605. * @param {object} modelData
  606. * @param {object|undefined} filter
  607. * @return {Promise<object>}
  608. */
  609. async create(modelName, modelData, filter = undefined) {
  610. await this.connect();
  611. const idPropName = this._getIdPropName(modelName);
  612. const idValue = modelData[idPropName];
  613. if (idValue == null) {
  614. const pkType = this._getIdType(modelName);
  615. if (pkType !== DataType.STRING && pkType !== DataType.ANY)
  616. throw new InvalidArgumentError(
  617. 'MongoDB unable to generate primary keys of %s. ' +
  618. 'Do provide your own value for the %v property ' +
  619. 'or set property type to String.',
  620. capitalize(pkType),
  621. idPropName,
  622. );
  623. delete modelData[idPropName];
  624. }
  625. const tableData = this._toDatabase(modelName, modelData);
  626. const table = this._getCollection(modelName);
  627. const {insertedId} = await table.insertOne(tableData);
  628. const projection = this._buildProjection(
  629. modelName,
  630. filter && filter.fields,
  631. );
  632. const insertedData = await table.findOne({_id: insertedId}, {projection});
  633. return this._fromDatabase(modelName, insertedData);
  634. }
  635. /**
  636. * Replace by id.
  637. *
  638. * @param {string} modelName
  639. * @param {string|number} id
  640. * @param {object} modelData
  641. * @param {object|undefined} filter
  642. * @return {Promise<object>}
  643. */
  644. async replaceById(modelName, id, modelData, filter = undefined) {
  645. await this.connect();
  646. id = this._coerceId(id);
  647. const idPropName = this._getIdPropName(modelName);
  648. modelData[idPropName] = id;
  649. const tableData = this._toDatabase(modelName, modelData);
  650. const table = this._getCollection(modelName);
  651. const {modifiedCount} = await table.replaceOne({_id: id}, tableData);
  652. if (modifiedCount < 1)
  653. throw new InvalidArgumentError('Identifier %v is not found.', String(id));
  654. const projection = this._buildProjection(
  655. modelName,
  656. filter && filter.fields,
  657. );
  658. const replacedData = await table.findOne({_id: id}, {projection});
  659. return this._fromDatabase(modelName, replacedData);
  660. }
  661. /**
  662. * Patch by id.
  663. *
  664. * @param {string} modelName
  665. * @param {string|number} id
  666. * @param {object} modelData
  667. * @param {object|undefined} filter
  668. * @return {Promise<object>}
  669. */
  670. async patchById(modelName, id, modelData, filter = undefined) {
  671. await this.connect();
  672. id = this._coerceId(id);
  673. const idPropName = this._getIdPropName(modelName);
  674. delete modelData[idPropName];
  675. const tableData = this._toDatabase(modelName, modelData);
  676. const table = this._getCollection(modelName);
  677. const {modifiedCount} = await table.updateOne({_id: id}, {$set: tableData});
  678. if (modifiedCount < 1)
  679. throw new InvalidArgumentError('Identifier %v is not found.', String(id));
  680. const projection = this._buildProjection(
  681. modelName,
  682. filter && filter.fields,
  683. );
  684. const patchedData = await table.findOne({_id: id}, {projection});
  685. return this._fromDatabase(modelName, patchedData);
  686. }
  687. /**
  688. * Find.
  689. *
  690. * @param {string} modelName
  691. * @param {object|undefined} filter
  692. * @return {Promise<object[]>}
  693. */
  694. async find(modelName, filter = undefined) {
  695. await this.connect();
  696. filter = filter || {};
  697. const query = this._buildQuery(modelName, filter.where);
  698. const sort = this._buildSort(modelName, filter.order);
  699. const limit = filter.limit || undefined;
  700. const skip = filter.skip || undefined;
  701. const projection = this._buildProjection(modelName, filter.fields);
  702. const collection = this._getCollection(modelName);
  703. const options = {sort, limit, skip, projection};
  704. const tableItems = await collection.find(query, options).toArray();
  705. return tableItems.map(v => this._fromDatabase(modelName, v));
  706. }
  707. /**
  708. * Find by id.
  709. *
  710. * @param {string} modelName
  711. * @param {string|number} id
  712. * @param {object|undefined} filter
  713. * @return {Promise<object>}
  714. */
  715. async findById(modelName, id, filter = undefined) {
  716. await this.connect();
  717. id = this._coerceId(id);
  718. const table = this._getCollection(modelName);
  719. const projection = this._buildProjection(
  720. modelName,
  721. filter && filter.fields,
  722. );
  723. const patchedData = await table.findOne({_id: id}, {projection});
  724. if (!patchedData)
  725. throw new InvalidArgumentError('Identifier %v is not found.', String(id));
  726. return this._fromDatabase(modelName, patchedData);
  727. }
  728. /**
  729. * Delete.
  730. *
  731. * @param {string} modelName
  732. * @param {object|undefined} where
  733. * @return {Promise<number>}
  734. */
  735. async delete(modelName, where = undefined) {
  736. await this.connect();
  737. const table = this._getCollection(modelName);
  738. const query = this._buildQuery(modelName, where);
  739. const {deletedCount} = await table.deleteMany(query);
  740. return deletedCount;
  741. }
  742. /**
  743. * Delete by id.
  744. *
  745. * @param {string} modelName
  746. * @param {string|number} id
  747. * @return {Promise<boolean>}
  748. */
  749. async deleteById(modelName, id) {
  750. await this.connect();
  751. id = this._coerceId(id);
  752. const table = this._getCollection(modelName);
  753. const {deletedCount} = await table.deleteOne({_id: id});
  754. return deletedCount > 0;
  755. }
  756. /**
  757. * Exists.
  758. *
  759. * @param {string} modelName
  760. * @param {string|number} id
  761. * @return {Promise<boolean>}
  762. */
  763. async exists(modelName, id) {
  764. await this.connect();
  765. id = this._coerceId(id);
  766. const table = this._getCollection(modelName);
  767. const result = await table.findOne({_id: id}, {});
  768. return result != null;
  769. }
  770. /**
  771. * Count.
  772. *
  773. * @param {string} modelName
  774. * @param {object|undefined} where
  775. * @return {Promise<number>}
  776. */
  777. async count(modelName, where = undefined) {
  778. await this.connect();
  779. const query = this._buildQuery(modelName, where);
  780. const table = this._getCollection(modelName);
  781. return await table.count(query);
  782. }
  783. }