mongodb-adapter.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881
  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. * @private
  126. */
  127. _getIdPropName(modelName) {
  128. return this.getService(ModelDefinitionUtils).getPrimaryKeyAsPropertyName(
  129. modelName,
  130. );
  131. }
  132. /**
  133. * Get id col name.
  134. *
  135. * @param modelName
  136. * @private
  137. */
  138. _getIdColName(modelName) {
  139. return this.getService(ModelDefinitionUtils).getPrimaryKeyAsColumnName(
  140. modelName,
  141. );
  142. }
  143. /**
  144. * Coerce id.
  145. *
  146. * @param value
  147. * @returns {ObjectId|*}
  148. * @private
  149. */
  150. _coerceId(value) {
  151. if (value == null) return value;
  152. if (isObjectId(value)) return new ObjectId(value);
  153. return value;
  154. }
  155. /**
  156. * Coerce date.
  157. *
  158. * @param value
  159. * @returns {Date|*}
  160. * @private
  161. */
  162. _coerceDate(value) {
  163. if (value == null) return value;
  164. if (value instanceof Date) return value;
  165. if (isIsoDate(value)) return new Date(value);
  166. return value;
  167. }
  168. /**
  169. * To database.
  170. *
  171. * @param {string} modelName
  172. * @param {object} modelData
  173. * @returns {object}
  174. * @private
  175. */
  176. _toDatabase(modelName, modelData) {
  177. const tableData = this.getService(
  178. ModelDefinitionUtils,
  179. ).convertPropertyNamesToColumnNames(modelName, modelData);
  180. const idColName = this._getIdColName(modelName);
  181. if (idColName !== 'id' && idColName !== '_id')
  182. throw new InvalidArgumentError(
  183. 'MongoDB is not supporting custom names of the primary key. ' +
  184. 'Do use "id" as a primary key instead of %v.',
  185. idColName,
  186. );
  187. if (idColName in tableData && idColName !== '_id') {
  188. tableData._id = tableData[idColName];
  189. delete tableData[idColName];
  190. }
  191. return transformValuesDeep(tableData, value => {
  192. if (value instanceof ObjectId) return value;
  193. if (value instanceof Date) return value;
  194. if (isObjectId(value)) return new ObjectId(value);
  195. if (isIsoDate(value)) return new Date(value);
  196. return value;
  197. });
  198. }
  199. /**
  200. * From database.
  201. *
  202. * @param {string} modelName
  203. * @param {object} tableData
  204. * @returns {object}
  205. * @private
  206. */
  207. _fromDatabase(modelName, tableData) {
  208. if ('_id' in tableData) {
  209. const idColName = this._getIdColName(modelName);
  210. if (idColName !== 'id' && idColName !== '_id')
  211. throw new InvalidArgumentError(
  212. 'MongoDB is not supporting custom names of the primary key. ' +
  213. 'Do use "id" as a primary key instead of %v.',
  214. idColName,
  215. );
  216. if (idColName !== '_id') {
  217. tableData[idColName] = tableData._id;
  218. delete tableData._id;
  219. }
  220. }
  221. const modelData = this.getService(
  222. ModelDefinitionUtils,
  223. ).convertColumnNamesToPropertyNames(modelName, tableData);
  224. return transformValuesDeep(modelData, value => {
  225. if (value instanceof ObjectId) return String(value);
  226. if (value instanceof Date) return value.toISOString();
  227. return value;
  228. });
  229. }
  230. /**
  231. * Get collection.
  232. *
  233. * @param {string} modelName
  234. * @returns {*}
  235. * @private
  236. */
  237. _getCollection(modelName) {
  238. let collection = this._collections.get(modelName);
  239. if (collection) return collection;
  240. const tableName =
  241. this.getService(ModelDefinitionUtils).getTableNameByModelName(modelName);
  242. collection = this.client.db(this.settings.database).collection(tableName);
  243. this._collections.set(modelName, collection);
  244. return collection;
  245. }
  246. /**
  247. * Get id type.
  248. *
  249. * @param modelName
  250. * @returns {string|*}
  251. * @private
  252. */
  253. _getIdType(modelName) {
  254. const utils = this.getService(ModelDefinitionUtils);
  255. const pkPropName = utils.getPrimaryKeyAsPropertyName(modelName);
  256. return utils.getDataTypeByPropertyName(modelName, pkPropName);
  257. }
  258. /**
  259. * Get col name.
  260. *
  261. * @param {string} modelName
  262. * @param {string} propName
  263. * @returns {string}
  264. * @private
  265. */
  266. _getColName(modelName, propName) {
  267. if (!propName || typeof propName !== 'string')
  268. throw new InvalidArgumentError(
  269. 'Property name must be a non-empty String, but %v given.',
  270. propName,
  271. );
  272. const utils = this.getService(ModelDefinitionUtils);
  273. let colName = propName;
  274. try {
  275. colName = utils.getColumnNameByPropertyName(modelName, propName);
  276. } catch (error) {
  277. if (
  278. !(error instanceof InvalidArgumentError) ||
  279. error.message.indexOf('does not have the property') === -1
  280. ) {
  281. throw error;
  282. }
  283. }
  284. return colName;
  285. }
  286. /**
  287. * Convert prop names chain to col names chain.
  288. *
  289. * @param {string} modelName
  290. * @param {string} propsChain
  291. * @returns {string}
  292. * @private
  293. */
  294. _convertPropNamesChainToColNamesChain(modelName, propsChain) {
  295. if (!modelName || typeof modelName !== 'string')
  296. throw new InvalidArgumentError(
  297. 'Model name must be a non-empty String, but %v given.',
  298. modelName,
  299. );
  300. if (!propsChain || typeof propsChain !== 'string')
  301. throw new InvalidArgumentError(
  302. 'Properties chain must be a non-empty String, but %v given.',
  303. propsChain,
  304. );
  305. // удаление повторяющихся точек,
  306. // где строка "foo..bar.baz...qux"
  307. // будет преобразована к "foo.bar.baz.qux"
  308. propsChain = propsChain.replace(/\.{2,}/g, '.');
  309. // разделение цепочки на массив свойств,
  310. // и формирование цепочки имен колонок
  311. const propNames = propsChain.split('.');
  312. const utils = this.getService(ModelDefinitionUtils);
  313. let currModelName = modelName;
  314. return propNames
  315. .map(currPropName => {
  316. if (!currModelName) return currPropName;
  317. const colName = this._getColName(currModelName, currPropName);
  318. currModelName = utils.getModelNameOfPropertyValueIfDefined(
  319. currModelName,
  320. currPropName,
  321. );
  322. return colName;
  323. })
  324. .join('.');
  325. }
  326. /**
  327. * Build projection.
  328. *
  329. * @param {string} modelName
  330. * @param {string|string[]} fields
  331. * @returns {Record<string, number>|undefined}
  332. * @private
  333. */
  334. _buildProjection(modelName, fields) {
  335. if (fields == null) return;
  336. if (Array.isArray(fields) === false) fields = [fields];
  337. if (!fields.length) return;
  338. if (fields.indexOf('_id') === -1) fields.push('_id');
  339. return fields.reduce((acc, field) => {
  340. if (!field || typeof field !== 'string')
  341. throw new InvalidArgumentError(
  342. 'The provided option "fields" should be a non-empty String ' +
  343. 'or an Array of non-empty String, but %v given.',
  344. field,
  345. );
  346. let colName = this._convertPropNamesChainToColNamesChain(
  347. modelName,
  348. field,
  349. );
  350. acc[colName] = 1;
  351. return acc;
  352. }, {});
  353. }
  354. /**
  355. * Build sort.
  356. *
  357. * @param {string} modelName
  358. * @param {string|string[]} clause
  359. * @returns {object|undefined}
  360. * @private
  361. */
  362. _buildSort(modelName, clause) {
  363. if (clause == null) return;
  364. if (Array.isArray(clause) === false) clause = [clause];
  365. if (!clause.length) return;
  366. const idPropName = this._getIdPropName(modelName);
  367. return clause.reduce((acc, order) => {
  368. if (!order || typeof order !== 'string')
  369. throw new InvalidArgumentError(
  370. 'The provided option "order" should be a non-empty String ' +
  371. 'or an Array of non-empty String, but %v given.',
  372. order,
  373. );
  374. const direction = order.match(/\s+(A|DE)SC$/);
  375. let field = order.replace(/\s+(A|DE)SC$/, '').trim();
  376. if (field === idPropName) {
  377. field = '_id';
  378. } else {
  379. try {
  380. field = this._convertPropNamesChainToColNamesChain(modelName, field);
  381. } catch (error) {
  382. if (
  383. !(error instanceof InvalidArgumentError) ||
  384. error.message.indexOf('does not have the property') === -1
  385. ) {
  386. throw error;
  387. }
  388. }
  389. }
  390. acc[field] = direction && direction[1] === 'DE' ? -1 : 1;
  391. return acc;
  392. }, {});
  393. }
  394. /**
  395. * Build query.
  396. *
  397. * @param {string} modelName
  398. * @param {object} clause
  399. * @returns {object}
  400. * @private
  401. */
  402. _buildQuery(modelName, clause) {
  403. if (clause == null) return;
  404. if (typeof clause !== 'object' || Array.isArray(clause))
  405. throw new InvalidArgumentError(
  406. 'The provided option "where" should be an Object, but %v given.',
  407. clause,
  408. );
  409. const query = {};
  410. const idPropName = this._getIdPropName(modelName);
  411. Object.keys(clause).forEach(key => {
  412. if (String(key).indexOf('$') !== -1)
  413. throw new InvalidArgumentError(
  414. 'The symbol "$" is not supported, but %v given.',
  415. key,
  416. );
  417. let cond = clause[key];
  418. // and/or/nor clause
  419. if (key === 'and' || key === 'or' || key === 'nor') {
  420. if (cond == null) return;
  421. if (!Array.isArray(cond))
  422. throw new InvalidOperatorValueError(key, 'an Array', cond);
  423. if (cond.length === 0) return;
  424. cond = cond.map(c => this._buildQuery(modelName, c));
  425. cond = cond.filter(c => c != null);
  426. const opKey = '$' + key;
  427. query[opKey] = query[opKey] ?? [];
  428. query[opKey] = [...query[opKey], ...cond];
  429. return;
  430. }
  431. // id
  432. if (key === idPropName) {
  433. key = '_id';
  434. } else {
  435. key = this._convertPropNamesChainToColNamesChain(modelName, key);
  436. }
  437. // string
  438. if (typeof cond === 'string') {
  439. query[key] = this._coerceId(cond);
  440. query[key] = this._coerceDate(query[key]);
  441. return;
  442. }
  443. // ObjectId
  444. if (cond instanceof ObjectId) {
  445. query[key] = cond;
  446. return;
  447. }
  448. // operator
  449. if (cond && cond.constructor && cond.constructor.name === 'Object') {
  450. const opConds = [];
  451. // eq
  452. if ('eq' in cond) {
  453. let eq = this._coerceId(cond.eq);
  454. eq = this._coerceDate(eq);
  455. opConds.push({$eq: eq});
  456. }
  457. // neq
  458. if ('neq' in cond) {
  459. let neq = this._coerceId(cond.neq);
  460. neq = this._coerceDate(neq);
  461. opConds.push({$ne: neq});
  462. }
  463. // gt
  464. if ('gt' in cond) {
  465. const gt = this._coerceDate(cond.gt);
  466. opConds.push({$gt: gt});
  467. }
  468. // lt
  469. if ('lt' in cond) {
  470. const lt = this._coerceDate(cond.lt);
  471. opConds.push({$lt: lt});
  472. }
  473. // gte
  474. if ('gte' in cond) {
  475. const gte = this._coerceDate(cond.gte);
  476. opConds.push({$gte: gte});
  477. }
  478. // lte
  479. if ('lte' in cond) {
  480. const lte = this._coerceDate(cond.lte);
  481. opConds.push({$lte: lte});
  482. }
  483. // inq
  484. if ('inq' in cond) {
  485. if (!cond.inq || !Array.isArray(cond.inq))
  486. throw new InvalidOperatorValueError(
  487. 'inq',
  488. 'an Array of possible values',
  489. cond.inq,
  490. );
  491. const inq = cond.inq.map(v => {
  492. v = this._coerceId(v);
  493. v = this._coerceDate(v);
  494. return v;
  495. });
  496. opConds.push({$in: inq});
  497. }
  498. // nin
  499. if ('nin' in cond) {
  500. if (!cond.nin || !Array.isArray(cond.nin))
  501. throw new InvalidOperatorValueError(
  502. 'nin',
  503. 'an Array of possible values',
  504. cond,
  505. );
  506. const nin = cond.nin.map(v => {
  507. v = this._coerceId(v);
  508. v = this._coerceDate(v);
  509. return v;
  510. });
  511. opConds.push({$nin: nin});
  512. }
  513. // between
  514. if ('between' in cond) {
  515. if (!Array.isArray(cond.between) || cond.between.length !== 2)
  516. throw new InvalidOperatorValueError(
  517. 'between',
  518. 'an Array of 2 elements',
  519. cond.between,
  520. );
  521. const gte = this._coerceDate(cond.between[0]);
  522. const lte = this._coerceDate(cond.between[1]);
  523. opConds.push({$gte: gte, $lte: lte});
  524. }
  525. // exists
  526. if ('exists' in cond) {
  527. if (typeof cond.exists !== 'boolean')
  528. throw new InvalidOperatorValueError(
  529. 'exists',
  530. 'a Boolean',
  531. cond.exists,
  532. );
  533. opConds.push({$exists: cond.exists});
  534. }
  535. // like
  536. if ('like' in cond) {
  537. if (typeof cond.like !== 'string' && !(cond.like instanceof RegExp))
  538. throw new InvalidOperatorValueError(
  539. 'like',
  540. 'a String or RegExp',
  541. cond.like,
  542. );
  543. opConds.push({$regex: stringToRegexp(cond.like)});
  544. }
  545. // nlike
  546. if ('nlike' in cond) {
  547. if (typeof cond.nlike !== 'string' && !(cond.nlike instanceof RegExp))
  548. throw new InvalidOperatorValueError(
  549. 'nlike',
  550. 'a String or RegExp',
  551. cond.nlike,
  552. );
  553. opConds.push({$not: stringToRegexp(cond.nlike)});
  554. }
  555. // ilike
  556. if ('ilike' in cond) {
  557. if (typeof cond.ilike !== 'string' && !(cond.ilike instanceof RegExp))
  558. throw new InvalidOperatorValueError(
  559. 'ilike',
  560. 'a String or RegExp',
  561. cond.ilike,
  562. );
  563. opConds.push({$regex: stringToRegexp(cond.ilike, 'i')});
  564. }
  565. // nilike
  566. if ('nilike' in cond) {
  567. if (
  568. typeof cond.nilike !== 'string' &&
  569. !(cond.nilike instanceof RegExp)
  570. ) {
  571. throw new InvalidOperatorValueError(
  572. 'nilike',
  573. 'a String or RegExp',
  574. cond.nilike,
  575. );
  576. }
  577. opConds.push({$not: stringToRegexp(cond.nilike, 'i')});
  578. }
  579. // regexp and flags (optional)
  580. if ('regexp' in cond) {
  581. if (
  582. typeof cond.regexp !== 'string' &&
  583. !(cond.regexp instanceof RegExp)
  584. ) {
  585. throw new InvalidOperatorValueError(
  586. 'regexp',
  587. 'a String or RegExp',
  588. cond.regexp,
  589. );
  590. }
  591. const flags = cond.flags || undefined;
  592. if (flags && typeof flags !== 'string')
  593. throw new InvalidArgumentError(
  594. 'RegExp flags must be a String, but %v given.',
  595. cond.flags,
  596. );
  597. opConds.push({$regex: stringToRegexp(cond.regexp, flags)});
  598. }
  599. // adds a single operator condition
  600. if (opConds.length === 1) {
  601. query[key] = opConds[0];
  602. // adds multiple operator conditions
  603. } else if (opConds.length > 1) {
  604. query['$and'] = query['$and'] ?? [];
  605. opConds.forEach(c => query['$and'].push({[key]: c}));
  606. }
  607. return;
  608. }
  609. // unknown
  610. query[key] = cond;
  611. });
  612. return Object.keys(query).length ? query : undefined;
  613. }
  614. /**
  615. * Create.
  616. *
  617. * @param {string} modelName
  618. * @param {object} modelData
  619. * @param {object|undefined} filter
  620. * @returns {Promise<object>}
  621. */
  622. async create(modelName, modelData, filter = undefined) {
  623. const idPropName = this._getIdPropName(modelName);
  624. const idValue = modelData[idPropName];
  625. if (idValue == null || idValue === '' || idValue === 0) {
  626. const pkType = this._getIdType(modelName);
  627. if (pkType !== DataType.STRING && pkType !== DataType.ANY)
  628. throw new InvalidArgumentError(
  629. 'MongoDB unable to generate primary keys of %s. ' +
  630. 'Do provide your own value for the %v property ' +
  631. 'or set property type to String.',
  632. capitalize(pkType),
  633. idPropName,
  634. );
  635. delete modelData[idPropName];
  636. }
  637. const tableData = this._toDatabase(modelName, modelData);
  638. const table = this._getCollection(modelName);
  639. const {insertedId} = await table.insertOne(tableData);
  640. const projection = this._buildProjection(
  641. modelName,
  642. filter && filter.fields,
  643. );
  644. const insertedData = await table.findOne({_id: insertedId}, {projection});
  645. return this._fromDatabase(modelName, insertedData);
  646. }
  647. /**
  648. * Replace by id.
  649. *
  650. * @param {string} modelName
  651. * @param {string|number} id
  652. * @param {object} modelData
  653. * @param {object|undefined} filter
  654. * @returns {Promise<object>}
  655. */
  656. async replaceById(modelName, id, modelData, filter = undefined) {
  657. id = this._coerceId(id);
  658. const idPropName = this._getIdPropName(modelName);
  659. modelData[idPropName] = id;
  660. const tableData = this._toDatabase(modelName, modelData);
  661. const table = this._getCollection(modelName);
  662. const {matchedCount} = await table.replaceOne({_id: id}, tableData);
  663. if (matchedCount < 1)
  664. throw new InvalidArgumentError('Identifier %v is not found.', String(id));
  665. const projection = this._buildProjection(
  666. modelName,
  667. filter && filter.fields,
  668. );
  669. const replacedData = await table.findOne({_id: id}, {projection});
  670. return this._fromDatabase(modelName, replacedData);
  671. }
  672. /**
  673. * Replace or create.
  674. *
  675. * @param {string} modelName
  676. * @param {object} modelData
  677. * @param {object|undefined} filter
  678. * @returns {Promise<object>}
  679. */
  680. async replaceOrCreate(modelName, modelData, filter = undefined) {
  681. const idPropName = this._getIdPropName(modelName);
  682. let idValue = modelData[idPropName];
  683. idValue = this._coerceId(idValue);
  684. if (idValue == null || idValue === '' || idValue === 0) {
  685. const pkType = this._getIdType(modelName);
  686. if (pkType !== DataType.STRING && pkType !== DataType.ANY)
  687. throw new InvalidArgumentError(
  688. 'MongoDB unable to generate primary keys of %s. ' +
  689. 'Do provide your own value for the %v property ' +
  690. 'or set property type to String.',
  691. capitalize(pkType),
  692. idPropName,
  693. );
  694. delete modelData[idPropName];
  695. idValue = undefined;
  696. }
  697. const tableData = this._toDatabase(modelName, modelData);
  698. const table = this._getCollection(modelName);
  699. if (idValue == null) {
  700. const {insertedId} = await table.insertOne(tableData);
  701. idValue = insertedId;
  702. } else {
  703. const {upsertedId} = await table.replaceOne({_id: idValue}, tableData, {
  704. upsert: true,
  705. });
  706. if (upsertedId) idValue = upsertedId;
  707. }
  708. const projection = this._buildProjection(
  709. modelName,
  710. filter && filter.fields,
  711. );
  712. const upsertedData = await table.findOne({_id: idValue}, {projection});
  713. return this._fromDatabase(modelName, upsertedData);
  714. }
  715. /**
  716. * Patch.
  717. *
  718. * @param {string} modelName
  719. * @param {object} modelData
  720. * @param {object|undefined} where
  721. * @returns {Promise<number>}
  722. */
  723. async patch(modelName, modelData, where = undefined) {
  724. const idPropName = this._getIdPropName(modelName);
  725. delete modelData[idPropName];
  726. const query = this._buildQuery(modelName, where) || {};
  727. const tableData = this._toDatabase(modelName, modelData);
  728. const table = this._getCollection(modelName);
  729. const {matchedCount} = await table.updateMany(query, {$set: tableData});
  730. return matchedCount;
  731. }
  732. /**
  733. * Patch by id.
  734. *
  735. * @param {string} modelName
  736. * @param {string|number} id
  737. * @param {object} modelData
  738. * @param {object|undefined} filter
  739. * @returns {Promise<object>}
  740. */
  741. async patchById(modelName, id, modelData, filter = undefined) {
  742. id = this._coerceId(id);
  743. const idPropName = this._getIdPropName(modelName);
  744. delete modelData[idPropName];
  745. const tableData = this._toDatabase(modelName, modelData);
  746. const table = this._getCollection(modelName);
  747. const {matchedCount} = await table.updateOne({_id: id}, {$set: tableData});
  748. if (matchedCount < 1)
  749. throw new InvalidArgumentError('Identifier %v is not found.', String(id));
  750. const projection = this._buildProjection(
  751. modelName,
  752. filter && filter.fields,
  753. );
  754. const patchedData = await table.findOne({_id: id}, {projection});
  755. return this._fromDatabase(modelName, patchedData);
  756. }
  757. /**
  758. * Find.
  759. *
  760. * @param {string} modelName
  761. * @param {object|undefined} filter
  762. * @returns {Promise<object[]>}
  763. */
  764. async find(modelName, filter = undefined) {
  765. filter = filter || {};
  766. const query = this._buildQuery(modelName, filter.where);
  767. const sort = this._buildSort(modelName, filter.order);
  768. const limit = filter.limit || undefined;
  769. const skip = filter.skip || undefined;
  770. const projection = this._buildProjection(modelName, filter.fields);
  771. const collection = this._getCollection(modelName);
  772. const options = {sort, limit, skip, projection};
  773. const tableItems = await collection.find(query, options).toArray();
  774. return tableItems.map(v => this._fromDatabase(modelName, v));
  775. }
  776. /**
  777. * Find by id.
  778. *
  779. * @param {string} modelName
  780. * @param {string|number} id
  781. * @param {object|undefined} filter
  782. * @returns {Promise<object>}
  783. */
  784. async findById(modelName, id, filter = undefined) {
  785. id = this._coerceId(id);
  786. const table = this._getCollection(modelName);
  787. const projection = this._buildProjection(
  788. modelName,
  789. filter && filter.fields,
  790. );
  791. const patchedData = await table.findOne({_id: id}, {projection});
  792. if (!patchedData)
  793. throw new InvalidArgumentError('Identifier %v is not found.', String(id));
  794. return this._fromDatabase(modelName, patchedData);
  795. }
  796. /**
  797. * Delete.
  798. *
  799. * @param {string} modelName
  800. * @param {object|undefined} where
  801. * @returns {Promise<number>}
  802. */
  803. async delete(modelName, where = undefined) {
  804. const table = this._getCollection(modelName);
  805. const query = this._buildQuery(modelName, where);
  806. const {deletedCount} = await table.deleteMany(query);
  807. return deletedCount;
  808. }
  809. /**
  810. * Delete by id.
  811. *
  812. * @param {string} modelName
  813. * @param {string|number} id
  814. * @returns {Promise<boolean>}
  815. */
  816. async deleteById(modelName, id) {
  817. id = this._coerceId(id);
  818. const table = this._getCollection(modelName);
  819. const {deletedCount} = await table.deleteOne({_id: id});
  820. return deletedCount > 0;
  821. }
  822. /**
  823. * Exists.
  824. *
  825. * @param {string} modelName
  826. * @param {string|number} id
  827. * @returns {Promise<boolean>}
  828. */
  829. async exists(modelName, id) {
  830. id = this._coerceId(id);
  831. const table = this._getCollection(modelName);
  832. const result = await table.findOne({_id: id}, {});
  833. return result != null;
  834. }
  835. /**
  836. * Count.
  837. *
  838. * @param {string} modelName
  839. * @param {object|undefined} where
  840. * @returns {Promise<number>}
  841. */
  842. async count(modelName, where = undefined) {
  843. const query = this._buildQuery(modelName, where);
  844. const table = this._getCollection(modelName);
  845. return await table.count(query);
  846. }
  847. }