UpsertNode.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. const { difference } = require('../../utils/objectUtils');
  2. const { isTempColumn } = require('../../utils/tmpColumnUtils');
  3. const BelongsToOneRelation = require('../../relations/belongsToOne/BelongsToOneRelation');
  4. const UpsertNodeType = Object.freeze({
  5. Insert: 'Insert',
  6. Delete: 'Delete',
  7. Update: 'Update',
  8. Patch: 'Patch',
  9. Relate: 'Relate',
  10. Unrelate: 'Unrelate',
  11. UpsertRecursively: 'UpsertRecursively',
  12. None: 'None'
  13. });
  14. const ChangeType = Object.freeze({
  15. HasOwnChanges: 'HasOwnChanges',
  16. HasRelationalChanges: 'HasRelationalChanges',
  17. NoChanges: 'NoChanges'
  18. });
  19. const OptionType = Object.freeze({
  20. Relate: 'relate',
  21. Unrelate: 'unrelate',
  22. InsertMissing: 'insertMissing',
  23. Update: 'update',
  24. NoInsert: 'noInsert',
  25. NoUpdate: 'noUpdate',
  26. NoDelete: 'noDelete',
  27. NoRelate: 'noRelate',
  28. NoUnrelate: 'noUnrelate'
  29. });
  30. class UpsertNode {
  31. constructor({ parentNode, relExpr, upsertModel, currentModel, dataPath, opt }) {
  32. this.parentNode = parentNode || null;
  33. this.relExpr = relExpr;
  34. this.relPathFromRoot = getRelationPathFromRoot(this);
  35. this.upsertModel = upsertModel || null;
  36. this.currentModel = currentModel || null;
  37. this.relations = Object.create(null);
  38. this.dataPath = dataPath;
  39. this.opt = opt || {};
  40. const { types, omitFromUpdate } = getTypes(this);
  41. this.types = types;
  42. if (upsertModel && currentModel) {
  43. copyCurrentToUpsert(currentModel, upsertModel);
  44. }
  45. if (omitFromUpdate) {
  46. this.upsertModel.$omitFromDatabaseJson(omitFromUpdate);
  47. }
  48. }
  49. static get Type() {
  50. return UpsertNodeType;
  51. }
  52. static get OptionType() {
  53. return OptionType;
  54. }
  55. get someModel() {
  56. return this.upsertModel || this.currentModel;
  57. }
  58. get modelClass() {
  59. return this.someModel.constructor;
  60. }
  61. get relationName() {
  62. if (this.parentNode !== null) {
  63. return this.relExpr.$relation;
  64. } else {
  65. return null;
  66. }
  67. }
  68. get relation() {
  69. if (this.parentNode !== null) {
  70. return this.parentNode.modelClass.getRelations()[this.relationName];
  71. } else {
  72. return null;
  73. }
  74. }
  75. hasType() {
  76. for (let i = 0, l = arguments.length; i < l; ++i) {
  77. if (this.types.indexOf(arguments[i]) !== -1) {
  78. return true;
  79. }
  80. }
  81. return false;
  82. }
  83. }
  84. function copyCurrentToUpsert(currentModel, upsertModel) {
  85. const props = Object.keys(currentModel);
  86. for (let i = 0, l = props.length; i < l; ++i) {
  87. const prop = props[i];
  88. // Temp columns are created by some queries and they are never meant to
  89. // be seen by the outside world. Skip those in addition to undefineds.
  90. if (!isTempColumn(prop) && upsertModel[prop] === undefined) {
  91. upsertModel[prop] = currentModel[prop];
  92. }
  93. }
  94. }
  95. function getTypes(node) {
  96. if (isInsertWithId(node)) {
  97. return getTypesInsertWithId(node);
  98. } else if (isInsert(node)) {
  99. return getTypesInsert(node);
  100. } else if (isDeleteOrUnrelate(node)) {
  101. return getTypesDeleteUnrelate(node);
  102. } else {
  103. return getTypesUpdate(node);
  104. }
  105. }
  106. function isInsertWithId(node) {
  107. // Database doesn't have the model, but the upsert graph does and the model
  108. // in the upsert graph has an id. Depending on other options this might end
  109. // up being either an insert or a relate.
  110. return isInsert(node) && node.upsertModel.$hasId();
  111. }
  112. function getTypesInsertWithId(node) {
  113. if (hasOption(node, OptionType.Relate) && node.relation !== null) {
  114. return getTypesRelate(node);
  115. } else if (hasOption(node, OptionType.InsertMissing)) {
  116. // If insertMissing option is set for the node, we insert the model
  117. // even though it has the id set.
  118. return getTypesInsert(node);
  119. } else {
  120. const parent = node.parentNode;
  121. throw new Error(
  122. [
  123. parent
  124. ? `model (id=${node.upsertModel.$id()}) is not a child of model (id=${parent.upsertModel.$id()}). `
  125. : `root model (id=${node.upsertModel.$id()}) does not exist. `,
  126. parent ? `If you want to relate it, use the relate option. ` : '',
  127. `If you want to insert it with an id, use the insertMissing option`
  128. ].join('')
  129. );
  130. }
  131. }
  132. function getTypesRelate(node) {
  133. const props = Object.keys(node.upsertModel);
  134. const rel = node.parentNode.modelClass.getRelations()[node.relationName];
  135. if (difference(props, rel.relatedProp.props).length !== 0) {
  136. const relateType = decideType(node, UpsertNodeType.Relate, OptionType.NoRelate);
  137. // If the relate model contains any other properties besides the foreign
  138. // keys needed to make the relation, we may also need to update it.
  139. const possibleUpdateType = decideType(
  140. node,
  141. UpsertNodeType.Patch,
  142. OptionType.Update,
  143. UpsertNodeType.Update
  144. );
  145. const updateType = decideType(node, possibleUpdateType, OptionType.NoUpdate);
  146. if (relateType === UpsertNodeType.None) {
  147. return {
  148. types: [UpsertNodeType.None]
  149. };
  150. } else if (updateType === UpsertNodeType.None) {
  151. return {
  152. types: [relateType]
  153. };
  154. } else if (
  155. relateType === UpsertNodeType.Relate &&
  156. hasRelationsInUpsertModel(node.upsertModel)
  157. ) {
  158. return {
  159. types: [
  160. decideType(node, UpsertNodeType.Relate, OptionType.NoRelate),
  161. decideType(node, UpsertNodeType.UpsertRecursively, OptionType.NoRelate)
  162. ]
  163. };
  164. } else {
  165. return {
  166. types: [relateType, updateType],
  167. // If we update, we don't want to update the relation props.
  168. omitFromUpdate: rel.relatedProp.props
  169. };
  170. }
  171. } else {
  172. return {
  173. types: [decideType(node, UpsertNodeType.Relate, OptionType.NoRelate)]
  174. };
  175. }
  176. }
  177. function isInsert(node) {
  178. // Database doesn't have the model, but the upsert graph does.
  179. return node.upsertModel !== null && node.currentModel === null;
  180. }
  181. function getTypesInsert(node) {
  182. return {
  183. types: [decideType(node, UpsertNodeType.Insert, OptionType.NoInsert)]
  184. };
  185. }
  186. function isDeleteOrUnrelate(node) {
  187. // Database has the model, but the upsert graph doesn't.
  188. return node.upsertModel === null && node.currentModel !== null;
  189. }
  190. function getTypesDeleteUnrelate(node) {
  191. const ciblingNodes = node.parentNode.relations[node.relation.name];
  192. const type = hasOption(node, OptionType.Unrelate)
  193. ? decideType(node, UpsertNodeType.Unrelate, OptionType.NoUnrelate)
  194. : decideType(node, UpsertNodeType.Delete, OptionType.NoDelete);
  195. // Optimization: If the relation is a BelongsToOneRelation and we are
  196. // going to relate a new model to it, we don't need to unrelate since
  197. // we would end up with useless update operation.
  198. if (
  199. type === UpsertNodeType.Unrelate &&
  200. node.relation instanceof BelongsToOneRelation &&
  201. ciblingNodes &&
  202. ciblingNodes.some(it => it.hasType(UpsertNodeType.Relate, UpsertNodeType.Insert))
  203. ) {
  204. return {
  205. types: [UpsertNodeType.None]
  206. };
  207. } else {
  208. return {
  209. types: [type]
  210. };
  211. }
  212. }
  213. function getTypesUpdate(node) {
  214. const { changeType, unchangedProps } = hasChanges(node.currentModel, node.upsertModel);
  215. if (changeType == ChangeType.NoChanges) {
  216. return {
  217. types: [UpsertNodeType.None],
  218. omitFromUpdate: unchangedProps
  219. };
  220. } else if (changeType == ChangeType.HasOwnChanges) {
  221. const possibleUpdateType = decideType(
  222. node,
  223. UpsertNodeType.Patch,
  224. OptionType.Update,
  225. UpsertNodeType.Update
  226. );
  227. const updateType = decideType(node, possibleUpdateType, OptionType.NoUpdate);
  228. return {
  229. types: [updateType],
  230. omitFromUpdate: unchangedProps
  231. };
  232. } else if (changeType == ChangeType.HasRelationalChanges) {
  233. // Always create a patch node for relational changes even if `noUpdate`
  234. // option is true.
  235. return {
  236. types: [UpsertNodeType.Patch],
  237. omitFromUpdate: unchangedProps
  238. };
  239. }
  240. }
  241. function hasOption(node, optName) {
  242. const opt = node.opt[optName];
  243. if (Array.isArray(opt)) {
  244. return opt.indexOf(node.relPathFromRoot) !== -1;
  245. } else {
  246. return !!opt;
  247. }
  248. }
  249. function decideType(node, defaultType, option, optionType = UpsertNodeType.None) {
  250. return hasOption(node, option) ? optionType : defaultType;
  251. }
  252. function getRelationPathFromRoot(node) {
  253. const path = [];
  254. while (node) {
  255. if (node.relExpr.$relation) {
  256. path.unshift(node.relExpr.$relation);
  257. }
  258. node = node.parentNode;
  259. }
  260. return path.join('.');
  261. }
  262. function hasChanges(currentModel, upsertModel) {
  263. let changeType = ChangeType.NoChanges;
  264. const changingRelProps = findChangingRelProps(currentModel, upsertModel);
  265. if (changingRelProps.length) {
  266. changeType = ChangeType.HasRelationalChanges;
  267. }
  268. if (changeType === ChangeType.NoChanges) {
  269. // If the upsert model has query properties, we cannot know if they will change
  270. // the value. We need to return HasOwnChanges just in case.
  271. if (upsertModel.$$queryProps && Object.keys(upsertModel.$$queryProps).length > 0) {
  272. changeType = ChangeType.HasOwnChanges;
  273. }
  274. }
  275. const keys = Object.keys(upsertModel);
  276. const relations = upsertModel.constructor.getRelations();
  277. const unchangedProps = [];
  278. for (let i = 0, l = keys.length; i < l; ++i) {
  279. const key = keys[i];
  280. if (key[0] === '$' || relations[key]) {
  281. continue;
  282. }
  283. // Use non-strict inequality here on purpose. See issue #732.
  284. if (currentModel[key] === undefined || currentModel[key] != upsertModel[key]) {
  285. if (changeType === ChangeType.NoChanges) {
  286. changeType = ChangeType.HasOwnChanges;
  287. }
  288. } else if (!changingRelProps.includes(key)) {
  289. unchangedProps.push(key);
  290. }
  291. }
  292. return {
  293. changeType: changeType,
  294. unchangedProps
  295. };
  296. }
  297. function hasRelationsInUpsertModel(upsertModel) {
  298. const relationArray = upsertModel.constructor.getRelationArray();
  299. for (let i = 0, l = relationArray.length; i < l; ++i) {
  300. const relation = relationArray[i];
  301. const upsertRelated = upsertModel[relation.name];
  302. if (upsertRelated) {
  303. return true;
  304. }
  305. }
  306. return false;
  307. }
  308. function findChangingRelProps(currentModel, upsertModel) {
  309. const relationArray = upsertModel.constructor.getRelationArray();
  310. const changingProps = [];
  311. for (let i = 0, l = relationArray.length; i < l; ++i) {
  312. const relation = relationArray[i];
  313. const upsertRelated = upsertModel[relation.name];
  314. if (upsertRelated && relation instanceof BelongsToOneRelation) {
  315. // If the the property is a `BelongsToOneRelation` me may need to update
  316. // this model if the related model changes causing this model's relation
  317. // property to need updating.
  318. const relatedProp = relation.relatedProp;
  319. const ownerProp = relation.ownerProp;
  320. const currentRelated = currentModel[relation.name];
  321. for (let j = 0, lr = relatedProp.size; j < lr; ++j) {
  322. const currentProp = currentRelated && relatedProp.getProp(currentRelated, j);
  323. const upsertProp = upsertRelated && relatedProp.getProp(upsertRelated, j);
  324. if (currentProp !== upsertProp) {
  325. changingProps.push(ownerProp.props[j]);
  326. }
  327. }
  328. }
  329. }
  330. return changingProps;
  331. }
  332. module.exports = UpsertNode;