diff --git a/packages/core/.eslintrc.json b/packages/core/.eslintrc.json index 24c308306e..45ab74368f 100644 --- a/packages/core/.eslintrc.json +++ b/packages/core/.eslintrc.json @@ -1,7 +1,8 @@ { "env": { "browser": true, - "es6": true + "es6": true, + "jest": true }, "extends": "eslint:recommended", "parser": "babel-eslint", diff --git a/packages/core/src/recordApi/save.js b/packages/core/src/recordApi/save.js index aef38476f2..8c21999d4e 100644 --- a/packages/core/src/recordApi/save.js +++ b/packages/core/src/recordApi/save.js @@ -3,6 +3,7 @@ import { flatten, map, filter, + isEqual } from 'lodash/fp'; import { initialiseChildCollections } from '../collectionApi/initialise'; import { validate } from './validate'; @@ -15,6 +16,7 @@ import { getExactNodeForPath, isRecord, getNode, + getLastPartInKey, fieldReversesReferenceToNode, } from '../templateApi/hierarchy'; import { mapRecord } from '../indexing/evaluate'; @@ -79,7 +81,6 @@ export const _save = async (app, record, context, skipValidation = false) => { getRecordFileName(recordClone.key), recordClone, ); - await app.publish(events.recordApi.save.onRecordUpdated, { old: oldRecord, new: recordClone, @@ -122,59 +123,6 @@ const initialiseReverseReferenceIndexes = async (app, record) => { } }; -const maintainReferentialIntegrity = async (app, indexingApi, oldRecord, newRecord) => { - /* - FOREACH Field that reference this object - - options Index node that for field - - has options index changed for referenced record? - - FOREACH reverse index of field - - FOREACH referencingRecord in reverse index - - Is field value still pointing to referencedRecord - - Update referencingRecord.fieldName to new value - - Save - */ - const recordNode = getExactNodeForPath(app.hierarchy)(newRecord.key); - const referenceFields = fieldsThatReferenceThisRecord( - app, recordNode, - ); - - const updates = $(referenceFields, [ - map(f => ({ - node: getNode( - app.hierarchy, f.typeOptions.indexNodeKey, - ), - field: f, - })), - map(n => ({ - old: mapRecord(oldRecord, n.node), - new: mapRecord(newRecord, n.node), - indexNode: n.node, - field: n.field, - reverseIndexKeys: $(n.field.typeOptions.reverseIndexNodeKeys, [ - map(k => joinKey( - newRecord.key, - getLastPartInKey(k), - )), - ]), - })), - filter(diff => !isEqual(diff.old)(diff.new)), - ]); - - for (const update of updates) { - for (const reverseIndexKey of update.reverseIndexKeys) { - const rows = await listItems(app)(reverseIndexKey); - - for (const key of map(r => r.key)(rows)) { - const record = await _load(app, key); - if (record[update.field.name].key === newRecord.key) { - record[update.field.name] = update.new; - await _save(app, indexingApi, record, undefined, true); - } - } - } - } -}; - const fieldsThatReferenceThisRecord = (app, recordNode) => $(app.hierarchy, [ getFlattenedHierarchy, filter(isRecord), diff --git a/packages/core/src/types/reference.js b/packages/core/src/types/reference.js index 59c540a7f9..30e0d780ba 100644 --- a/packages/core/src/types/reference.js +++ b/packages/core/src/types/reference.js @@ -24,8 +24,24 @@ const hasStringValue = (ob, path) => has(ob, path) const isObjectWithKey = v => isObjectLike(v) && hasStringValue(v, 'key'); +const tryParseFromString = s => { + + try { + const asObj = JSON.parse(s); + if(isObjectWithKey) { + return parsedSuccess(asObj); + } + } + catch(_) { + // EMPTY + } + + return parsedFailed(s); +} + const referenceTryParse = v => switchCase( [isObjectWithKey, parsedSuccess], + [isString, tryParseFromString], [isNull, () => parsedSuccess(referenceNothing())], [defaultCase, parsedFailed], )(v); diff --git a/packages/core/test/recordApi.reindex.spec.js b/packages/core/test/recordApi.reindex.spec.js index 86a3648d8f..3fac75afb2 100644 --- a/packages/core/test/recordApi.reindex.spec.js +++ b/packages/core/test/recordApi.reindex.spec.js @@ -2,7 +2,7 @@ import {setupApphierarchy, basicAppHierarchyCreator_WithFields, basicAppHierarchyCreator_WithFields_AndIndexes} from "./specHelpers"; import {joinKey} from "../src/common"; -import {some, isArray} from "lodash"; +import {some, isArray, isObjectLike} from "lodash"; describe("recordApi > create > reindex", () => { @@ -107,6 +107,32 @@ describe("recordApi > create > reindex", () => { expect(customers[0].name).toBe("Ledog"); }); + it("should add reference field to index and reparse", async () => { + const {recordApi, indexApi} = + await setupApphierarchy(basicAppHierarchyCreator_WithFields); + + const partner = recordApi.getNew("/partners", "partner"); + partner.businessName = "ACME"; + partner.phone = "098766e6"; + await recordApi.save(partner); + + const customer = recordApi.getNew("/customers", "customer"); + customer.surname = "Ledog"; + customer.age = 9; + customer.isalive = true, + customer.createdDate = new Date(); + customer.partner = partner; + await recordApi.save(customer); + + const customers = await indexApi.listItems("/customer_index"); + + expect(customers.length).toBe(1); + expect(isObjectLike(customer.partner)).toBeTruthy(); + expect(customers[0].partner.key).toBe(partner.key); + expect(customers[0].partner.name).toBe(partner.businessName); + expect(customers[0].partner.phone).toBe(partner.phone); + }); + it("should add to reverse reference index, when required", async () => { const {recordApi, indexApi} = await setupApphierarchy(basicAppHierarchyCreator_WithFields); @@ -573,7 +599,7 @@ describe("referenced object changed", () => { it("should update the reference", async () => { - const {recordApi, indexApi} = + const { recordApi } = await setupApphierarchy(basicAppHierarchyCreator_WithFields); const partner1 = recordApi.getNew("/partners", "partner"); diff --git a/packages/core/test/recordApi.spec.js b/packages/core/test/recordApi.spec.js index 724ba22585..364410aa74 100644 --- a/packages/core/test/recordApi.spec.js +++ b/packages/core/test/recordApi.spec.js @@ -189,6 +189,32 @@ describe('recordApi > save then load', () => { const savedAgain = await recordApi.load(saved.key); expect(savedAgain.surname).toBe(saved.surname); }); + + it("should maintain referential integrity", async () => { + const {recordApi} = + await setupApphierarchy(basicAppHierarchyCreator_WithFields); + + const referredByCustomer = recordApi.getNew("/customers", "customer"); + referredByCustomer.surname = "Ledog"; + referredByCustomer.age = 9; + referredByCustomer.isalive = true, + referredByCustomer.createdDate = new Date(); + const savedReferredBy = await recordApi.save(referredByCustomer); + + const referredCustomer = recordApi.getNew("/customers", "customer"); + referredCustomer.surname = "Zeecat"; + referredCustomer.age = 9; + referredCustomer.isalive = true, + referredCustomer.createdDate = new Date(); + referredCustomer.referredBy = referredByCustomer; + await recordApi.save(referredCustomer); + + savedReferredBy.surname = "Zeedog"; + await recordApi.save(savedReferredBy); + + const loadedReferredTo = await recordApi.load(referredCustomer.key); + expect(loadedReferredTo.referredBy.surname).toBe("Zeedog"); + }); }); describe("save", () => { diff --git a/packages/core/test/specHelpers.js b/packages/core/test/specHelpers.js index ade93c8b3d..fc7bd0900f 100644 --- a/packages/core/test/specHelpers.js +++ b/packages/core/test/specHelpers.js @@ -1,16 +1,15 @@ import path from "path"; -import {getAppApis, getRecordApi, +import {getRecordApi, getCollectionApi, getIndexApi, getActionsApi} from "../src"; import memory from "./memory"; import {setupDatastore} from "../src/appInitialise"; import {configFolder, fieldDefinitions, - templateDefinitions, isNothing, + templateDefinitions, joinKey, isSomething} from "../src/common"; import { getNewIndexTemplate } from "../src/templateApi/createNodes"; import {indexTypes} from "../src/templateApi/indexes"; import getTemplateApi from "../src/templateApi"; -import {getApplicationDefinition} from "../src/templateApi/getApplicationDefinition"; import getAuthApi from "../src/authApi"; import {createEventAggregator} from "../src/appInitialise/eventAggregator"; import {filter, find} from "lodash/fp"; @@ -116,7 +115,7 @@ export const hierarchyFactory = (...additionalFeatures) => templateApi => { const customerRecord = templateApi.getNewRecordTemplate(root, "customer"); customerRecord.collectionName = "customers"; - findCollectionDefaultIndex(customerRecord).map = "return {surname:record.surname, isalive:record.isalive};"; + findCollectionDefaultIndex(customerRecord).map = "return {surname:record.surname, isalive:record.isalive, partner:record.partner};"; const partnerRecord = templateApi.getNewRecordTemplate(root, "partner"); partnerRecord.collectionName = "partners"; @@ -157,7 +156,7 @@ export const withFields = (hierarchy, templateApi) => { const partnersReferenceIndex = templateApi.getNewIndexTemplate(root); partnersReferenceIndex.name = "partnersReference"; - partnersReferenceIndex.map = "return {name:record.businessName};"; + partnersReferenceIndex.map = "return {name:record.businessName, phone:record.phone};"; partnersReferenceIndex.allowedRecordNodeIds = [partnerRecord.nodeId]; const partnerCustomersReverseIndex = templateApi.getNewIndexTemplate(partnerRecord, indexTypes.reference); @@ -172,7 +171,7 @@ export const withFields = (hierarchy, templateApi) => { newCustomerField("createddate", "datetime"); newCustomerField("age", "number"); newCustomerField("profilepic", "file"); - const customerPartnerField = newCustomerField("partner", "reference", undefined, { + newCustomerField("partner", "reference", undefined, { indexNodeKey : "/partnersReference", displayValue : "name", reverseIndexNodeKeys : [joinKey( @@ -205,6 +204,7 @@ export const withFields = (hierarchy, templateApi) => { const newPartnerField = getNewFieldAndAdd(templateApi, partnerRecord); newPartnerField("businessName", "string"); + newPartnerField("phone", "string"); const newPartnerInvoiceField = getNewFieldAndAdd(templateApi, partnerInvoiceRecord); const partnerInvoiceTotalIncVatVield = newPartnerInvoiceField("totalIncVat", "number"); @@ -215,7 +215,7 @@ export const withFields = (hierarchy, templateApi) => { const newChargeField = getNewFieldAndAdd(templateApi, chargeRecord); newChargeField("amount", "number"); - const chargePartnerInvoiceField = newChargeField("partnerInvoice", "reference", undefined, { + newChargeField("partnerInvoice", "reference", undefined, { reverseIndexNodeKeys : [joinKey( partnerInvoiceRecord.nodeKey(), "partnerCharges" )], @@ -236,7 +236,7 @@ export const withFields = (hierarchy, templateApi) => { customersReferenceIndex.filter = "record.isalive === true"; customersReferenceIndex.allowedRecordNodeIds = [customerRecord.nodeId]; - const invoiceCustomerField = newInvoiceField("customer", "reference", undefined, { + newInvoiceField("customer", "reference", undefined, { indexNodeKey : "/customersReference", reverseIndexNodeKeys : [findCollectionDefaultIndex(invoiceRecord).nodeKey()], displayValue : "name" @@ -376,15 +376,11 @@ export const setupApphierarchy = async (creator, disableCleanupTransactions=fals return apis; }; -const disableCleanupTransactions = app => { - -} - export const getNewFieldAndAdd = (templateApi, record) => (name, type, initial, typeOptions) => { const field = templateApi.getNewField(type); field.name = name; field.getInitialValue = !initial ? "default" : initial; - if(!!typeOptions) + if(typeOptions) field.typeOptions = typeOptions; templateApi.addField(record, field); return field; @@ -397,8 +393,8 @@ export const stubEventHandler = () => { events.push({name, context}); }, events, - getEvents: n => filter(e => e.name === n) - (events) + getEvents: n => filter( + e => e.name === n)(events) }; };