belongs-to-resolver.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. import {Service} from '@e22m4u/js-service';
  2. import {cloneDeep} from '../utils/index.js';
  3. import {singularize} from '../utils/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. * Belongs to resolver.
  9. */
  10. export class BelongsToResolver 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|undefined} foreignKey
  19. * @param {object|undefined} scope
  20. * @returns {Promise<void>}
  21. */
  22. async includeTo(
  23. entities,
  24. sourceName,
  25. targetName,
  26. relationName,
  27. foreignKey = undefined,
  28. scope = undefined,
  29. ) {
  30. if (!entities || !Array.isArray(entities))
  31. throw new InvalidArgumentError(
  32. 'The parameter "entities" of BelongsToResolver.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 BelongsToResolver.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 BelongsToResolver.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 BelongsToResolver.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 provided parameter "foreignKey" of BelongsToResolver.includeTo ' +
  57. 'should be a 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 BelongsToResolver.includeTo ' +
  63. 'should be an Object, but %v given.',
  64. scope,
  65. );
  66. if (foreignKey == null) foreignKey = `${relationName}Id`;
  67. const targetIds = entities.reduce((acc, entity) => {
  68. if (!entity || typeof entity !== 'object' || Array.isArray(entity))
  69. throw new InvalidArgumentError(
  70. 'The parameter "entities" of BelongsToResolver.includeTo requires ' +
  71. 'an Array of Object, but %v given.',
  72. entity,
  73. );
  74. const targetId = entity[foreignKey];
  75. return targetId != null ? [...acc, targetId] : acc;
  76. }, []);
  77. const targetRepository =
  78. this.getService(RepositoryRegistry).getRepository(targetName);
  79. const targetPkPropName =
  80. this.getService(ModelDefinitionUtils).getPrimaryKeyAsPropertyName(
  81. targetName,
  82. );
  83. scope = scope ? cloneDeep(scope) : {};
  84. const filter = cloneDeep(scope);
  85. filter.where = {
  86. and: [
  87. {[targetPkPropName]: {inq: targetIds}},
  88. ...(scope.where ? [scope.where] : []),
  89. ],
  90. };
  91. const targets = await targetRepository.find(filter);
  92. entities.forEach(entity => {
  93. const target = targets.find(
  94. e => e[targetPkPropName] === entity[foreignKey],
  95. );
  96. if (target) entity[relationName] = target;
  97. });
  98. }
  99. /**
  100. * Include polymorphic to.
  101. *
  102. * @param {object[]} entities
  103. * @param {string} sourceName
  104. * @param {string} relationName
  105. * @param {string|undefined} foreignKey
  106. * @param {string|undefined} discriminator
  107. * @param {object|undefined} scope
  108. * @returns {Promise<void>}
  109. */
  110. async includePolymorphicTo(
  111. entities,
  112. sourceName,
  113. relationName,
  114. foreignKey = undefined,
  115. discriminator = undefined,
  116. scope = undefined,
  117. ) {
  118. if (!entities || !Array.isArray(entities))
  119. throw new InvalidArgumentError(
  120. 'The parameter "entities" of BelongsToResolver.includePolymorphicTo ' +
  121. 'requires an Array of Object, but %v given.',
  122. entities,
  123. );
  124. if (!sourceName || typeof sourceName !== 'string')
  125. throw new InvalidArgumentError(
  126. 'The parameter "sourceName" of BelongsToResolver.includePolymorphicTo ' +
  127. 'requires a non-empty String, but %v given.',
  128. sourceName,
  129. );
  130. if (!relationName || typeof relationName !== 'string')
  131. throw new InvalidArgumentError(
  132. 'The parameter "relationName" of BelongsToResolver.includePolymorphicTo ' +
  133. 'requires a non-empty String, but %v given.',
  134. relationName,
  135. );
  136. if (foreignKey && typeof foreignKey !== 'string')
  137. throw new InvalidArgumentError(
  138. 'The provided parameter "foreignKey" of BelongsToResolver.includePolymorphicTo ' +
  139. 'should be a String, but %v given.',
  140. foreignKey,
  141. );
  142. if (discriminator && typeof discriminator !== 'string')
  143. throw new InvalidArgumentError(
  144. 'The provided parameter "discriminator" of BelongsToResolver.includePolymorphicTo ' +
  145. 'should be a String, but %v given.',
  146. discriminator,
  147. );
  148. if (scope && (typeof scope !== 'object' || Array.isArray(scope)))
  149. throw new InvalidArgumentError(
  150. 'The provided parameter "scope" of BelongsToResolver.includePolymorphicTo ' +
  151. 'should be an Object, but %v given.',
  152. scope,
  153. );
  154. if (foreignKey == null) {
  155. const singularRelationName = singularize(relationName);
  156. foreignKey = `${singularRelationName}Id`;
  157. }
  158. if (discriminator == null) {
  159. const singularRelationName = singularize(relationName);
  160. discriminator = `${singularRelationName}Type`;
  161. }
  162. const targetIdsByTargetName = {};
  163. entities.forEach(entity => {
  164. if (!entity || typeof entity !== 'object' || Array.isArray(entity))
  165. throw new InvalidArgumentError(
  166. 'The parameter "entities" of BelongsToResolver.includePolymorphicTo requires ' +
  167. 'an Array of Object, but %v given.',
  168. entity,
  169. );
  170. const targetId = entity[foreignKey];
  171. const targetName = entity[discriminator];
  172. if (targetId == null || targetName == null) return;
  173. if (targetIdsByTargetName[targetName] == null)
  174. targetIdsByTargetName[targetName] = [];
  175. if (!targetIdsByTargetName[targetName].includes(targetId))
  176. targetIdsByTargetName[targetName].push(targetId);
  177. });
  178. const promises = [];
  179. const targetNames = Object.keys(targetIdsByTargetName);
  180. scope = scope ? cloneDeep(scope) : {};
  181. const targetEntitiesByTargetNames = {};
  182. targetNames.forEach(targetName => {
  183. let targetRepository;
  184. try {
  185. targetRepository =
  186. this.getService(RepositoryRegistry).getRepository(targetName);
  187. } catch (error) {
  188. if (error instanceof InvalidArgumentError) {
  189. if (
  190. error.message === `The model "${targetName}" is not defined.` ||
  191. error.message ===
  192. `The model "${targetName}" does not have a specified datasource.`
  193. ) {
  194. return;
  195. }
  196. } else {
  197. throw error;
  198. }
  199. }
  200. const targetPkPropName =
  201. this.getService(ModelDefinitionUtils).getPrimaryKeyAsPropertyName(
  202. targetName,
  203. );
  204. const targetFilter = cloneDeep(scope);
  205. const targetIds = targetIdsByTargetName[targetName];
  206. targetFilter.where = {
  207. and: [
  208. {[targetPkPropName]: {inq: targetIds}},
  209. ...(scope.where ? [scope.where] : []),
  210. ],
  211. };
  212. const promise = targetRepository.find(targetFilter).then(result => {
  213. targetEntitiesByTargetNames[targetName] = [
  214. ...(targetEntitiesByTargetNames[targetName] ?? []),
  215. ...result,
  216. ];
  217. });
  218. promises.push(promise);
  219. });
  220. await Promise.all(promises);
  221. entities.forEach(entity => {
  222. const targetId = entity[foreignKey];
  223. const targetName = entity[discriminator];
  224. if (
  225. targetId == null ||
  226. targetName == null ||
  227. targetEntitiesByTargetNames[targetName] == null
  228. ) {
  229. return;
  230. }
  231. const targetEntities = targetEntitiesByTargetNames[targetName] ?? [];
  232. const targetPkPropName =
  233. this.getService(ModelDefinitionUtils).getPrimaryKeyAsPropertyName(
  234. targetName,
  235. );
  236. const target = targetEntities.find(e => e[targetPkPropName] === targetId);
  237. if (target) entity[relationName] = target;
  238. });
  239. }
  240. }