mongodb-adapter.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948
  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 {DefinitionRegistry} from '@e22m4u/js-repository';
  15. import {ModelDefinitionUtils} from '@e22m4u/js-repository';
  16. import {InvalidArgumentError} from '@e22m4u/js-repository';
  17. import {modelNameToCollectionName} from './utils/index.js';
  18. import {InvalidOperatorValueError} from '@e22m4u/js-repository';
  19. /**
  20. * Mongodb option names.
  21. * 6.20
  22. *
  23. * https://mongodb.github.io/node-mongodb-native/6.20/interfaces/MongoClientOptions.html
  24. *
  25. * @type {string[]}
  26. */
  27. const MONGODB_OPTION_NAMES = [
  28. 'ALPNProtocols',
  29. 'allowPartialTrustChain',
  30. 'appName',
  31. 'auth',
  32. 'authMechanism',
  33. 'authMechanismProperties',
  34. 'authSource',
  35. 'autoEncryption',
  36. 'autoSelectFamily',
  37. 'autoSelectFamilyAttemptTimeout',
  38. 'bsonRegExp',
  39. 'ca',
  40. 'cert',
  41. 'checkKeys',
  42. 'checkServerIdentity',
  43. 'ciphers',
  44. 'compressors',
  45. 'connectTimeoutMS',
  46. 'crl',
  47. 'directConnection',
  48. 'driverInfo',
  49. 'ecdhCurve',
  50. 'enableUtf8Validation',
  51. 'family',
  52. 'fieldsAsRaw',
  53. 'forceServerObjectId',
  54. 'heartbeatFrequencyMS',
  55. 'hints',
  56. 'ignoreUndefined',
  57. 'journal',
  58. 'keepAliveInitialDelay',
  59. 'key',
  60. 'loadBalanced',
  61. 'localAddress',
  62. 'localPort',
  63. 'localThresholdMS',
  64. 'lookup',
  65. 'maxConnecting',
  66. 'maxIdleTimeMS',
  67. 'maxPoolSize',
  68. 'maxStalenessSeconds',
  69. 'minDHSize',
  70. 'minHeartbeatFrequencyMS',
  71. 'minPoolSize',
  72. 'mongodbLogComponentSeverities',
  73. 'mongodbLogMaxDocumentLength',
  74. 'mongodbLogPath',
  75. 'monitorCommands',
  76. 'noDelay',
  77. 'passphrase',
  78. 'pfx',
  79. 'pkFactory',
  80. 'promoteBuffers',
  81. 'promoteLongs',
  82. 'promoteValues',
  83. 'proxyHost',
  84. 'proxyPassword',
  85. 'proxyPort',
  86. 'proxyUsername',
  87. 'raw',
  88. 'readConcern',
  89. 'readConcernLevel',
  90. 'readPreference',
  91. 'readPreferenceTags',
  92. 'rejectUnauthorized',
  93. 'replicaSet',
  94. 'retryReads',
  95. 'retryWrites',
  96. 'secureContext',
  97. 'secureProtocol',
  98. 'serializeFunctions',
  99. 'serverApi',
  100. 'serverMonitoringMode',
  101. 'serverSelectionTimeoutMS',
  102. 'servername',
  103. 'session',
  104. 'socketTimeoutMS',
  105. 'srvMaxHosts',
  106. 'srvServiceName',
  107. 'ssl',
  108. 'timeoutMS',
  109. 'tls',
  110. 'tlsAllowInvalidCertificates',
  111. 'tlsAllowInvalidHostnames',
  112. 'tlsCAFile',
  113. 'tlsCRLFile',
  114. 'tlsCertificateKeyFile',
  115. 'tlsCertificateKeyFilePassword',
  116. 'tlsInsecure',
  117. 'useBigInt64',
  118. 'w',
  119. 'waitQueueTimeoutMS',
  120. 'writeConcern',
  121. 'wtimeoutMS',
  122. 'zlibCompressionLevel',
  123. ];
  124. /**
  125. * Default settings.
  126. *
  127. * @type {object}
  128. */
  129. const DEFAULT_SETTINGS = {
  130. // connectTimeoutMS: 2500,
  131. // serverSelectionTimeoutMS: 2500,
  132. };
  133. /**
  134. * Mongodb adapter.
  135. */
  136. export class MongodbAdapter extends Adapter {
  137. /**
  138. * Mongodb instance.
  139. *
  140. * @type {MongoClient}
  141. * @private
  142. */
  143. _client;
  144. /**
  145. * Client.
  146. *
  147. * @returns {MongoClient}
  148. */
  149. get client() {
  150. return this._client;
  151. }
  152. /**
  153. * Collections.
  154. *
  155. * @type {Map<any, any>}
  156. * @private
  157. */
  158. _collections = new Map();
  159. /**
  160. * Constructor.
  161. *
  162. * @param {ServiceContainer} container
  163. * @param settings
  164. */
  165. constructor(container, settings) {
  166. settings = Object.assign({}, DEFAULT_SETTINGS, settings || {});
  167. settings.protocol = settings.protocol || 'mongodb';
  168. settings.hostname = settings.hostname || settings.host || '127.0.0.1';
  169. settings.port = settings.port || 27017;
  170. settings.database = settings.database || settings.db || 'database';
  171. super(container, settings);
  172. const options = selectObjectKeys(this.settings, MONGODB_OPTION_NAMES);
  173. const url = createMongodbUrl(this.settings);
  174. this._client = new MongoClient(url, options);
  175. }
  176. /**
  177. * Get id prop name.
  178. *
  179. * @param modelName
  180. * @private
  181. */
  182. _getIdPropName(modelName) {
  183. return this.getService(ModelDefinitionUtils).getPrimaryKeyAsPropertyName(
  184. modelName,
  185. );
  186. }
  187. /**
  188. * Get id col name.
  189. *
  190. * @param modelName
  191. * @private
  192. */
  193. _getIdColName(modelName) {
  194. return this.getService(ModelDefinitionUtils).getPrimaryKeyAsColumnName(
  195. modelName,
  196. );
  197. }
  198. /**
  199. * Coerce id.
  200. *
  201. * @param value
  202. * @returns {ObjectId|*}
  203. * @private
  204. */
  205. _coerceId(value) {
  206. if (value == null) return value;
  207. if (isObjectId(value)) return new ObjectId(value);
  208. return value;
  209. }
  210. /**
  211. * Coerce date.
  212. *
  213. * @param value
  214. * @returns {Date|*}
  215. * @private
  216. */
  217. _coerceDate(value) {
  218. if (value == null) return value;
  219. if (value instanceof Date) return value;
  220. if (isIsoDate(value)) return new Date(value);
  221. return value;
  222. }
  223. /**
  224. * To database.
  225. *
  226. * @param {string} modelName
  227. * @param {object} modelData
  228. * @returns {object}
  229. * @private
  230. */
  231. _toDatabase(modelName, modelData) {
  232. const tableData = this.getService(
  233. ModelDefinitionUtils,
  234. ).convertPropertyNamesToColumnNames(modelName, modelData);
  235. const idColName = this._getIdColName(modelName);
  236. if (idColName !== 'id' && idColName !== '_id')
  237. throw new InvalidArgumentError(
  238. 'MongoDB is not supporting custom names of the primary key. ' +
  239. 'Do use "id" as a primary key instead of %v.',
  240. idColName,
  241. );
  242. if (idColName in tableData && idColName !== '_id') {
  243. tableData._id = tableData[idColName];
  244. delete tableData[idColName];
  245. }
  246. return transformValuesDeep(tableData, value => {
  247. if (value instanceof ObjectId) return value;
  248. if (value instanceof Date) return value;
  249. if (isObjectId(value)) return new ObjectId(value);
  250. if (isIsoDate(value)) return new Date(value);
  251. return value;
  252. });
  253. }
  254. /**
  255. * From database.
  256. *
  257. * @param {string} modelName
  258. * @param {object} tableData
  259. * @returns {object}
  260. * @private
  261. */
  262. _fromDatabase(modelName, tableData) {
  263. if ('_id' in tableData) {
  264. const idColName = this._getIdColName(modelName);
  265. if (idColName !== 'id' && idColName !== '_id')
  266. throw new InvalidArgumentError(
  267. 'MongoDB is not supporting custom names of the primary key. ' +
  268. 'Do use "id" as a primary key instead of %v.',
  269. idColName,
  270. );
  271. if (idColName !== '_id') {
  272. tableData[idColName] = tableData._id;
  273. delete tableData._id;
  274. }
  275. }
  276. const modelData = this.getService(
  277. ModelDefinitionUtils,
  278. ).convertColumnNamesToPropertyNames(modelName, tableData);
  279. return transformValuesDeep(modelData, value => {
  280. if (value instanceof ObjectId) return String(value);
  281. if (value instanceof Date) return value.toISOString();
  282. return value;
  283. });
  284. }
  285. /**
  286. * Get collection name by model name.
  287. *
  288. * @param {string} modelName
  289. */
  290. _getCollectionNameByModelName(modelName) {
  291. const modelDef = this.getService(DefinitionRegistry).getModel(modelName);
  292. if (modelDef.tableName != null) return modelDef.tableName;
  293. return modelNameToCollectionName(modelDef.name);
  294. }
  295. /**
  296. * Get collection.
  297. *
  298. * @param {string} modelName
  299. * @returns {*}
  300. * @private
  301. */
  302. _getCollection(modelName) {
  303. let collection = this._collections.get(modelName);
  304. if (collection) return collection;
  305. const collectionName = this._getCollectionNameByModelName(modelName);
  306. collection = this.client
  307. .db(this.settings.database)
  308. .collection(collectionName);
  309. this._collections.set(modelName, collection);
  310. return collection;
  311. }
  312. /**
  313. * Get id type.
  314. *
  315. * @param modelName
  316. * @returns {string|*}
  317. * @private
  318. */
  319. _getIdType(modelName) {
  320. const utils = this.getService(ModelDefinitionUtils);
  321. const pkPropName = utils.getPrimaryKeyAsPropertyName(modelName);
  322. return utils.getDataTypeByPropertyName(modelName, pkPropName);
  323. }
  324. /**
  325. * Get col name.
  326. *
  327. * @param {string} modelName
  328. * @param {string} propName
  329. * @returns {string}
  330. * @private
  331. */
  332. _getColName(modelName, propName) {
  333. if (!propName || typeof propName !== 'string')
  334. throw new InvalidArgumentError(
  335. 'Property name must be a non-empty String, but %v given.',
  336. propName,
  337. );
  338. const utils = this.getService(ModelDefinitionUtils);
  339. let colName = propName;
  340. try {
  341. colName = utils.getColumnNameByPropertyName(modelName, propName);
  342. } catch (error) {
  343. if (
  344. !(error instanceof InvalidArgumentError) ||
  345. error.message.indexOf('does not have the property') === -1
  346. ) {
  347. throw error;
  348. }
  349. }
  350. return colName;
  351. }
  352. /**
  353. * Convert prop names chain to col names chain.
  354. *
  355. * @param {string} modelName
  356. * @param {string} propsChain
  357. * @returns {string}
  358. * @private
  359. */
  360. _convertPropNamesChainToColNamesChain(modelName, propsChain) {
  361. if (!modelName || typeof modelName !== 'string')
  362. throw new InvalidArgumentError(
  363. 'Model name must be a non-empty String, but %v given.',
  364. modelName,
  365. );
  366. if (!propsChain || typeof propsChain !== 'string')
  367. throw new InvalidArgumentError(
  368. 'Properties chain must be a non-empty String, but %v given.',
  369. propsChain,
  370. );
  371. // удаление повторяющихся точек,
  372. // где строка "foo..bar.baz...qux"
  373. // будет преобразована к "foo.bar.baz.qux"
  374. propsChain = propsChain.replace(/\.{2,}/g, '.');
  375. // разделение цепочки на массив свойств,
  376. // и формирование цепочки имен колонок
  377. const propNames = propsChain.split('.');
  378. const utils = this.getService(ModelDefinitionUtils);
  379. let currModelName = modelName;
  380. return propNames
  381. .map(currPropName => {
  382. if (!currModelName) return currPropName;
  383. const colName = this._getColName(currModelName, currPropName);
  384. currModelName = utils.getModelNameOfPropertyValueIfDefined(
  385. currModelName,
  386. currPropName,
  387. );
  388. return colName;
  389. })
  390. .join('.');
  391. }
  392. /**
  393. * Build projection.
  394. *
  395. * @param {string} modelName
  396. * @param {string|string[]} fields
  397. * @returns {Record<string, number>|undefined}
  398. * @private
  399. */
  400. _buildProjection(modelName, fields) {
  401. if (fields == null) return;
  402. if (Array.isArray(fields) === false) fields = [fields];
  403. if (!fields.length) return;
  404. if (fields.indexOf('_id') === -1) fields.push('_id');
  405. return fields.reduce((acc, field) => {
  406. if (!field || typeof field !== 'string')
  407. throw new InvalidArgumentError(
  408. 'The provided option "fields" should be a non-empty String ' +
  409. 'or an Array of non-empty String, but %v given.',
  410. field,
  411. );
  412. let colName = this._convertPropNamesChainToColNamesChain(
  413. modelName,
  414. field,
  415. );
  416. acc[colName] = 1;
  417. return acc;
  418. }, {});
  419. }
  420. /**
  421. * Build sort.
  422. *
  423. * @param {string} modelName
  424. * @param {string|string[]} clause
  425. * @returns {object|undefined}
  426. * @private
  427. */
  428. _buildSort(modelName, clause) {
  429. if (clause == null) return;
  430. if (Array.isArray(clause) === false) clause = [clause];
  431. if (!clause.length) return;
  432. const idPropName = this._getIdPropName(modelName);
  433. return clause.reduce((acc, order) => {
  434. if (!order || typeof order !== 'string')
  435. throw new InvalidArgumentError(
  436. 'The provided option "order" should be a non-empty String ' +
  437. 'or an Array of non-empty String, but %v given.',
  438. order,
  439. );
  440. const direction = order.match(/\s+(A|DE)SC$/);
  441. let field = order.replace(/\s+(A|DE)SC$/, '').trim();
  442. if (field === idPropName) {
  443. field = '_id';
  444. } else {
  445. try {
  446. field = this._convertPropNamesChainToColNamesChain(modelName, field);
  447. } catch (error) {
  448. if (
  449. !(error instanceof InvalidArgumentError) ||
  450. error.message.indexOf('does not have the property') === -1
  451. ) {
  452. throw error;
  453. }
  454. }
  455. }
  456. acc[field] = direction && direction[1] === 'DE' ? -1 : 1;
  457. return acc;
  458. }, {});
  459. }
  460. /**
  461. * Build query.
  462. *
  463. * @param {string} modelName
  464. * @param {object} clause
  465. * @returns {object}
  466. * @private
  467. */
  468. _buildQuery(modelName, clause) {
  469. if (clause == null) return;
  470. if (typeof clause !== 'object' || Array.isArray(clause))
  471. throw new InvalidArgumentError(
  472. 'The provided option "where" should be an Object, but %v given.',
  473. clause,
  474. );
  475. const query = {};
  476. const idPropName = this._getIdPropName(modelName);
  477. Object.keys(clause).forEach(key => {
  478. if (String(key).indexOf('$') !== -1)
  479. throw new InvalidArgumentError(
  480. 'The symbol "$" is not supported, but %v given.',
  481. key,
  482. );
  483. let cond = clause[key];
  484. // and/or/nor clause
  485. if (key === 'and' || key === 'or' || key === 'nor') {
  486. if (cond == null) return;
  487. if (!Array.isArray(cond))
  488. throw new InvalidOperatorValueError(key, 'an Array', cond);
  489. if (cond.length === 0) return;
  490. cond = cond.map(c => this._buildQuery(modelName, c));
  491. cond = cond.filter(c => c != null);
  492. const opKey = '$' + key;
  493. query[opKey] = query[opKey] ?? [];
  494. query[opKey] = [...query[opKey], ...cond];
  495. return;
  496. }
  497. // id
  498. if (key === idPropName) {
  499. key = '_id';
  500. } else {
  501. key = this._convertPropNamesChainToColNamesChain(modelName, key);
  502. }
  503. // string
  504. if (typeof cond === 'string') {
  505. query[key] = this._coerceId(cond);
  506. query[key] = this._coerceDate(query[key]);
  507. return;
  508. }
  509. // ObjectId
  510. if (cond instanceof ObjectId) {
  511. query[key] = cond;
  512. return;
  513. }
  514. // operator
  515. if (cond && cond.constructor && cond.constructor.name === 'Object') {
  516. const opConds = [];
  517. // eq
  518. if ('eq' in cond) {
  519. let eq = this._coerceId(cond.eq);
  520. eq = this._coerceDate(eq);
  521. opConds.push({$eq: eq});
  522. }
  523. // neq
  524. if ('neq' in cond) {
  525. let neq = this._coerceId(cond.neq);
  526. neq = this._coerceDate(neq);
  527. opConds.push({$ne: neq});
  528. }
  529. // gt
  530. if ('gt' in cond) {
  531. const gt = this._coerceDate(cond.gt);
  532. opConds.push({$gt: gt});
  533. }
  534. // lt
  535. if ('lt' in cond) {
  536. const lt = this._coerceDate(cond.lt);
  537. opConds.push({$lt: lt});
  538. }
  539. // gte
  540. if ('gte' in cond) {
  541. const gte = this._coerceDate(cond.gte);
  542. opConds.push({$gte: gte});
  543. }
  544. // lte
  545. if ('lte' in cond) {
  546. const lte = this._coerceDate(cond.lte);
  547. opConds.push({$lte: lte});
  548. }
  549. // inq
  550. if ('inq' in cond) {
  551. if (!cond.inq || !Array.isArray(cond.inq))
  552. throw new InvalidOperatorValueError(
  553. 'inq',
  554. 'an Array of possible values',
  555. cond.inq,
  556. );
  557. const inq = cond.inq.map(v => {
  558. v = this._coerceId(v);
  559. v = this._coerceDate(v);
  560. return v;
  561. });
  562. opConds.push({$in: inq});
  563. }
  564. // nin
  565. if ('nin' in cond) {
  566. if (!cond.nin || !Array.isArray(cond.nin))
  567. throw new InvalidOperatorValueError(
  568. 'nin',
  569. 'an Array of possible values',
  570. cond,
  571. );
  572. const nin = cond.nin.map(v => {
  573. v = this._coerceId(v);
  574. v = this._coerceDate(v);
  575. return v;
  576. });
  577. opConds.push({$nin: nin});
  578. }
  579. // between
  580. if ('between' in cond) {
  581. if (!Array.isArray(cond.between) || cond.between.length !== 2)
  582. throw new InvalidOperatorValueError(
  583. 'between',
  584. 'an Array of 2 elements',
  585. cond.between,
  586. );
  587. const gte = this._coerceDate(cond.between[0]);
  588. const lte = this._coerceDate(cond.between[1]);
  589. opConds.push({$gte: gte, $lte: lte});
  590. }
  591. // exists
  592. if ('exists' in cond) {
  593. if (typeof cond.exists !== 'boolean')
  594. throw new InvalidOperatorValueError(
  595. 'exists',
  596. 'a Boolean',
  597. cond.exists,
  598. );
  599. opConds.push({$exists: cond.exists});
  600. }
  601. // like
  602. if ('like' in cond) {
  603. if (typeof cond.like !== 'string' && !(cond.like instanceof RegExp))
  604. throw new InvalidOperatorValueError(
  605. 'like',
  606. 'a String or RegExp',
  607. cond.like,
  608. );
  609. opConds.push({$regex: stringToRegexp(cond.like)});
  610. }
  611. // nlike
  612. if ('nlike' in cond) {
  613. if (typeof cond.nlike !== 'string' && !(cond.nlike instanceof RegExp))
  614. throw new InvalidOperatorValueError(
  615. 'nlike',
  616. 'a String or RegExp',
  617. cond.nlike,
  618. );
  619. opConds.push({$not: stringToRegexp(cond.nlike)});
  620. }
  621. // ilike
  622. if ('ilike' in cond) {
  623. if (typeof cond.ilike !== 'string' && !(cond.ilike instanceof RegExp))
  624. throw new InvalidOperatorValueError(
  625. 'ilike',
  626. 'a String or RegExp',
  627. cond.ilike,
  628. );
  629. opConds.push({$regex: stringToRegexp(cond.ilike, 'i')});
  630. }
  631. // nilike
  632. if ('nilike' in cond) {
  633. if (
  634. typeof cond.nilike !== 'string' &&
  635. !(cond.nilike instanceof RegExp)
  636. ) {
  637. throw new InvalidOperatorValueError(
  638. 'nilike',
  639. 'a String or RegExp',
  640. cond.nilike,
  641. );
  642. }
  643. opConds.push({$not: stringToRegexp(cond.nilike, 'i')});
  644. }
  645. // regexp and flags (optional)
  646. if ('regexp' in cond) {
  647. if (
  648. typeof cond.regexp !== 'string' &&
  649. !(cond.regexp instanceof RegExp)
  650. ) {
  651. throw new InvalidOperatorValueError(
  652. 'regexp',
  653. 'a String or RegExp',
  654. cond.regexp,
  655. );
  656. }
  657. const flags = cond.flags || undefined;
  658. if (flags && typeof flags !== 'string')
  659. throw new InvalidArgumentError(
  660. 'RegExp flags must be a String, but %v given.',
  661. cond.flags,
  662. );
  663. opConds.push({$regex: stringToRegexp(cond.regexp, flags)});
  664. }
  665. // adds a single operator condition
  666. if (opConds.length === 1) {
  667. query[key] = opConds[0];
  668. // adds multiple operator conditions
  669. } else if (opConds.length > 1) {
  670. query['$and'] = query['$and'] ?? [];
  671. opConds.forEach(c => query['$and'].push({[key]: c}));
  672. }
  673. return;
  674. }
  675. // unknown
  676. query[key] = cond;
  677. });
  678. return Object.keys(query).length ? query : undefined;
  679. }
  680. /**
  681. * Create.
  682. *
  683. * @param {string} modelName
  684. * @param {object} modelData
  685. * @param {object|undefined} filter
  686. * @returns {Promise<object>}
  687. */
  688. async create(modelName, modelData, filter = undefined) {
  689. const idPropName = this._getIdPropName(modelName);
  690. const idValue = modelData[idPropName];
  691. if (idValue == null || idValue === '' || idValue === 0) {
  692. const pkType = this._getIdType(modelName);
  693. if (pkType !== DataType.STRING && pkType !== DataType.ANY)
  694. throw new InvalidArgumentError(
  695. 'MongoDB unable to generate primary keys of %s. ' +
  696. 'Do provide your own value for the %v property ' +
  697. 'or set property type to String.',
  698. capitalize(pkType),
  699. idPropName,
  700. );
  701. delete modelData[idPropName];
  702. }
  703. const tableData = this._toDatabase(modelName, modelData);
  704. const table = this._getCollection(modelName);
  705. const {insertedId} = await table.insertOne(tableData);
  706. const projection = this._buildProjection(
  707. modelName,
  708. filter && filter.fields,
  709. );
  710. const insertedData = await table.findOne({_id: insertedId}, {projection});
  711. return this._fromDatabase(modelName, insertedData);
  712. }
  713. /**
  714. * Replace by id.
  715. *
  716. * @param {string} modelName
  717. * @param {string|number} id
  718. * @param {object} modelData
  719. * @param {object|undefined} filter
  720. * @returns {Promise<object>}
  721. */
  722. async replaceById(modelName, id, modelData, filter = undefined) {
  723. id = this._coerceId(id);
  724. const idPropName = this._getIdPropName(modelName);
  725. modelData[idPropName] = id;
  726. const tableData = this._toDatabase(modelName, modelData);
  727. const table = this._getCollection(modelName);
  728. const {matchedCount} = await table.replaceOne({_id: id}, tableData);
  729. if (matchedCount < 1)
  730. throw new InvalidArgumentError('Identifier %v is not found.', String(id));
  731. const projection = this._buildProjection(
  732. modelName,
  733. filter && filter.fields,
  734. );
  735. const replacedData = await table.findOne({_id: id}, {projection});
  736. return this._fromDatabase(modelName, replacedData);
  737. }
  738. /**
  739. * Replace or create.
  740. *
  741. * @param {string} modelName
  742. * @param {object} modelData
  743. * @param {object|undefined} filter
  744. * @returns {Promise<object>}
  745. */
  746. async replaceOrCreate(modelName, modelData, filter = undefined) {
  747. const idPropName = this._getIdPropName(modelName);
  748. let idValue = modelData[idPropName];
  749. idValue = this._coerceId(idValue);
  750. if (idValue == null || idValue === '' || idValue === 0) {
  751. const pkType = this._getIdType(modelName);
  752. if (pkType !== DataType.STRING && pkType !== DataType.ANY)
  753. throw new InvalidArgumentError(
  754. 'MongoDB unable to generate primary keys of %s. ' +
  755. 'Do provide your own value for the %v property ' +
  756. 'or set property type to String.',
  757. capitalize(pkType),
  758. idPropName,
  759. );
  760. delete modelData[idPropName];
  761. idValue = undefined;
  762. }
  763. const tableData = this._toDatabase(modelName, modelData);
  764. const table = this._getCollection(modelName);
  765. if (idValue == null) {
  766. const {insertedId} = await table.insertOne(tableData);
  767. idValue = insertedId;
  768. } else {
  769. const {upsertedId} = await table.replaceOne({_id: idValue}, tableData, {
  770. upsert: true,
  771. });
  772. if (upsertedId) idValue = upsertedId;
  773. }
  774. const projection = this._buildProjection(
  775. modelName,
  776. filter && filter.fields,
  777. );
  778. const upsertedData = await table.findOne({_id: idValue}, {projection});
  779. return this._fromDatabase(modelName, upsertedData);
  780. }
  781. /**
  782. * Patch.
  783. *
  784. * @param {string} modelName
  785. * @param {object} modelData
  786. * @param {object|undefined} where
  787. * @returns {Promise<number>}
  788. */
  789. async patch(modelName, modelData, where = undefined) {
  790. const idPropName = this._getIdPropName(modelName);
  791. delete modelData[idPropName];
  792. const query = this._buildQuery(modelName, where) || {};
  793. const tableData = this._toDatabase(modelName, modelData);
  794. const table = this._getCollection(modelName);
  795. const {matchedCount} = await table.updateMany(query, {$set: tableData});
  796. return matchedCount;
  797. }
  798. /**
  799. * Patch by id.
  800. *
  801. * @param {string} modelName
  802. * @param {string|number} id
  803. * @param {object} modelData
  804. * @param {object|undefined} filter
  805. * @returns {Promise<object>}
  806. */
  807. async patchById(modelName, id, modelData, filter = undefined) {
  808. id = this._coerceId(id);
  809. const idPropName = this._getIdPropName(modelName);
  810. delete modelData[idPropName];
  811. const tableData = this._toDatabase(modelName, modelData);
  812. const table = this._getCollection(modelName);
  813. const {matchedCount} = await table.updateOne({_id: id}, {$set: tableData});
  814. if (matchedCount < 1)
  815. throw new InvalidArgumentError('Identifier %v is not found.', String(id));
  816. const projection = this._buildProjection(
  817. modelName,
  818. filter && filter.fields,
  819. );
  820. const patchedData = await table.findOne({_id: id}, {projection});
  821. return this._fromDatabase(modelName, patchedData);
  822. }
  823. /**
  824. * Find.
  825. *
  826. * @param {string} modelName
  827. * @param {object|undefined} filter
  828. * @returns {Promise<object[]>}
  829. */
  830. async find(modelName, filter = undefined) {
  831. filter = filter || {};
  832. const query = this._buildQuery(modelName, filter.where);
  833. const sort = this._buildSort(modelName, filter.order);
  834. const limit = filter.limit || undefined;
  835. const skip = filter.skip || undefined;
  836. const projection = this._buildProjection(modelName, filter.fields);
  837. const collection = this._getCollection(modelName);
  838. const options = {sort, limit, skip, projection};
  839. const tableItems = await collection.find(query, options).toArray();
  840. return tableItems.map(v => this._fromDatabase(modelName, v));
  841. }
  842. /**
  843. * Find by id.
  844. *
  845. * @param {string} modelName
  846. * @param {string|number} id
  847. * @param {object|undefined} filter
  848. * @returns {Promise<object>}
  849. */
  850. async findById(modelName, id, filter = undefined) {
  851. id = this._coerceId(id);
  852. const table = this._getCollection(modelName);
  853. const projection = this._buildProjection(
  854. modelName,
  855. filter && filter.fields,
  856. );
  857. const patchedData = await table.findOne({_id: id}, {projection});
  858. if (!patchedData)
  859. throw new InvalidArgumentError('Identifier %v is not found.', String(id));
  860. return this._fromDatabase(modelName, patchedData);
  861. }
  862. /**
  863. * Delete.
  864. *
  865. * @param {string} modelName
  866. * @param {object|undefined} where
  867. * @returns {Promise<number>}
  868. */
  869. async delete(modelName, where = undefined) {
  870. const table = this._getCollection(modelName);
  871. const query = this._buildQuery(modelName, where);
  872. const {deletedCount} = await table.deleteMany(query);
  873. return deletedCount;
  874. }
  875. /**
  876. * Delete by id.
  877. *
  878. * @param {string} modelName
  879. * @param {string|number} id
  880. * @returns {Promise<boolean>}
  881. */
  882. async deleteById(modelName, id) {
  883. id = this._coerceId(id);
  884. const table = this._getCollection(modelName);
  885. const {deletedCount} = await table.deleteOne({_id: id});
  886. return deletedCount > 0;
  887. }
  888. /**
  889. * Exists.
  890. *
  891. * @param {string} modelName
  892. * @param {string|number} id
  893. * @returns {Promise<boolean>}
  894. */
  895. async exists(modelName, id) {
  896. id = this._coerceId(id);
  897. const table = this._getCollection(modelName);
  898. const result = await table.findOne({_id: id}, {});
  899. return result != null;
  900. }
  901. /**
  902. * Count.
  903. *
  904. * @param {string} modelName
  905. * @param {object|undefined} where
  906. * @returns {Promise<number>}
  907. */
  908. async count(modelName, where = undefined) {
  909. const query = this._buildQuery(modelName, where);
  910. const table = this._getCollection(modelName);
  911. return await table.countDocuments(query);
  912. }
  913. }