relations-definition-validator.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. import {Service} from '@e22m4u/service';
  2. import {RelationType} from './relation-type.js';
  3. import {RelationType as Type} from './relation-type.js';
  4. import {InvalidArgumentError} from '../../../errors/index.js';
  5. /**
  6. * Relations definition validator.
  7. */
  8. export class RelationsDefinitionValidator extends Service {
  9. /**
  10. * Validate.
  11. *
  12. * @param {string} modelName
  13. * @param {object} relDefs
  14. */
  15. validate(modelName, relDefs) {
  16. if (!modelName || typeof modelName !== 'string')
  17. throw new InvalidArgumentError(
  18. 'A first argument of RelationsDefinitionValidator.validate ' +
  19. 'should be a non-empty String, but %v given.',
  20. modelName,
  21. );
  22. if (!relDefs || typeof relDefs !== 'object' || Array.isArray(relDefs))
  23. throw new InvalidArgumentError(
  24. 'The provided option "relations" of the model %v ' +
  25. 'should be an Object, but %v given.',
  26. modelName,
  27. relDefs,
  28. );
  29. const relNames = Object.keys(relDefs);
  30. relNames.forEach(relName => {
  31. const relDef = relDefs[relName];
  32. this._validateRelation(modelName, relName, relDef);
  33. });
  34. }
  35. /**
  36. * Validate relation.
  37. *
  38. * @param {string} modelName
  39. * @param {string} relName
  40. * @param {object} relDef
  41. */
  42. _validateRelation(modelName, relName, relDef) {
  43. if (!modelName || typeof modelName !== 'string')
  44. throw new InvalidArgumentError(
  45. 'A first argument of RelationsDefinitionValidator._validateRelation ' +
  46. 'should be a non-empty String, but %v given.',
  47. modelName,
  48. );
  49. if (!relName || typeof relName !== 'string')
  50. throw new InvalidArgumentError(
  51. 'The relation name of the model %v should be ' +
  52. 'a non-empty String, but %v given.',
  53. modelName,
  54. relName,
  55. );
  56. if (!relDef || typeof relDef !== 'object' || Array.isArray(relDef))
  57. throw new InvalidArgumentError(
  58. 'The relation %v of the model %v should be an Object, but %v given.',
  59. relName,
  60. modelName,
  61. relDef,
  62. );
  63. if (!relDef.type || !Object.values(Type).includes(relDef.type))
  64. throw new InvalidArgumentError(
  65. 'The relation %v of the model %v requires the option "type" ' +
  66. 'to have one of relation types: %l, but %v given.',
  67. relName,
  68. modelName,
  69. Object.values(Type),
  70. relDef.type,
  71. );
  72. this._validateBelongsTo(modelName, relName, relDef);
  73. this._validateHasOne(modelName, relName, relDef);
  74. this._validateHasMany(modelName, relName, relDef);
  75. this._validateReferencesMany(modelName, relName, relDef);
  76. }
  77. /**
  78. * Validate "belongsTo".
  79. *
  80. * @example The regular "belongsTo" relation.
  81. * ```
  82. * {
  83. * type: RelationType.BELONGS_TO,
  84. * model: 'model',
  85. * foreignKey: 'modelId', // optional
  86. * }
  87. * ```
  88. *
  89. * @example The polymorphic "belongsTo" relation.
  90. * ```
  91. * {
  92. * type: RelationType.BELONGS_TO,
  93. * polymorphic: true,
  94. * foreignKey: 'referenceId', // optional
  95. * discriminator: 'referenceType, // optional
  96. * }
  97. * ```
  98. *
  99. * @param {string} modelName
  100. * @param {string} relName
  101. * @param {object} relDef
  102. * @private
  103. */
  104. _validateBelongsTo(modelName, relName, relDef) {
  105. if (relDef.type !== Type.BELONGS_TO) return;
  106. if (relDef.polymorphic) {
  107. // A polymorphic "belongsTo" relation.
  108. if (typeof relDef.polymorphic !== 'boolean')
  109. throw new InvalidArgumentError(
  110. 'The relation %v of the model %v has the type "belongsTo", ' +
  111. 'so it expects the option "polymorphic" to be a Boolean, ' +
  112. 'but %v given.',
  113. relName,
  114. modelName,
  115. relDef.polymorphic,
  116. );
  117. if (relDef.foreignKey && typeof relDef.foreignKey !== 'string')
  118. throw new InvalidArgumentError(
  119. 'The relation %v of the model %v is a polymorphic "belongsTo" relation, ' +
  120. 'so it expects the provided option "foreignKey" to be a String, ' +
  121. 'but %v given.',
  122. relName,
  123. modelName,
  124. relDef.foreignKey,
  125. );
  126. if (relDef.discriminator && typeof relDef.discriminator !== 'string')
  127. throw new InvalidArgumentError(
  128. 'The relation %v of the model %v is a polymorphic "belongsTo" relation, ' +
  129. 'so it expects the provided option "discriminator" to be a String, ' +
  130. 'but %v given.',
  131. relName,
  132. modelName,
  133. relDef.discriminator,
  134. );
  135. } else {
  136. // A regular "belongsTo" relation.
  137. if (!relDef.model || typeof relDef.model !== 'string')
  138. throw new InvalidArgumentError(
  139. 'The relation %v of the model %v has the type "belongsTo", ' +
  140. 'so it requires the option "model" to be a non-empty String, ' +
  141. 'but %v given.',
  142. relName,
  143. modelName,
  144. relDef.model,
  145. );
  146. if (relDef.foreignKey && typeof relDef.foreignKey !== 'string')
  147. throw new InvalidArgumentError(
  148. 'The relation %v of the model %v has the type "belongsTo", ' +
  149. 'so it expects the provided option "foreignKey" to be a String, ' +
  150. 'but %v given.',
  151. relName,
  152. modelName,
  153. relDef.foreignKey,
  154. );
  155. if (relDef.discriminator)
  156. throw new InvalidArgumentError(
  157. 'The relation %v of the model %v is a non-polymorphic "belongsTo" relation, ' +
  158. 'so it should not have the option "discriminator" to be provided.',
  159. relName,
  160. modelName,
  161. );
  162. }
  163. }
  164. /**
  165. * Validate "hasOne".
  166. *
  167. * @example The regular "hasOne" relation.
  168. * ```
  169. * {
  170. * type: RelationType.HAS_ONE,
  171. * model: 'model',
  172. * foreignKey: 'modelId',
  173. * }
  174. * ```
  175. *
  176. * @example The polymorphic "hasOne" relation with a target relation name.
  177. * ```
  178. * {
  179. * type: RelationType.HAS_ONE,
  180. * model: 'model',
  181. * polymorphic: 'reference',
  182. * }
  183. * ```
  184. *
  185. * @example The polymorphic "hasOne" relation with target relation keys.
  186. * ```
  187. * {
  188. * type: RelationType.HAS_ONE,
  189. * model: 'model',
  190. * polymorphic: true,
  191. * foreignKey: 'referenceId',
  192. * discriminator: 'referenceType,
  193. * }
  194. * ```
  195. *
  196. * @param {string} modelName
  197. * @param {string} relName
  198. * @param {object} relDef
  199. * @private
  200. */
  201. _validateHasOne(modelName, relName, relDef) {
  202. if (relDef.type !== RelationType.HAS_ONE) return;
  203. if (!relDef.model || typeof relDef.model !== 'string')
  204. throw new InvalidArgumentError(
  205. 'The relation %v of the model %v has the type "hasOne", ' +
  206. 'so it requires the option "model" to be a non-empty String, ' +
  207. 'but %v given.',
  208. relName,
  209. modelName,
  210. relDef.model,
  211. );
  212. if (relDef.polymorphic) {
  213. if (typeof relDef.polymorphic === 'string') {
  214. // A polymorphic "hasOne" relation with a target relation name.
  215. if (relDef.foreignKey)
  216. throw new InvalidArgumentError(
  217. 'The relation %v of the model %v has the option "polymorphic" with ' +
  218. 'a String value, so it should not have the option "foreignKey" ' +
  219. 'to be provided.',
  220. relName,
  221. modelName,
  222. );
  223. if (relDef.discriminator)
  224. throw new InvalidArgumentError(
  225. 'The relation %v of the model %v has the option "polymorphic" with ' +
  226. 'a String value, so it should not have the option "discriminator" ' +
  227. 'to be provided.',
  228. relName,
  229. modelName,
  230. );
  231. } else if (typeof relDef.polymorphic === 'boolean') {
  232. // A polymorphic "hasOne" relation with target relation keys.
  233. if (!relDef.foreignKey || typeof relDef.foreignKey !== 'string')
  234. throw new InvalidArgumentError(
  235. 'The relation %v of the model %v has the option "polymorphic" ' +
  236. 'with "true" value, so it requires the option "foreignKey" ' +
  237. 'to be a non-empty String, but %v given.',
  238. relName,
  239. modelName,
  240. relDef.foreignKey,
  241. );
  242. if (!relDef.discriminator || typeof relDef.discriminator !== 'string')
  243. throw new InvalidArgumentError(
  244. 'The relation %v of the model %v has the option "polymorphic" ' +
  245. 'with "true" value, so it requires the option "discriminator" ' +
  246. 'to be a non-empty String, but %v given.',
  247. relName,
  248. modelName,
  249. relDef.discriminator,
  250. );
  251. } else {
  252. throw new InvalidArgumentError(
  253. 'The relation %v of the model %v has the type "hasOne", ' +
  254. 'so it expects the provided option "polymorphic" to be ' +
  255. 'a String or a Boolean, but %v given.',
  256. relName,
  257. modelName,
  258. relDef.polymorphic,
  259. );
  260. }
  261. } else {
  262. // A regular "hasOne" relation.
  263. if (!relDef.foreignKey || typeof relDef.foreignKey !== 'string')
  264. throw new InvalidArgumentError(
  265. 'The relation %v of the model %v has the type "hasOne", ' +
  266. 'so it requires the option "foreignKey" to be a non-empty String, ' +
  267. 'but %v given.',
  268. relName,
  269. modelName,
  270. relDef.foreignKey,
  271. );
  272. if (relDef.discriminator)
  273. throw new InvalidArgumentError(
  274. 'The relation %v of the model %v is a non-polymorphic "hasOne" relation, ' +
  275. 'so it should not have the option "discriminator" to be provided.',
  276. relName,
  277. modelName,
  278. );
  279. }
  280. }
  281. /**
  282. * Validate "hasMany".
  283. *
  284. * @example The regular "hasMany" relation.
  285. * ```
  286. * {
  287. * type: RelationType.HAS_MANY,
  288. * model: 'model',
  289. * foreignKey: 'modelId',
  290. * }
  291. * ```
  292. *
  293. * @example The polymorphic "hasMany" relation with a target relation name.
  294. * ```
  295. * {
  296. * type: RelationType.HAS_MANY,
  297. * model: 'model',
  298. * polymorphic: 'reference',
  299. * }
  300. * ```
  301. *
  302. * @example The polymorphic "hasMany" relation with target relation keys.
  303. * ```
  304. * {
  305. * type: RelationType.HAS_MANY,
  306. * model: 'model',
  307. * polymorphic: true,
  308. * foreignKey: 'referenceId',
  309. * discriminator: 'referenceType,
  310. * }
  311. * ```
  312. *
  313. * @param {string} modelName
  314. * @param {string} relName
  315. * @param {object} relDef
  316. * @private
  317. */
  318. _validateHasMany(modelName, relName, relDef) {
  319. if (relDef.type !== RelationType.HAS_MANY) return;
  320. if (!relDef.model || typeof relDef.model !== 'string')
  321. throw new InvalidArgumentError(
  322. 'The relation %v of the model %v has the type "hasMany", ' +
  323. 'so it requires the option "model" to be a non-empty String, ' +
  324. 'but %v given.',
  325. relName,
  326. modelName,
  327. relDef.model,
  328. );
  329. if (relDef.polymorphic) {
  330. if (typeof relDef.polymorphic === 'string') {
  331. // A polymorphic "hasMany" relation with a target relation name.
  332. if (relDef.foreignKey)
  333. throw new InvalidArgumentError(
  334. 'The relation %v of the model %v has the option "polymorphic" with ' +
  335. 'a String value, so it should not have the option "foreignKey" ' +
  336. 'to be provided.',
  337. relName,
  338. modelName,
  339. );
  340. if (relDef.discriminator)
  341. throw new InvalidArgumentError(
  342. 'The relation %v of the model %v has the option "polymorphic" with ' +
  343. 'a String value, so it should not have the option "discriminator" ' +
  344. 'to be provided.',
  345. relName,
  346. modelName,
  347. );
  348. } else if (typeof relDef.polymorphic === 'boolean') {
  349. // A polymorphic "hasMany" relation with target relation keys.
  350. if (!relDef.foreignKey || typeof relDef.foreignKey !== 'string')
  351. throw new InvalidArgumentError(
  352. 'The relation %v of the model %v has the option "polymorphic" ' +
  353. 'with "true" value, so it requires the option "foreignKey" ' +
  354. 'to be a non-empty String, but %v given.',
  355. relName,
  356. modelName,
  357. relDef.foreignKey,
  358. );
  359. if (!relDef.discriminator || typeof relDef.discriminator !== 'string')
  360. throw new InvalidArgumentError(
  361. 'The relation %v of the model %v has the option "polymorphic" ' +
  362. 'with "true" value, so it requires the option "discriminator" ' +
  363. 'to be a non-empty String, but %v given.',
  364. relName,
  365. modelName,
  366. relDef.discriminator,
  367. );
  368. } else {
  369. throw new InvalidArgumentError(
  370. 'The relation %v of the model %v has the type "hasMany", ' +
  371. 'so it expects the provided option "polymorphic" to be ' +
  372. 'a String or a Boolean, but %v given.',
  373. relName,
  374. modelName,
  375. relDef.polymorphic,
  376. );
  377. }
  378. } else {
  379. // A regular "hasMany" relation.
  380. if (!relDef.foreignKey || typeof relDef.foreignKey !== 'string')
  381. throw new InvalidArgumentError(
  382. 'The relation %v of the model %v has the type "hasMany", ' +
  383. 'so it requires the option "foreignKey" to be a non-empty String, ' +
  384. 'but %v given.',
  385. relName,
  386. modelName,
  387. relDef.foreignKey,
  388. );
  389. if (relDef.discriminator)
  390. throw new InvalidArgumentError(
  391. 'The relation %v of the model %v is a non-polymorphic "hasMany" relation, ' +
  392. 'so it should not have the option "discriminator" to be provided.',
  393. relName,
  394. modelName,
  395. );
  396. }
  397. }
  398. /**
  399. * Validate "referencesMany".
  400. *
  401. * @example
  402. * ```
  403. * {
  404. * type: RelationType.REFERENCES_MANY,
  405. * model: 'model',
  406. * foreignKey: 'modelIds', // optional
  407. * }
  408. * ```
  409. *
  410. * @param {string} modelName
  411. * @param {string} relName
  412. * @param {object} relDef
  413. * @private
  414. */
  415. _validateReferencesMany(modelName, relName, relDef) {
  416. if (relDef.type !== Type.REFERENCES_MANY) return;
  417. if (!relDef.model || typeof relDef.model !== 'string')
  418. throw new InvalidArgumentError(
  419. 'The relation %v of the model %v has the type "referencesMany", ' +
  420. 'so it requires the option "model" to be a non-empty String, ' +
  421. 'but %v given.',
  422. relName,
  423. modelName,
  424. relDef.model,
  425. );
  426. if (relDef.foreignKey && typeof relDef.foreignKey !== 'string')
  427. throw new InvalidArgumentError(
  428. 'The relation %v of the model %v has the type "referencesMany", ' +
  429. 'so it expects the provided option "foreignKey" to be a String, ' +
  430. 'but %v given.',
  431. relName,
  432. modelName,
  433. relDef.foreignKey,
  434. );
  435. if (relDef.discriminator)
  436. throw new InvalidArgumentError(
  437. 'The relation %v of the model %v has the type "referencesMany", ' +
  438. 'so it should not have the option "discriminator" to be provided.',
  439. relName,
  440. modelName,
  441. );
  442. }
  443. }