From 1334fae8c653ea62292f5f74d9d01ac37dbdff5c Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Mon, 21 Oct 2024 19:31:19 -0400 Subject: [PATCH] records can be updated without being stored (#957) This PR resolves the issue where a user is forced to `store()` a record prior to updating it. Now a user can pass `store: false` to the update options to signal they do not wish to store the updated record. The default behavior remains the same, effectively store is defaulted to true. If the initial record was not already stored, it will throw with an error as it does today forcing the user to explicitly `store()` the record prior to updating it unless they do not want to store the udpate. Resolves https://github.com/TBD54566975/web5-js/issues/955 --- .changeset/old-buckets-kiss.md | 5 + packages/api/src/record.ts | 6 +- packages/api/tests/record.spec.ts | 150 ++++++++++++++++-------------- 3 files changed, 88 insertions(+), 73 deletions(-) create mode 100644 .changeset/old-buckets-kiss.md diff --git a/.changeset/old-buckets-kiss.md b/.changeset/old-buckets-kiss.md new file mode 100644 index 000000000..e9fa387eb --- /dev/null +++ b/.changeset/old-buckets-kiss.md @@ -0,0 +1,5 @@ +--- +"@web5/api": patch +--- + +Allow records to be updated without storing. diff --git a/packages/api/src/record.ts b/packages/api/src/record.ts index e93b2463f..5ae0bed0b 100644 --- a/packages/api/src/record.ts +++ b/packages/api/src/record.ts @@ -141,6 +141,9 @@ export type RecordUpdateParams = { */ dataCid?: DwnMessageDescriptor[DwnInterface.RecordsWrite]['dataCid']; + /** Whether or not to store the updated message. */ + store?: boolean; + /** The data format/MIME type of the supplied data */ dataFormat?: string; @@ -706,7 +709,7 @@ export class Record implements RecordModel { * * @beta */ - async update({ dateModified, data, protocolRole, ...params }: RecordUpdateParams): Promise { + async update({ dateModified, data, protocolRole, store = true, ...params }: RecordUpdateParams): Promise { if (this.deleted) { throw new Error('Record: Cannot revive a deleted record.'); @@ -760,6 +763,7 @@ export class Record implements RecordModel { messageParams : { ...updateMessage }, messageType : DwnInterface.RecordsWrite, target : this._connectedDid, + store }; if (this._delegateDid) { diff --git a/packages/api/tests/record.spec.ts b/packages/api/tests/record.spec.ts index 8a14b5bb0..e0bd8066f 100644 --- a/packages/api/tests/record.spec.ts +++ b/packages/api/tests/record.spec.ts @@ -2611,58 +2611,6 @@ describe('Record', () => { expect(readResultAfterUpdate.status.code).to.equal(401); }); - it('updates a record locally that only written to a remote DWN', async () => { - // Create a record but do not store it on the local DWN. - const { status, record } = await dwnAlice.records.write({ - store : false, - data : 'Hello, world!', - message : { - schema : 'foo/bar', - dataFormat : 'text/plain' - } - }); - expect(status.code).to.equal(202); - expect(record).to.not.be.undefined; - - // Store the data CID of the record before it is updated. - const dataCidBeforeDataUpdate = record!.dataCid; - - // Write the record to a remote DWN. - const { status: sendStatus } = await record!.send(aliceDid.uri); - expect(sendStatus.code).to.equal(202); - - // fails because record has not been stored in the local dwn yet - let updateResult = await record!.update({ data: 'bye' }); - expect(updateResult.status.code).to.equal(400); - expect(updateResult.status.detail).to.equal('RecordsWriteGetInitialWriteNotFound: Initial write is not found.'); - - const { status: recordStoreStatus }= await record.store(); - expect(recordStoreStatus.code).to.equal(202); - - // now succeeds with the update - updateResult = await record!.update({ data: 'bye' }); - expect(updateResult.status.code).to.equal(202); - - // Confirm that the record was written to the local DWN. - const readResult = await dwnAlice.records.read({ - message: { - filter: { - recordId: record!.id - } - } - }); - expect(readResult.status.code).to.equal(200); - expect(readResult.record).to.not.be.undefined; - - // Confirm that the data CID of the record was updated. - expect(readResult.record.dataCid).to.not.equal(dataCidBeforeDataUpdate); - expect(readResult.record.dataCid).to.equal(record!.dataCid); - - // Confirm that the data payload of the record was modified. - const updatedData = await record!.data.text(); - expect(updatedData).to.equal('bye'); - }); - it('allows to update a record locally that was initially read from a remote DWN if store() is issued', async () => { // Create a record but do not store it on the local DWN. const { status, record } = await dwnAlice.records.write({ @@ -2725,7 +2673,7 @@ describe('Record', () => { expect(readResult.record.dataCid).to.equal(readRecord.dataCid); }); - it('updates a record locally that was initially queried from a remote DWN', async () => { + it('updates a record that was queried from a remote DWN without storing it', async () => { // Create a record but do not store it on the local DWN. const { status, record } = await dwnAlice.records.write({ store : false, @@ -2746,7 +2694,7 @@ describe('Record', () => { expect(sendStatus.code).to.equal(202); // Query the record from the remote DWN. - const queryResult = await dwnAlice.records.query({ + let queryResult = await dwnAlice.records.query({ from : aliceDid.uri, message : { filter: { @@ -2758,37 +2706,95 @@ describe('Record', () => { expect(queryResult.records).to.not.be.undefined; expect(queryResult.records.length).to.equal(1); - // Attempt to update the queried record, which will fail because we haven't stored the queried record locally yet + // Attempt to update the queried record const [ queriedRecord ] = queryResult.records; - let updateResult = await queriedRecord!.update({ data: 'bye' }); - expect(updateResult.status.code).to.equal(400); - expect(updateResult.status.detail).to.equal('RecordsWriteGetInitialWriteNotFound: Initial write is not found.'); - - // store the queried record - const { status: queriedStoreStatus } = await queriedRecord.store(); - expect(queriedStoreStatus.code).to.equal(202); - - updateResult = await queriedRecord!.update({ data: 'bye' }); + let updateResult = await queriedRecord!.update({ data: 'Updated, world!', store: false }); expect(updateResult.status.code).to.equal(202); - // Confirm that the record was written to the local DWN. - const readResult = await dwnAlice.records.read({ + // confirm that the record does not exist locally + queryResult = await dwnAlice.records.read({ message: { filter: { recordId: record!.id } } }); + expect(queryResult.status.code).to.equal(404); + }); + + it('updates a record which has a parent reference from a remote DWN without storing it or its parent', async () => { + // create a parent thread + const { status: threadStatus, record: threadRecord } = await dwnAlice.records.write({ + store : false, + data : 'Hello, world!', + message : { + protocol : protocolDefinition.protocol, + schema : protocolDefinition.types.thread.schema, + protocolPath : 'thread' + } + }); + + expect(threadStatus.code).to.equal(202); + expect(threadRecord).to.not.be.undefined; + + const { status: threadSendStatus } = await threadRecord.send(); + expect(threadSendStatus.code).to.equal(202); + + // create an email with the thread as a parent + const { status: emailStatus, record: emailRecord } = await dwnAlice.records.write({ + store : false, + data : 'Hello, world!', + message : { + parentContextId : threadRecord.contextId, + protocol : protocolDefinition.protocol, + protocolPath : 'thread/email', + schema : protocolDefinition.types.email.schema + } + }); + expect(emailStatus.code).to.equal(202); + expect(emailRecord).to.not.be.undefined; + + const { status: emailSendStatus } = await emailRecord!.send(); + expect(emailSendStatus.code).to.equal(202); + + // update email record + const { status: updateStatus } = await emailRecord!.update({ data: 'updated email record', store: false }); + expect(updateStatus.code).to.equal(202); + + const { status: updateEmailSendStatus } = await emailRecord!.send(); + expect(updateEmailSendStatus.code).to.equal(202); + + let readResult = await dwnAlice.records.read({ + from : aliceDid.uri, + message : { + filter: { + recordId: emailRecord.id + } + } + }); + expect(readResult.status.code).to.equal(200); expect(readResult.record).to.not.be.undefined; + expect(await readResult.record.data.text()).to.equal('updated email record'); - // Confirm that the data CID of the record was updated. - expect(readResult.record.dataCid).to.not.equal(dataCidBeforeDataUpdate); - expect(readResult.record.dataCid).to.equal(queriedRecord!.dataCid); + // confirm that records do not exist locally + readResult = await dwnAlice.records.read({ + message: { + filter: { + recordId: emailRecord.id + } + } + }); + expect(readResult.status.code).to.equal(404); - // Confirm that the data payload of the record was modified. - const updatedData = await queriedRecord!.data.text(); - expect(updatedData).to.equal('bye'); + readResult = await dwnAlice.records.read({ + message: { + filter: { + recordId: threadRecord.id + } + } + }); + expect(readResult.status.code).to.equal(404); }); it('updates a record which has a parent reference', async () => {