has-one-resolver.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  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 one resolver.
  9. */
  10. export class HasOneResolver 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 HasOneResolver.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 HasOneResolver.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 HasOneResolver.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 HasOneResolver.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 HasOneResolver.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 HasOneResolver.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 HasOneResolver.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 targetBySourceId = new Map();
  87. sourceIds.forEach(sourceId => {
  88. const filter = cloneDeep(scope);
  89. filter.where = {
  90. and: [{[foreignKey]: sourceId}, ...(scope.where ? [scope.where] : [])],
  91. };
  92. filter.limit = 1;
  93. promises.push(
  94. targetRepository.find(filter).then(result => {
  95. if (result.length) targetBySourceId.set(sourceId, result[0]);
  96. }),
  97. );
  98. });
  99. await Promise.all(promises);
  100. Array.from(targetBySourceId.keys()).forEach(sourceId => {
  101. const sources = entities.filter(v => v[sourcePkPropName] === sourceId);
  102. sources.forEach(v => (v[relationName] = targetBySourceId.get(sourceId)));
  103. });
  104. }
  105. /**
  106. * Include polymorphic to.
  107. *
  108. * @param {object[]} entities
  109. * @param {string} sourceName
  110. * @param {string} targetName
  111. * @param {string} relationName
  112. * @param {string} foreignKey
  113. * @param {string} discriminator
  114. * @param {object|undefined} scope
  115. * @returns {Promise<void>}
  116. */
  117. async includePolymorphicTo(
  118. entities,
  119. sourceName,
  120. targetName,
  121. relationName,
  122. foreignKey,
  123. discriminator,
  124. scope = undefined,
  125. ) {
  126. if (!entities || !Array.isArray(entities))
  127. throw new InvalidArgumentError(
  128. 'The parameter "entities" of HasOneResolver.includePolymorphicTo requires ' +
  129. 'an Array of Object, but %v given.',
  130. entities,
  131. );
  132. if (!sourceName || typeof sourceName !== 'string')
  133. throw new InvalidArgumentError(
  134. 'The parameter "sourceName" of HasOneResolver.includePolymorphicTo requires ' +
  135. 'a non-empty String, but %v given.',
  136. sourceName,
  137. );
  138. if (!targetName || typeof targetName !== 'string')
  139. throw new InvalidArgumentError(
  140. 'The parameter "targetName" of HasOneResolver.includePolymorphicTo requires ' +
  141. 'a non-empty String, but %v given.',
  142. targetName,
  143. );
  144. if (!relationName || typeof relationName !== 'string')
  145. throw new InvalidArgumentError(
  146. 'The parameter "relationName" of HasOneResolver.includePolymorphicTo requires ' +
  147. 'a non-empty String, but %v given.',
  148. relationName,
  149. );
  150. if (!foreignKey || typeof foreignKey !== 'string')
  151. throw new InvalidArgumentError(
  152. 'The parameter "foreignKey" of HasOneResolver.includePolymorphicTo requires ' +
  153. 'a non-empty String, but %v given.',
  154. foreignKey,
  155. );
  156. if (!discriminator || typeof discriminator !== 'string')
  157. throw new InvalidArgumentError(
  158. 'The parameter "discriminator" of HasOneResolver.includePolymorphicTo requires ' +
  159. 'a non-empty String, but %v given.',
  160. discriminator,
  161. );
  162. if (scope && (typeof scope !== 'object' || Array.isArray(scope)))
  163. throw new InvalidArgumentError(
  164. 'The provided parameter "scope" of HasOneResolver.includePolymorphicTo ' +
  165. 'should be an Object, but %v given.',
  166. scope,
  167. );
  168. const sourcePkPropName =
  169. this.getService(ModelDefinitionUtils).getPrimaryKeyAsPropertyName(
  170. sourceName,
  171. );
  172. const sourceIds = [];
  173. entities.forEach(entity => {
  174. if (!entity || typeof entity !== 'object' || Array.isArray(entity))
  175. throw new InvalidArgumentError(
  176. 'The parameter "entities" of HasOneResolver.includePolymorphicTo requires ' +
  177. 'an Array of Object, but %v given.',
  178. entity,
  179. );
  180. const sourceId = entity[sourcePkPropName];
  181. if (sourceIds.includes(sourceId)) return;
  182. sourceIds.push(sourceId);
  183. });
  184. const promises = [];
  185. const targetRepository =
  186. this.getService(RepositoryRegistry).getRepository(targetName);
  187. scope = scope ? cloneDeep(scope) : {};
  188. const targetBySourceId = new Map();
  189. sourceIds.forEach(sourceId => {
  190. const filter = cloneDeep(scope);
  191. filter.where = {
  192. and: [
  193. {[foreignKey]: sourceId, [discriminator]: sourceName},
  194. ...(scope.where ? [scope.where] : []),
  195. ],
  196. };
  197. filter.limit = 1;
  198. promises.push(
  199. targetRepository.find(filter).then(result => {
  200. if (result.length) targetBySourceId.set(sourceId, result[0]);
  201. }),
  202. );
  203. });
  204. await Promise.all(promises);
  205. Array.from(targetBySourceId.keys()).forEach(sourceId => {
  206. const sources = entities.filter(v => v[sourcePkPropName] === sourceId);
  207. sources.forEach(v => (v[relationName] = targetBySourceId.get(sourceId)));
  208. });
  209. }
  210. /**
  211. * Include polymorphic by relation name.
  212. *
  213. * @param {object[]} entities
  214. * @param {string} sourceName
  215. * @param {string} targetName
  216. * @param {string} relationName
  217. * @param {string} targetRelationName
  218. * @param {object|undefined} scope
  219. * @returns {Promise<void>}
  220. */
  221. async includePolymorphicByRelationName(
  222. entities,
  223. sourceName,
  224. targetName,
  225. relationName,
  226. targetRelationName,
  227. scope = undefined,
  228. ) {
  229. if (!entities || !Array.isArray(entities))
  230. throw new InvalidArgumentError(
  231. 'The parameter "entities" of HasOneResolver.includePolymorphicByRelationName requires ' +
  232. 'an Array of Object, but %v given.',
  233. entities,
  234. );
  235. if (!sourceName || typeof sourceName !== 'string')
  236. throw new InvalidArgumentError(
  237. 'The parameter "sourceName" of HasOneResolver.includePolymorphicByRelationName requires ' +
  238. 'a non-empty String, but %v given.',
  239. sourceName,
  240. );
  241. if (!targetName || typeof targetName !== 'string')
  242. throw new InvalidArgumentError(
  243. 'The parameter "targetName" of HasOneResolver.includePolymorphicByRelationName requires ' +
  244. 'a non-empty String, but %v given.',
  245. targetName,
  246. );
  247. if (!relationName || typeof relationName !== 'string')
  248. throw new InvalidArgumentError(
  249. 'The parameter "relationName" of HasOneResolver.includePolymorphicByRelationName requires ' +
  250. 'a non-empty String, but %v given.',
  251. relationName,
  252. );
  253. if (!targetRelationName || typeof targetRelationName !== 'string')
  254. throw new InvalidArgumentError(
  255. 'The parameter "targetRelationName" of HasOneResolver.includePolymorphicByRelationName requires ' +
  256. 'a non-empty String, but %v given.',
  257. targetRelationName,
  258. );
  259. if (scope && (typeof scope !== 'object' || Array.isArray(scope)))
  260. throw new InvalidArgumentError(
  261. 'The provided parameter "scope" of HasOneResolver.includePolymorphicByRelationName ' +
  262. 'should be an Object, but %v given.',
  263. scope,
  264. );
  265. const targetRelationDef = this.getService(
  266. ModelDefinitionUtils,
  267. ).getRelationDefinitionByName(targetName, targetRelationName);
  268. if (targetRelationDef.type !== RelationType.BELONGS_TO)
  269. throw new InvalidArgumentError(
  270. 'The relation %v of the model %v is a polymorphic "hasOne" relation, ' +
  271. 'so it requires the target relation %v to be a polymorphic "belongsTo", ' +
  272. 'but %v type given.',
  273. relationName,
  274. sourceName,
  275. targetRelationName,
  276. targetRelationDef.type,
  277. );
  278. if (!targetRelationDef.polymorphic)
  279. throw new InvalidArgumentError(
  280. 'The relation %v of the model %v is a polymorphic "hasOne" relation, ' +
  281. 'so it requires the target relation %v to be a polymorphic too.',
  282. relationName,
  283. sourceName,
  284. targetRelationName,
  285. );
  286. const foreignKey =
  287. targetRelationDef.foreignKey || `${targetRelationName}Id`;
  288. const discriminator =
  289. targetRelationDef.discriminator || `${targetRelationName}Type`;
  290. return this.includePolymorphicTo(
  291. entities,
  292. sourceName,
  293. targetName,
  294. relationName,
  295. foreignKey,
  296. discriminator,
  297. scope,
  298. );
  299. }
  300. }