mongodb-adapter.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739
  1. /* eslint no-unused-vars: 0 */
  2. import {ObjectId} from 'mongodb';
  3. import {MongoClient} from 'mongodb';
  4. import {isIsoDate} from './utils/index.js';
  5. import {isObjectId} from './utils/index.js';
  6. import {Adapter} from '@e22m4u/js-repository';
  7. import {DataType} from '@e22m4u/js-repository';
  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 {object}
  73. */
  74. const DEFAULT_SETTINGS = {
  75. // connectTimeoutMS: 2500,
  76. // serverSelectionTimeoutMS: 2500,
  77. };
  78. /**
  79. * Mongodb adapter.
  80. */
  81. export class MongodbAdapter extends Adapter {
  82. /**
  83. * Mongodb instance.
  84. *
  85. * @type {MongoClient}
  86. * @private
  87. */
  88. _client;
  89. /**
  90. * Client.
  91. *
  92. * @returns {MongoClient}
  93. */
  94. get client() {
  95. return this._client;
  96. }
  97. /**
  98. * Collections.
  99. *
  100. * @type {Map<any, any>}
  101. * @private
  102. */
  103. _collections = new Map();
  104. /**
  105. * Constructor.
  106. *
  107. * @param {ServiceContainer} container
  108. * @param settings
  109. */
  110. constructor(container, settings) {
  111. settings = Object.assign({}, DEFAULT_SETTINGS, settings || {});
  112. settings.protocol = settings.protocol || 'mongodb';
  113. settings.hostname = settings.hostname || settings.host || '127.0.0.1';
  114. settings.port = settings.port || 27017;
  115. settings.database = settings.database || settings.db || 'database';
  116. super(container, settings);
  117. const options = selectObjectKeys(this.settings, MONGODB_OPTION_NAMES);
  118. const url = createMongodbUrl(this.settings);
  119. this._client = new MongoClient(url, options);
  120. }
  121. /**
  122. * Get id prop name.
  123. *
  124. * @param modelName
  125. */
  126. _getIdPropName(modelName) {
  127. return this.getService(ModelDefinitionUtils).getPrimaryKeyAsPropertyName(
  128. modelName,
  129. );
  130. }
  131. /**
  132. * Get id col name.
  133. *
  134. * @param modelName
  135. */
  136. _getIdColName(modelName) {
  137. return this.getService(ModelDefinitionUtils).getPrimaryKeyAsColumnName(
  138. modelName,
  139. );
  140. }
  141. /**
  142. * Coerce id.
  143. *
  144. * @param value
  145. * @return {ObjectId|*}
  146. * @private
  147. */
  148. _coerceId(value) {
  149. if (value == null) return value;
  150. if (isObjectId(value)) return new ObjectId(value);
  151. return value;
  152. }
  153. /**
  154. * To database.
  155. *
  156. * @param {string} modelName
  157. * @param {object} modelData
  158. * @return {object}
  159. * @private
  160. */
  161. _toDatabase(modelName, modelData) {
  162. const tableData = this.getService(
  163. ModelDefinitionUtils,
  164. ).convertPropertyNamesToColumnNames(modelName, modelData);
  165. const idColName = this._getIdColName(modelName);
  166. if (idColName !== 'id' && idColName !== '_id')
  167. throw new InvalidArgumentError(
  168. 'MongoDB is not supporting custom names of the primary key. ' +
  169. 'Do use "id" as a primary key instead of %v.',
  170. idColName,
  171. );
  172. if (idColName in tableData && idColName !== '_id') {
  173. tableData._id = tableData[idColName];
  174. delete tableData[idColName];
  175. }
  176. return transformValuesDeep(tableData, value => {
  177. if (value instanceof ObjectId) return value;
  178. if (value instanceof Date) return value;
  179. if (isObjectId(value)) return new ObjectId(value);
  180. if (isIsoDate(value)) return new Date(value);
  181. return value;
  182. });
  183. }
  184. /**
  185. * From database.
  186. *
  187. * @param {string} modelName
  188. * @param {object} tableData
  189. * @return {object}
  190. * @private
  191. */
  192. _fromDatabase(modelName, tableData) {
  193. if ('_id' in tableData) {
  194. const idColName = this._getIdColName(modelName);
  195. if (idColName !== 'id' && idColName !== '_id')
  196. throw new InvalidArgumentError(
  197. 'MongoDB is not supporting custom names of the primary key. ' +
  198. 'Do use "id" as a primary key instead of %v.',
  199. idColName,
  200. );
  201. if (idColName !== '_id') {
  202. tableData[idColName] = tableData._id;
  203. delete tableData._id;
  204. }
  205. }
  206. const modelData = this.getService(
  207. ModelDefinitionUtils,
  208. ).convertColumnNamesToPropertyNames(modelName, tableData);
  209. return transformValuesDeep(modelData, value => {
  210. if (value instanceof ObjectId) return String(value);
  211. if (value instanceof Date) return value.toISOString();
  212. return value;
  213. });
  214. }
  215. /**
  216. * Get collection.
  217. *
  218. * @param {string} modelName
  219. * @return {*}
  220. * @private
  221. */
  222. _getCollection(modelName) {
  223. let collection = this._collections.get(modelName);
  224. if (collection) return collection;
  225. const tableName =
  226. this.getService(ModelDefinitionUtils).getTableNameByModelName(modelName);
  227. collection = this.client.db(this.settings.database).collection(tableName);
  228. this._collections.set(modelName, collection);
  229. return collection;
  230. }
  231. /**
  232. * Get id type.
  233. *
  234. * @param modelName
  235. * @return {string|*}
  236. * @private
  237. */
  238. _getIdType(modelName) {
  239. const utils = this.getService(ModelDefinitionUtils);
  240. const pkPropName = utils.getPrimaryKeyAsPropertyName(modelName);
  241. return utils.getDataTypeByPropertyName(modelName, pkPropName);
  242. }
  243. /**
  244. * Build projection.
  245. *
  246. * @param {string} modelName
  247. * @param {string|string[]} fields
  248. * @return {Record<string, number>|undefined}
  249. * @private
  250. */
  251. _buildProjection(modelName, fields) {
  252. if (fields == null) return;
  253. if (Array.isArray(fields) === false) fields = [fields];
  254. if (!fields.length) return;
  255. if (fields.indexOf('_id') === -1) fields.push('_id');
  256. return fields.reduce((acc, field) => {
  257. if (!field || typeof field !== 'string')
  258. throw new InvalidArgumentError(
  259. 'The provided option "fields" should be a non-empty String ' +
  260. 'or an Array of non-empty String, but %v given.',
  261. field,
  262. );
  263. let colName = this._getColName(modelName, field);
  264. acc[colName] = 1;
  265. return acc;
  266. }, {});
  267. }
  268. /**
  269. * Get col name.
  270. *
  271. * @param {string} modelName
  272. * @param {string} propName
  273. * @return {string}
  274. * @private
  275. */
  276. _getColName(modelName, propName) {
  277. if (!propName || typeof propName !== 'string')
  278. throw new InvalidArgumentError(
  279. 'A property name must be a non-empty String, but %v given.',
  280. propName,
  281. );
  282. const utils = this.getService(ModelDefinitionUtils);
  283. let colName = propName;
  284. try {
  285. colName = utils.getColumnNameByPropertyName(modelName, propName);
  286. } catch (error) {
  287. if (
  288. !(error instanceof InvalidArgumentError) ||
  289. error.message.indexOf('does not have the property') === -1
  290. ) {
  291. throw error;
  292. }
  293. }
  294. return colName;
  295. }
  296. /**
  297. * Build sort.
  298. *
  299. * @param {string} modelName
  300. * @param {string|string[]} clause
  301. * @return {object|undefined}
  302. * @private
  303. */
  304. _buildSort(modelName, clause) {
  305. if (clause == null) return;
  306. if (Array.isArray(clause) === false) clause = [clause];
  307. if (!clause.length) return;
  308. const utils = this.getService(ModelDefinitionUtils);
  309. const idPropName = this._getIdPropName(modelName);
  310. return clause.reduce((acc, order) => {
  311. if (!order || typeof order !== 'string')
  312. throw new InvalidArgumentError(
  313. 'The provided option "order" should be a non-empty String ' +
  314. 'or an Array of non-empty String, but %v given.',
  315. order,
  316. );
  317. const direction = order.match(/\s+(A|DE)SC$/);
  318. let field = order.replace(/\s+(A|DE)SC$/, '').trim();
  319. if (field === idPropName) {
  320. field = '_id';
  321. } else {
  322. try {
  323. field = utils.getColumnNameByPropertyName(modelName, field);
  324. } catch (error) {
  325. if (
  326. !(error instanceof InvalidArgumentError) ||
  327. error.message.indexOf('does not have the property') === -1
  328. ) {
  329. throw error;
  330. }
  331. }
  332. }
  333. acc[field] = direction && direction[1] === 'DE' ? -1 : 1;
  334. return acc;
  335. }, {});
  336. }
  337. /**
  338. * Build query.
  339. *
  340. * @param {string} modelName
  341. * @param {object} clause
  342. * @return {object}
  343. * @private
  344. */
  345. _buildQuery(modelName, clause) {
  346. if (clause == null) return;
  347. if (typeof clause !== 'object' || Array.isArray(clause))
  348. throw new InvalidArgumentError(
  349. 'The provided option "where" should be an Object, but %v given.',
  350. clause,
  351. );
  352. const query = {};
  353. const idPropName = this._getIdPropName(modelName);
  354. Object.keys(clause).forEach(key => {
  355. if (String(key).indexOf('$') !== -1)
  356. throw new InvalidArgumentError(
  357. 'The symbol "$" is not supported, but %v given.',
  358. key,
  359. );
  360. let cond = clause[key];
  361. // and/or/nor clause
  362. if (key === 'and' || key === 'or' || key === 'nor') {
  363. if (cond == null) return;
  364. if (!Array.isArray(cond))
  365. throw new InvalidOperatorValueError(key, 'an Array', cond);
  366. if (cond.length === 0) return;
  367. cond = cond.map(c => this._buildQuery(modelName, c));
  368. cond = cond.filter(c => c != null);
  369. const opKey = '$' + key;
  370. query[opKey] = query[opKey] ?? [];
  371. query[opKey] = [...query[opKey], ...cond];
  372. return;
  373. }
  374. // id
  375. if (key === idPropName) {
  376. key = '_id';
  377. } else {
  378. key = this._getColName(modelName, key);
  379. }
  380. // string
  381. if (typeof cond === 'string') {
  382. query[key] = this._coerceId(cond);
  383. return;
  384. }
  385. // ObjectId
  386. if (cond instanceof ObjectId) {
  387. query[key] = cond;
  388. return;
  389. }
  390. // operator
  391. if (cond && cond.constructor && cond.constructor.name === 'Object') {
  392. const opConds = [];
  393. // eq
  394. if ('eq' in cond) {
  395. opConds.push({$eq: this._coerceId(cond.eq)});
  396. }
  397. // neq
  398. if ('neq' in cond) {
  399. opConds.push({$ne: this._coerceId(cond.neq)});
  400. }
  401. // gt
  402. if ('gt' in cond) {
  403. opConds.push({$gt: cond.gt});
  404. }
  405. // lt
  406. if ('lt' in cond) {
  407. opConds.push({$lt: cond.lt});
  408. }
  409. // gte
  410. if ('gte' in cond) {
  411. opConds.push({$gte: cond.gte});
  412. }
  413. // lte
  414. if ('lte' in cond) {
  415. opConds.push({$lte: cond.lte});
  416. }
  417. // inq
  418. if ('inq' in cond) {
  419. if (!cond.inq || !Array.isArray(cond.inq))
  420. throw new InvalidOperatorValueError(
  421. 'inq',
  422. 'an Array of possible values',
  423. cond.inq,
  424. );
  425. opConds.push({$in: cond.inq.map(v => this._coerceId(v))});
  426. }
  427. // nin
  428. if ('nin' in cond) {
  429. if (!cond.nin || !Array.isArray(cond.nin))
  430. throw new InvalidOperatorValueError(
  431. 'nin',
  432. 'an Array of possible values',
  433. cond,
  434. );
  435. opConds.push({$nin: cond.nin.map(v => this._coerceId(v))});
  436. }
  437. // between
  438. if ('between' in cond) {
  439. if (!Array.isArray(cond.between) || cond.between.length !== 2)
  440. throw new InvalidOperatorValueError(
  441. 'between',
  442. 'an Array of 2 elements',
  443. cond.between,
  444. );
  445. opConds.push({$gte: cond.between[0], $lte: cond.between[1]});
  446. }
  447. // exists
  448. if ('exists' in cond) {
  449. if (typeof cond.exists !== 'boolean')
  450. throw new InvalidOperatorValueError(
  451. 'exists',
  452. 'a Boolean',
  453. cond.exists,
  454. );
  455. opConds.push({$exists: cond.exists});
  456. }
  457. // like
  458. if ('like' in cond) {
  459. if (typeof cond.like !== 'string' && !(cond.like instanceof RegExp))
  460. throw new InvalidOperatorValueError(
  461. 'like',
  462. 'a String or RegExp',
  463. cond.like,
  464. );
  465. opConds.push({$regex: stringToRegexp(cond.like)});
  466. }
  467. // nlike
  468. if ('nlike' in cond) {
  469. if (typeof cond.nlike !== 'string' && !(cond.nlike instanceof RegExp))
  470. throw new InvalidOperatorValueError(
  471. 'nlike',
  472. 'a String or RegExp',
  473. cond.nlike,
  474. );
  475. opConds.push({$not: stringToRegexp(cond.nlike)});
  476. }
  477. // ilike
  478. if ('ilike' in cond) {
  479. if (typeof cond.ilike !== 'string' && !(cond.ilike instanceof RegExp))
  480. throw new InvalidOperatorValueError(
  481. 'ilike',
  482. 'a String or RegExp',
  483. cond.ilike,
  484. );
  485. opConds.push({$regex: stringToRegexp(cond.ilike, 'i')});
  486. }
  487. // nilike
  488. if ('nilike' in cond) {
  489. if (
  490. typeof cond.nilike !== 'string' &&
  491. !(cond.nilike instanceof RegExp)
  492. ) {
  493. throw new InvalidOperatorValueError(
  494. 'nilike',
  495. 'a String or RegExp',
  496. cond.nilike,
  497. );
  498. }
  499. opConds.push({$not: stringToRegexp(cond.nilike, 'i')});
  500. }
  501. // regexp and flags (optional)
  502. if ('regexp' in cond) {
  503. if (
  504. typeof cond.regexp !== 'string' &&
  505. !(cond.regexp instanceof RegExp)
  506. ) {
  507. throw new InvalidOperatorValueError(
  508. 'regexp',
  509. 'a String or RegExp',
  510. cond.regexp,
  511. );
  512. }
  513. const flags = cond.flags || undefined;
  514. if (flags && typeof flags !== 'string')
  515. throw new InvalidArgumentError(
  516. 'RegExp flags must be a String, but %v given.',
  517. cond.flags,
  518. );
  519. opConds.push({$regex: stringToRegexp(cond.regexp, flags)});
  520. }
  521. // adds a single operator condition
  522. if (opConds.length === 1) {
  523. query[key] = opConds[0];
  524. // adds multiple operator conditions
  525. } else if (opConds.length > 1) {
  526. query['$and'] = query['$and'] ?? [];
  527. opConds.forEach(c => query['$and'].push({[key]: c}));
  528. }
  529. return;
  530. }
  531. // unknown
  532. query[key] = cond;
  533. });
  534. return Object.keys(query).length ? query : undefined;
  535. }
  536. /**
  537. * Create.
  538. *
  539. * @param {string} modelName
  540. * @param {object} modelData
  541. * @param {object|undefined} filter
  542. * @return {Promise<object>}
  543. */
  544. async create(modelName, modelData, filter = undefined) {
  545. const idPropName = this._getIdPropName(modelName);
  546. const idValue = modelData[idPropName];
  547. if (idValue == null) {
  548. const pkType = this._getIdType(modelName);
  549. if (pkType !== DataType.STRING && pkType !== DataType.ANY)
  550. throw new InvalidArgumentError(
  551. 'MongoDB unable to generate primary keys of %s. ' +
  552. 'Do provide your own value for the %v property ' +
  553. 'or set property type to String.',
  554. capitalize(pkType),
  555. idPropName,
  556. );
  557. delete modelData[idPropName];
  558. }
  559. const tableData = this._toDatabase(modelName, modelData);
  560. const table = this._getCollection(modelName);
  561. const {insertedId} = await table.insertOne(tableData);
  562. const projection = this._buildProjection(
  563. modelName,
  564. filter && filter.fields,
  565. );
  566. const insertedData = await table.findOne({_id: insertedId}, {projection});
  567. return this._fromDatabase(modelName, insertedData);
  568. }
  569. /**
  570. * Replace by id.
  571. *
  572. * @param {string} modelName
  573. * @param {string|number} id
  574. * @param {object} modelData
  575. * @param {object|undefined} filter
  576. * @return {Promise<object>}
  577. */
  578. async replaceById(modelName, id, modelData, filter = undefined) {
  579. id = this._coerceId(id);
  580. const idPropName = this._getIdPropName(modelName);
  581. modelData[idPropName] = id;
  582. const tableData = this._toDatabase(modelName, modelData);
  583. const table = this._getCollection(modelName);
  584. const {matchedCount} = await table.replaceOne({_id: id}, tableData);
  585. if (matchedCount < 1)
  586. throw new InvalidArgumentError('Identifier %v is not found.', String(id));
  587. const projection = this._buildProjection(
  588. modelName,
  589. filter && filter.fields,
  590. );
  591. const replacedData = await table.findOne({_id: id}, {projection});
  592. return this._fromDatabase(modelName, replacedData);
  593. }
  594. /**
  595. * Patch by id.
  596. *
  597. * @param {string} modelName
  598. * @param {string|number} id
  599. * @param {object} modelData
  600. * @param {object|undefined} filter
  601. * @return {Promise<object>}
  602. */
  603. async patchById(modelName, id, modelData, filter = undefined) {
  604. id = this._coerceId(id);
  605. const idPropName = this._getIdPropName(modelName);
  606. delete modelData[idPropName];
  607. const tableData = this._toDatabase(modelName, modelData);
  608. const table = this._getCollection(modelName);
  609. const {matchedCount} = await table.updateOne({_id: id}, {$set: tableData});
  610. if (matchedCount < 1)
  611. throw new InvalidArgumentError('Identifier %v is not found.', String(id));
  612. const projection = this._buildProjection(
  613. modelName,
  614. filter && filter.fields,
  615. );
  616. const patchedData = await table.findOne({_id: id}, {projection});
  617. return this._fromDatabase(modelName, patchedData);
  618. }
  619. /**
  620. * Find.
  621. *
  622. * @param {string} modelName
  623. * @param {object|undefined} filter
  624. * @return {Promise<object[]>}
  625. */
  626. async find(modelName, filter = undefined) {
  627. filter = filter || {};
  628. const query = this._buildQuery(modelName, filter.where);
  629. const sort = this._buildSort(modelName, filter.order);
  630. const limit = filter.limit || undefined;
  631. const skip = filter.skip || undefined;
  632. const projection = this._buildProjection(modelName, filter.fields);
  633. const collection = this._getCollection(modelName);
  634. const options = {sort, limit, skip, projection};
  635. const tableItems = await collection.find(query, options).toArray();
  636. return tableItems.map(v => this._fromDatabase(modelName, v));
  637. }
  638. /**
  639. * Find by id.
  640. *
  641. * @param {string} modelName
  642. * @param {string|number} id
  643. * @param {object|undefined} filter
  644. * @return {Promise<object>}
  645. */
  646. async findById(modelName, id, filter = undefined) {
  647. id = this._coerceId(id);
  648. const table = this._getCollection(modelName);
  649. const projection = this._buildProjection(
  650. modelName,
  651. filter && filter.fields,
  652. );
  653. const patchedData = await table.findOne({_id: id}, {projection});
  654. if (!patchedData)
  655. throw new InvalidArgumentError('Identifier %v is not found.', String(id));
  656. return this._fromDatabase(modelName, patchedData);
  657. }
  658. /**
  659. * Delete.
  660. *
  661. * @param {string} modelName
  662. * @param {object|undefined} where
  663. * @return {Promise<number>}
  664. */
  665. async delete(modelName, where = undefined) {
  666. const table = this._getCollection(modelName);
  667. const query = this._buildQuery(modelName, where);
  668. const {deletedCount} = await table.deleteMany(query);
  669. return deletedCount;
  670. }
  671. /**
  672. * Delete by id.
  673. *
  674. * @param {string} modelName
  675. * @param {string|number} id
  676. * @return {Promise<boolean>}
  677. */
  678. async deleteById(modelName, id) {
  679. id = this._coerceId(id);
  680. const table = this._getCollection(modelName);
  681. const {deletedCount} = await table.deleteOne({_id: id});
  682. return deletedCount > 0;
  683. }
  684. /**
  685. * Exists.
  686. *
  687. * @param {string} modelName
  688. * @param {string|number} id
  689. * @return {Promise<boolean>}
  690. */
  691. async exists(modelName, id) {
  692. id = this._coerceId(id);
  693. const table = this._getCollection(modelName);
  694. const result = await table.findOne({_id: id}, {});
  695. return result != null;
  696. }
  697. /**
  698. * Count.
  699. *
  700. * @param {string} modelName
  701. * @param {object|undefined} where
  702. * @return {Promise<number>}
  703. */
  704. async count(modelName, where = undefined) {
  705. const query = this._buildQuery(modelName, where);
  706. const table = this._getCollection(modelName);
  707. return await table.count(query);
  708. }
  709. }