model-definition-utils.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. import {Service} from '@e22m4u/js-service';
  2. import {DataType} from './properties/index.js';
  3. import {cloneDeep} from '../../utils/index.js';
  4. import {excludeObjectKeys} from '../../utils/index.js';
  5. import {EmptyValuesDefiner} from './properties/index.js';
  6. import {InvalidArgumentError} from '../../errors/index.js';
  7. import {DefinitionRegistry} from '../definition-registry.js';
  8. /**
  9. * Default primary key property name.
  10. *
  11. * @type {string}
  12. */
  13. export const DEFAULT_PRIMARY_KEY_PROPERTY_NAME = 'id';
  14. /**
  15. * Model definition utils.
  16. */
  17. export class ModelDefinitionUtils extends Service {
  18. /**
  19. * Get primary key as property name.
  20. *
  21. * @param {string} modelName
  22. * @returns {string}
  23. */
  24. getPrimaryKeyAsPropertyName(modelName) {
  25. const propDefs =
  26. this.getPropertiesDefinitionInBaseModelHierarchy(modelName);
  27. const propNames = Object.keys(propDefs).filter(propName => {
  28. const propDef = propDefs[propName];
  29. return propDef && typeof propDef === 'object' && propDef.primaryKey;
  30. });
  31. if (propNames.length < 1) {
  32. const isDefaultPrimaryKeyAlreadyInUse = Object.keys(propDefs).includes(
  33. DEFAULT_PRIMARY_KEY_PROPERTY_NAME,
  34. );
  35. if (isDefaultPrimaryKeyAlreadyInUse)
  36. throw new InvalidArgumentError(
  37. 'The property name %v of the model %v is defined as a regular property. ' +
  38. 'In this case, a primary key should be defined explicitly. ' +
  39. 'Do use the option "primaryKey" to specify the primary key.',
  40. DEFAULT_PRIMARY_KEY_PROPERTY_NAME,
  41. modelName,
  42. );
  43. return DEFAULT_PRIMARY_KEY_PROPERTY_NAME;
  44. }
  45. return propNames[0];
  46. }
  47. /**
  48. * Get primary key as column name.
  49. *
  50. * @param {string} modelName
  51. * @returns {string}
  52. */
  53. getPrimaryKeyAsColumnName(modelName) {
  54. const pkPropName = this.getPrimaryKeyAsPropertyName(modelName);
  55. let pkColName;
  56. try {
  57. pkColName = this.getColumnNameByPropertyName(modelName, pkPropName);
  58. } catch (error) {
  59. if (!(error instanceof InvalidArgumentError)) throw error;
  60. }
  61. if (pkColName === undefined) return pkPropName;
  62. return pkColName;
  63. }
  64. /**
  65. * Get table name by model name.
  66. *
  67. * @param {string} modelName
  68. * @returns {string}
  69. */
  70. getTableNameByModelName(modelName) {
  71. const modelDef = this.getService(DefinitionRegistry).getModel(modelName);
  72. return modelDef.tableName ?? modelName;
  73. }
  74. /**
  75. * Get column name by property name.
  76. *
  77. * @param {string} modelName
  78. * @param {string} propertyName
  79. * @returns {string}
  80. */
  81. getColumnNameByPropertyName(modelName, propertyName) {
  82. const propDefs =
  83. this.getPropertiesDefinitionInBaseModelHierarchy(modelName);
  84. const propDef = propDefs[propertyName];
  85. if (!propDef)
  86. throw new InvalidArgumentError(
  87. 'The model %v does not have the property %v.',
  88. modelName,
  89. propertyName,
  90. );
  91. if (propDef && typeof propDef === 'object')
  92. return propDef.columnName ?? propertyName;
  93. return propertyName;
  94. }
  95. /**
  96. * Get default property value.
  97. *
  98. * @param {string} modelName
  99. * @param {string} propertyName
  100. * @returns {*}
  101. */
  102. getDefaultPropertyValue(modelName, propertyName) {
  103. const propDefs =
  104. this.getPropertiesDefinitionInBaseModelHierarchy(modelName);
  105. const propDef = propDefs[propertyName];
  106. if (!propDef)
  107. throw new InvalidArgumentError(
  108. 'The model %v does not have the property %v.',
  109. modelName,
  110. propertyName,
  111. );
  112. if (propDef && typeof propDef === 'object')
  113. return propDef.default instanceof Function
  114. ? propDef.default()
  115. : propDef.default;
  116. }
  117. /**
  118. * Set default values for empty properties.
  119. *
  120. * @param {string} modelName
  121. * @param {object} modelData
  122. * @param {boolean|undefined} onlyProvidedProperties
  123. * @returns {object}
  124. */
  125. setDefaultValuesToEmptyProperties(
  126. modelName,
  127. modelData,
  128. onlyProvidedProperties = false,
  129. ) {
  130. const propDefs =
  131. this.getPropertiesDefinitionInBaseModelHierarchy(modelName);
  132. const propNames = onlyProvidedProperties
  133. ? Object.keys(modelData)
  134. : Object.keys(propDefs);
  135. const extendedData = cloneDeep(modelData);
  136. const emptyValueDefiner = this.getService(EmptyValuesDefiner);
  137. propNames.forEach(propName => {
  138. const propDef = propDefs[propName];
  139. const propValue = extendedData[propName];
  140. const propType =
  141. propDef != null
  142. ? this.getDataTypeFromPropertyDefinition(propDef)
  143. : DataType.ANY;
  144. const isEmpty = emptyValueDefiner.isEmpty(propType, propValue);
  145. if (!isEmpty) return;
  146. if (
  147. propDef &&
  148. typeof propDef === 'object' &&
  149. propDef.default !== undefined
  150. ) {
  151. extendedData[propName] = this.getDefaultPropertyValue(
  152. modelName,
  153. propName,
  154. );
  155. }
  156. });
  157. return extendedData;
  158. }
  159. /**
  160. * Convert property names to column names.
  161. *
  162. * @param {string} modelName
  163. * @param {object} modelData
  164. * @returns {object}
  165. */
  166. convertPropertyNamesToColumnNames(modelName, modelData) {
  167. const propDefs =
  168. this.getPropertiesDefinitionInBaseModelHierarchy(modelName);
  169. const propNames = Object.keys(propDefs);
  170. const convertedData = cloneDeep(modelData);
  171. propNames.forEach(propName => {
  172. if (!(propName in convertedData)) return;
  173. const colName = this.getColumnNameByPropertyName(modelName, propName);
  174. let propValue = convertedData[propName];
  175. // если значением является объект, то проверяем
  176. // тип свойства и наличие модели для замены
  177. // полей данного объекта
  178. const propDef = propDefs[propName];
  179. if (
  180. propValue !== null &&
  181. typeof propValue === 'object' &&
  182. !Array.isArray(propValue) &&
  183. propDef !== null &&
  184. typeof propDef === 'object' &&
  185. propDef.type === DataType.OBJECT &&
  186. propDef.model
  187. ) {
  188. propValue = this.convertPropertyNamesToColumnNames(
  189. propDef.model,
  190. propValue,
  191. );
  192. }
  193. // если значением является массив, то проверяем
  194. // тип свойства и наличие модели элементов массива
  195. // для замены полей каждого объекта
  196. if (
  197. Array.isArray(propValue) &&
  198. propDef !== null &&
  199. typeof propDef === 'object' &&
  200. propDef.type === DataType.ARRAY &&
  201. propDef.itemModel
  202. ) {
  203. propValue = propValue.map(el => {
  204. // если элементом массива является объект,
  205. // то конвертируем поля согласно модели
  206. return el !== null && typeof el === 'object' && !Array.isArray(el)
  207. ? this.convertPropertyNamesToColumnNames(propDef.itemModel, el)
  208. : el;
  209. });
  210. }
  211. delete convertedData[propName];
  212. convertedData[colName] = propValue;
  213. });
  214. return convertedData;
  215. }
  216. /**
  217. * Convert column names to property names.
  218. *
  219. * @param {string} modelName
  220. * @param {object} tableData
  221. * @returns {object}
  222. */
  223. convertColumnNamesToPropertyNames(modelName, tableData) {
  224. const propDefs =
  225. this.getPropertiesDefinitionInBaseModelHierarchy(modelName);
  226. const propNames = Object.keys(propDefs);
  227. const convertedData = cloneDeep(tableData);
  228. propNames.forEach(propName => {
  229. const colName = this.getColumnNameByPropertyName(modelName, propName);
  230. if (!(colName in convertedData)) return;
  231. let colValue = convertedData[colName];
  232. // если значением является объект, то проверяем
  233. // тип свойства и наличие модели для замены
  234. // полей данного объекта
  235. const propDef = propDefs[propName];
  236. if (
  237. colValue !== null &&
  238. typeof colValue === 'object' &&
  239. !Array.isArray(colValue) &&
  240. propDef !== null &&
  241. typeof propDef === 'object' &&
  242. propDef.type === DataType.OBJECT &&
  243. propDef.model
  244. ) {
  245. colValue = this.convertColumnNamesToPropertyNames(
  246. propDef.model,
  247. colValue,
  248. );
  249. }
  250. // если значением является массив, то проверяем
  251. // тип свойства и наличие модели элементов массива
  252. // для замены полей каждого объекта
  253. if (
  254. Array.isArray(colValue) &&
  255. propDef !== null &&
  256. typeof propDef === 'object' &&
  257. propDef.type === DataType.ARRAY &&
  258. propDef.itemModel
  259. ) {
  260. colValue = colValue.map(el => {
  261. // если элементом массива является объект,
  262. // то конвертируем поля согласно модели
  263. return el !== null && typeof el === 'object' && !Array.isArray(el)
  264. ? this.convertColumnNamesToPropertyNames(propDef.itemModel, el)
  265. : el;
  266. });
  267. }
  268. delete convertedData[colName];
  269. convertedData[propName] = colValue;
  270. });
  271. return convertedData;
  272. }
  273. /**
  274. * Get data type by property name.
  275. *
  276. * @param {string} modelName
  277. * @param {string} propertyName
  278. * @returns {string}
  279. */
  280. getDataTypeByPropertyName(modelName, propertyName) {
  281. const propDefs =
  282. this.getPropertiesDefinitionInBaseModelHierarchy(modelName);
  283. const propDef = propDefs[propertyName];
  284. if (!propDef) {
  285. const pkPropName = this.getPrimaryKeyAsPropertyName(modelName);
  286. if (pkPropName === propertyName) return DataType.ANY;
  287. throw new InvalidArgumentError(
  288. 'The model %v does not have the property %v.',
  289. modelName,
  290. propertyName,
  291. );
  292. }
  293. if (typeof propDef === 'string') return propDef;
  294. return propDef.type;
  295. }
  296. /**
  297. * Get data type from property definition.
  298. *
  299. * @param {object} propDef
  300. * @returns {string}
  301. */
  302. getDataTypeFromPropertyDefinition(propDef) {
  303. if (
  304. (!propDef || typeof propDef !== 'object') &&
  305. !Object.values(DataType).includes(propDef)
  306. ) {
  307. throw new InvalidArgumentError(
  308. 'The argument "propDef" of the ModelDefinitionUtils.getDataTypeFromPropertyDefinition ' +
  309. 'should be an Object or the DataType enum, but %v given.',
  310. propDef,
  311. );
  312. }
  313. if (typeof propDef === 'string') return propDef;
  314. const dataType = propDef.type;
  315. if (!Object.values(DataType).includes(dataType))
  316. throw new InvalidArgumentError(
  317. 'The given Object to the ModelDefinitionUtils.getDataTypeFromPropertyDefinition ' +
  318. 'should have the "type" property with one of values: %l, but %v given.',
  319. Object.values(DataType),
  320. propDef.type,
  321. );
  322. return dataType;
  323. }
  324. /**
  325. * Get own properties definition of primary keys.
  326. *
  327. * @param {string} modelName
  328. * @returns {object}
  329. */
  330. getOwnPropertiesDefinitionOfPrimaryKeys(modelName) {
  331. const modelDef = this.getService(DefinitionRegistry).getModel(modelName);
  332. const propDefs = modelDef.properties ?? {};
  333. const pkPropNames = Object.keys(propDefs).filter(propName => {
  334. const propDef = propDefs[propName];
  335. return typeof propDef === 'object' && propDef.primaryKey;
  336. });
  337. return pkPropNames.reduce((a, k) => ({...a, [k]: propDefs[k]}), {});
  338. }
  339. /**
  340. * Get own properties definition without primary keys.
  341. *
  342. * @param {string} modelName
  343. * @returns {object}
  344. */
  345. getOwnPropertiesDefinitionWithoutPrimaryKeys(modelName) {
  346. const modelDef = this.getService(DefinitionRegistry).getModel(modelName);
  347. const propDefs = modelDef.properties ?? {};
  348. return Object.keys(propDefs).reduce((result, propName) => {
  349. const propDef = propDefs[propName];
  350. if (typeof propDef === 'object' && propDef.primaryKey) return result;
  351. return {...result, [propName]: propDef};
  352. }, {});
  353. }
  354. /**
  355. * Get properties definition in base model hierarchy.
  356. *
  357. * @param {string} modelName
  358. * @returns {object}
  359. */
  360. getPropertiesDefinitionInBaseModelHierarchy(modelName) {
  361. let result = {};
  362. let pkPropDefs = {};
  363. const recursion = (currModelName, prevModelName = undefined) => {
  364. if (currModelName === prevModelName)
  365. throw new InvalidArgumentError(
  366. 'The model %v has a circular inheritance.',
  367. currModelName,
  368. );
  369. if (Object.keys(pkPropDefs).length === 0) {
  370. pkPropDefs =
  371. this.getOwnPropertiesDefinitionOfPrimaryKeys(currModelName);
  372. result = {...result, ...pkPropDefs};
  373. }
  374. const regularPropDefs =
  375. this.getOwnPropertiesDefinitionWithoutPrimaryKeys(currModelName);
  376. result = {...regularPropDefs, ...result};
  377. const modelDef =
  378. this.getService(DefinitionRegistry).getModel(currModelName);
  379. if (modelDef.base) recursion(modelDef.base, currModelName);
  380. };
  381. recursion(modelName);
  382. return result;
  383. }
  384. /**
  385. * Get own relations definition.
  386. *
  387. * @param {string} modelName
  388. * @returns {object}
  389. */
  390. getOwnRelationsDefinition(modelName) {
  391. const modelDef = this.getService(DefinitionRegistry).getModel(modelName);
  392. return modelDef.relations ?? {};
  393. }
  394. /**
  395. * Get relations definition in base model hierarchy.
  396. *
  397. * @param {string} modelName
  398. * @returns {object}
  399. */
  400. getRelationsDefinitionInBaseModelHierarchy(modelName) {
  401. let result = {};
  402. const recursion = (currModelName, prevModelName = undefined) => {
  403. if (currModelName === prevModelName)
  404. throw new InvalidArgumentError(
  405. 'The model %v has a circular inheritance.',
  406. currModelName,
  407. );
  408. const modelDef =
  409. this.getService(DefinitionRegistry).getModel(currModelName);
  410. const ownRelDefs = modelDef.relations ?? {};
  411. result = {...ownRelDefs, ...result};
  412. if (modelDef.base) recursion(modelDef.base, currModelName);
  413. };
  414. recursion(modelName);
  415. return result;
  416. }
  417. /**
  418. * Get relation definition by name.
  419. *
  420. * @param {string} modelName
  421. * @param {string} relationName
  422. * @returns {object}
  423. */
  424. getRelationDefinitionByName(modelName, relationName) {
  425. const relDefs = this.getRelationsDefinitionInBaseModelHierarchy(modelName);
  426. const relNames = Object.keys(relDefs);
  427. let foundDef;
  428. for (const relName of relNames) {
  429. if (relName === relationName) {
  430. foundDef = relDefs[relName];
  431. break;
  432. }
  433. }
  434. if (!foundDef)
  435. throw new InvalidArgumentError(
  436. 'The model %v does not have relation name %v.',
  437. modelName,
  438. relationName,
  439. );
  440. return foundDef;
  441. }
  442. /**
  443. * Exclude object keys by relation names.
  444. *
  445. * @param {string} modelName
  446. * @param {object} modelData
  447. * @returns {object}
  448. */
  449. excludeObjectKeysByRelationNames(modelName, modelData) {
  450. if (!modelData || typeof modelData !== 'object' || Array.isArray(modelData))
  451. throw new InvalidArgumentError(
  452. 'The second argument of ModelDefinitionUtils.excludeObjectKeysByRelationNames ' +
  453. 'should be an Object, but %v given.',
  454. modelData,
  455. );
  456. const relDefs = this.getRelationsDefinitionInBaseModelHierarchy(modelName);
  457. const relNames = Object.keys(relDefs);
  458. return excludeObjectKeys(modelData, relNames);
  459. }
  460. /**
  461. * Get model name of property value if defined.
  462. *
  463. * @param {string} modelName
  464. * @param {string} propertyName
  465. * @returns {undefined|string}
  466. */
  467. getModelNameOfPropertyValueIfDefined(modelName, propertyName) {
  468. if (!modelName || typeof modelName !== 'string')
  469. throw new InvalidArgumentError(
  470. 'Parameter "modelName" of ' +
  471. 'ModelDefinitionUtils.getModelNameOfPropertyValueIfDefined ' +
  472. 'requires a non-empty String, but %v given.',
  473. modelName,
  474. );
  475. if (!propertyName || typeof propertyName !== 'string')
  476. throw new InvalidArgumentError(
  477. 'Parameter "propertyName" of ' +
  478. 'ModelDefinitionUtils.getModelNameOfPropertyValueIfDefined ' +
  479. 'requires a non-empty String, but %v given.',
  480. propertyName,
  481. );
  482. // если определение свойства не найдено,
  483. // то возвращаем undefined
  484. const propDefs =
  485. this.getPropertiesDefinitionInBaseModelHierarchy(modelName);
  486. const propDef = propDefs[propertyName];
  487. if (!propDef) return undefined;
  488. // если определение свойства является
  489. // объектом, то проверяем тип и возвращаем
  490. // название модели
  491. if (propDef && typeof propDef === 'object') {
  492. // если тип свойства является объектом,
  493. // то возвращаем значение опции "model",
  494. // или undefined
  495. if (propDef.type === DataType.OBJECT) return propDef.model || undefined;
  496. // если тип свойства является массивом,
  497. // то возвращаем значение опции "itemModel",
  498. // или undefined
  499. if (propDef.type === DataType.ARRAY)
  500. return propDef.itemModel || undefined;
  501. }
  502. // если определение свойства не является
  503. // объектом, то возвращаем undefined
  504. return undefined;
  505. }
  506. }