has-many-resolver.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. import {Service} from '@e22m4u/js-service';
  2. import {cloneDeep} from '../utils/index.js';
  3. import {RelationType} from '../definition/index.js';
  4. import {InvalidArgumentError} from '../errors/index.js';
  5. import {RepositoryRegistry} from '../repository/index.js';
  6. import {ModelDefinitionUtils} from '../definition/index.js';
  7. /**
  8. * Has many resolver.
  9. */
  10. export class HasManyResolver extends Service {
  11. /**
  12. * Include to.
  13. *
  14. * @param {object[]} entities
  15. * @param {string} sourceName
  16. * @param {string} targetName
  17. * @param {string} relationName
  18. * @param {string} foreignKey
  19. * @param {object|undefined} scope
  20. * @returns {Promise<void>}
  21. */
  22. async includeTo(
  23. entities,
  24. sourceName,
  25. targetName,
  26. relationName,
  27. foreignKey,
  28. scope = undefined,
  29. ) {
  30. if (!entities || !Array.isArray(entities))
  31. throw new InvalidArgumentError(
  32. 'The parameter "entities" of HasManyResolver.includeTo requires ' +
  33. 'an Array of Object, but %v given.',
  34. entities,
  35. );
  36. if (!sourceName || typeof sourceName !== 'string')
  37. throw new InvalidArgumentError(
  38. 'The parameter "sourceName" of HasManyResolver.includeTo requires ' +
  39. 'a non-empty String, but %v given.',
  40. sourceName,
  41. );
  42. if (!targetName || typeof targetName !== 'string')
  43. throw new InvalidArgumentError(
  44. 'The parameter "targetName" of HasManyResolver.includeTo requires ' +
  45. 'a non-empty String, but %v given.',
  46. targetName,
  47. );
  48. if (!relationName || typeof relationName !== 'string')
  49. throw new InvalidArgumentError(
  50. 'The parameter "relationName" of HasManyResolver.includeTo requires ' +
  51. 'a non-empty String, but %v given.',
  52. relationName,
  53. );
  54. if (!foreignKey || typeof foreignKey !== 'string')
  55. throw new InvalidArgumentError(
  56. 'The parameter "foreignKey" of HasManyResolver.includeTo requires ' +
  57. 'a non-empty String, but %v given.',
  58. foreignKey,
  59. );
  60. if (scope && (typeof scope !== 'object' || Array.isArray(scope)))
  61. throw new InvalidArgumentError(
  62. 'The provided parameter "scope" of HasManyResolver.includeTo ' +
  63. 'should be an Object, but %v given.',
  64. scope,
  65. );
  66. const sourcePkPropName =
  67. this.getService(ModelDefinitionUtils).getPrimaryKeyAsPropertyName(
  68. sourceName,
  69. );
  70. const sourceIds = [];
  71. entities.forEach(entity => {
  72. if (!entity || typeof entity !== 'object' || Array.isArray(entity))
  73. throw new InvalidArgumentError(
  74. 'The parameter "entities" of HasManyResolver.includeTo requires ' +
  75. 'an Array of Object, but %v given.',
  76. entity,
  77. );
  78. const sourceId = entity[sourcePkPropName];
  79. if (sourceIds.includes(sourceId)) return;
  80. sourceIds.push(sourceId);
  81. });
  82. const promises = [];
  83. const targetRepository =
  84. this.getService(RepositoryRegistry).getRepository(targetName);
  85. scope = scope ? cloneDeep(scope) : {};
  86. const targetsBySourceId = new Map();
  87. sourceIds.forEach(sourceId => {
  88. const filter = cloneDeep(scope);
  89. filter.where = {
  90. and: [{[foreignKey]: sourceId}, ...(scope.where ? [scope.where] : [])],
  91. };
  92. promises.push(
  93. targetRepository.find(filter).then(result => {
  94. if (result.length) {
  95. let targets = targetsBySourceId.get(sourceId) ?? [];
  96. targets = [...targets, ...result];
  97. targetsBySourceId.set(sourceId, targets);
  98. }
  99. }),
  100. );
  101. });
  102. await Promise.all(promises);
  103. entities.forEach(entity => {
  104. const sourceId = entity[sourcePkPropName];
  105. entity[relationName] = targetsBySourceId.get(sourceId) ?? [];
  106. });
  107. }
  108. /**
  109. * Include polymorphic to.
  110. *
  111. * @param {object[]} entities
  112. * @param {string} sourceName
  113. * @param {string} targetName
  114. * @param {string} relationName
  115. * @param {string} foreignKey
  116. * @param {string} discriminator
  117. * @param {object|undefined} scope
  118. * @returns {Promise<void>}
  119. */
  120. async includePolymorphicTo(
  121. entities,
  122. sourceName,
  123. targetName,
  124. relationName,
  125. foreignKey,
  126. discriminator,
  127. scope = undefined,
  128. ) {
  129. if (!entities || !Array.isArray(entities))
  130. throw new InvalidArgumentError(
  131. 'The parameter "entities" of HasManyResolver.includePolymorphicTo requires ' +
  132. 'an Array of Object, but %v given.',
  133. entities,
  134. );
  135. if (!sourceName || typeof sourceName !== 'string')
  136. throw new InvalidArgumentError(
  137. 'The parameter "sourceName" of HasManyResolver.includePolymorphicTo requires ' +
  138. 'a non-empty String, but %v given.',
  139. sourceName,
  140. );
  141. if (!targetName || typeof targetName !== 'string')
  142. throw new InvalidArgumentError(
  143. 'The parameter "targetName" of HasManyResolver.includePolymorphicTo requires ' +
  144. 'a non-empty String, but %v given.',
  145. targetName,
  146. );
  147. if (!relationName || typeof relationName !== 'string')
  148. throw new InvalidArgumentError(
  149. 'The parameter "relationName" of HasManyResolver.includePolymorphicTo requires ' +
  150. 'a non-empty String, but %v given.',
  151. relationName,
  152. );
  153. if (!foreignKey || typeof foreignKey !== 'string')
  154. throw new InvalidArgumentError(
  155. 'The parameter "foreignKey" of HasManyResolver.includePolymorphicTo requires ' +
  156. 'a non-empty String, but %v given.',
  157. foreignKey,
  158. );
  159. if (!discriminator || typeof discriminator !== 'string')
  160. throw new InvalidArgumentError(
  161. 'The parameter "discriminator" of HasManyResolver.includePolymorphicTo requires ' +
  162. 'a non-empty String, but %v given.',
  163. discriminator,
  164. );
  165. if (scope && (typeof scope !== 'object' || Array.isArray(scope)))
  166. throw new InvalidArgumentError(
  167. 'The provided parameter "scope" of HasManyResolver.includePolymorphicTo ' +
  168. 'should be an Object, but %v given.',
  169. scope,
  170. );
  171. const sourcePkPropName =
  172. this.getService(ModelDefinitionUtils).getPrimaryKeyAsPropertyName(
  173. sourceName,
  174. );
  175. const sourceIds = [];
  176. entities.forEach(entity => {
  177. if (!entity || typeof entity !== 'object' || Array.isArray(entity))
  178. throw new InvalidArgumentError(
  179. 'The parameter "entities" of HasManyResolver.includePolymorphicTo requires ' +
  180. 'an Array of Object, but %v given.',
  181. entity,
  182. );
  183. const sourceId = entity[sourcePkPropName];
  184. if (sourceIds.includes(sourceId)) return;
  185. sourceIds.push(sourceId);
  186. });
  187. const promises = [];
  188. const targetRepository =
  189. this.getService(RepositoryRegistry).getRepository(targetName);
  190. scope = scope ? cloneDeep(scope) : {};
  191. const targetsBySourceId = new Map();
  192. sourceIds.forEach(sourceId => {
  193. const filter = cloneDeep(scope);
  194. filter.where = {
  195. and: [
  196. {[foreignKey]: sourceId, [discriminator]: sourceName},
  197. ...(scope.where ? [scope.where] : []),
  198. ],
  199. };
  200. promises.push(
  201. targetRepository.find(filter).then(result => {
  202. if (result.length) {
  203. let targets = targetsBySourceId.get(sourceId) ?? [];
  204. targets = [...targets, ...result];
  205. targetsBySourceId.set(sourceId, targets);
  206. }
  207. }),
  208. );
  209. });
  210. await Promise.all(promises);
  211. entities.forEach(entity => {
  212. const sourceId = entity[sourcePkPropName];
  213. entity[relationName] = targetsBySourceId.get(sourceId) ?? [];
  214. });
  215. }
  216. /**
  217. * Include polymorphic by relation name.
  218. *
  219. * @param {object[]} entities
  220. * @param {string} sourceName
  221. * @param {string} targetName
  222. * @param {string} relationName
  223. * @param {string} targetRelationName
  224. * @param {object|undefined} scope
  225. * @returns {Promise<void>}
  226. */
  227. async includePolymorphicByRelationName(
  228. entities,
  229. sourceName,
  230. targetName,
  231. relationName,
  232. targetRelationName,
  233. scope = undefined,
  234. ) {
  235. if (!entities || !Array.isArray(entities))
  236. throw new InvalidArgumentError(
  237. 'The parameter "entities" of HasManyResolver.includePolymorphicByRelationName requires ' +
  238. 'an Array of Object, but %v given.',
  239. entities,
  240. );
  241. if (!sourceName || typeof sourceName !== 'string')
  242. throw new InvalidArgumentError(
  243. 'The parameter "sourceName" of HasManyResolver.includePolymorphicByRelationName requires ' +
  244. 'a non-empty String, but %v given.',
  245. sourceName,
  246. );
  247. if (!targetName || typeof targetName !== 'string')
  248. throw new InvalidArgumentError(
  249. 'The parameter "targetName" of HasManyResolver.includePolymorphicByRelationName requires ' +
  250. 'a non-empty String, but %v given.',
  251. targetName,
  252. );
  253. if (!relationName || typeof relationName !== 'string')
  254. throw new InvalidArgumentError(
  255. 'The parameter "relationName" of HasManyResolver.includePolymorphicByRelationName requires ' +
  256. 'a non-empty String, but %v given.',
  257. relationName,
  258. );
  259. if (!targetRelationName || typeof targetRelationName !== 'string')
  260. throw new InvalidArgumentError(
  261. 'The parameter "targetRelationName" of HasManyResolver.includePolymorphicByRelationName requires ' +
  262. 'a non-empty String, but %v given.',
  263. targetRelationName,
  264. );
  265. if (scope && (typeof scope !== 'object' || Array.isArray(scope)))
  266. throw new InvalidArgumentError(
  267. 'The provided parameter "scope" of HasManyResolver.includePolymorphicByRelationName ' +
  268. 'should be an Object, but %v given.',
  269. scope,
  270. );
  271. const targetRelationDef = this.getService(
  272. ModelDefinitionUtils,
  273. ).getRelationDefinitionByName(targetName, targetRelationName);
  274. if (targetRelationDef.type !== RelationType.BELONGS_TO)
  275. throw new InvalidArgumentError(
  276. 'The relation %v of the model %v is a polymorphic "hasMany" relation, ' +
  277. 'so it requires the target relation %v to be a polymorphic "belongsTo", ' +
  278. 'but %v type given.',
  279. relationName,
  280. sourceName,
  281. targetRelationName,
  282. targetRelationDef.type,
  283. );
  284. if (!targetRelationDef.polymorphic)
  285. throw new InvalidArgumentError(
  286. 'The relation %v of the model %v is a polymorphic "hasMany" relation, ' +
  287. 'so it requires the target relation %v to be a polymorphic too.',
  288. relationName,
  289. sourceName,
  290. targetRelationName,
  291. );
  292. const foreignKey =
  293. targetRelationDef.foreignKey || `${targetRelationName}Id`;
  294. const discriminator =
  295. targetRelationDef.discriminator || `${targetRelationName}Type`;
  296. return this.includePolymorphicTo(
  297. entities,
  298. sourceName,
  299. targetName,
  300. relationName,
  301. foreignKey,
  302. discriminator,
  303. scope,
  304. );
  305. }
  306. }