From b989994fc317760d51aba0da4d3e6d61f9077454 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Thu, 25 Apr 2024 18:50:27 -0400 Subject: [PATCH 1/9] upgrade api and agent --- .changeset/few-oranges-compare.md | 12 +++++ package.json | 2 +- packages/agent/package.json | 4 +- packages/api/package.json | 4 +- packages/api/src/record.ts | 5 +- packages/api/tests/dwn-api.spec.ts | 28 ++++++++++- packages/dev-env/docker-compose.yaml | 2 +- packages/identity-agent/package.json | 2 +- packages/proxy-agent/package.json | 2 +- packages/user-agent/package.json | 2 +- pnpm-lock.yaml | 73 +++++++++++++++++----------- 11 files changed, 96 insertions(+), 40 deletions(-) create mode 100644 .changeset/few-oranges-compare.md diff --git a/.changeset/few-oranges-compare.md b/.changeset/few-oranges-compare.md new file mode 100644 index 000000000..9c8040ac8 --- /dev/null +++ b/.changeset/few-oranges-compare.md @@ -0,0 +1,12 @@ +--- +"@web5/identity-agent": patch +"@web5/proxy-agent": patch +"@web5/user-agent": patch +"@web5/agent": patch +"@web5/api": patch +--- + +Upgrade DWN SDK with newest features + +- remove `Permissions` interface and replace permissions with a first-class protocol representing it +- adding `RecordsTags` functionality diff --git a/package.json b/package.json index 31135457a..9086084e0 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@changesets/cli": "^2.27.1", "@npmcli/package-json": "5.0.0", "@typescript-eslint/eslint-plugin": "6.4.0", - "@web5/dwn-server": "0.1.17", + "@web5/dwn-server": "0.2.1", "eslint-plugin-mocha": "10.1.0", "npkill": "0.11.3" }, diff --git a/packages/agent/package.json b/packages/agent/package.json index b4de4b539..1cf7765ca 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -71,10 +71,10 @@ "dependencies": { "@noble/ciphers": "0.4.1", "@scure/bip39": "1.2.2", - "@tbd54566975/dwn-sdk-js": "0.2.22", + "@tbd54566975/dwn-sdk-js": "0.3.1", "@web5/common": "1.0.0", "@web5/crypto": "1.0.0", - "@web5/dids": "1.0.0", + "@web5/dids": "1.0.1", "abstract-level": "1.0.4", "ed25519-keygen": "0.4.11", "level": "8.0.0", diff --git a/packages/api/package.json b/packages/api/package.json index 690da6b76..02a6f952d 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -80,12 +80,12 @@ "@web5/agent": "0.3.1", "@web5/common": "1.0.0", "@web5/crypto": "1.0.0", - "@web5/dids": "1.0.0", + "@web5/dids": "1.0.1", "@web5/user-agent": "0.3.1" }, "devDependencies": { "@playwright/test": "1.40.1", - "@tbd54566975/dwn-sdk-js": "0.2.22", + "@tbd54566975/dwn-sdk-js": "0.3.1", "@types/chai": "4.3.6", "@types/eslint": "8.44.2", "@types/mocha": "10.0.1", diff --git a/packages/api/src/record.ts b/packages/api/src/record.ts index 0de337695..06e7e9b02 100644 --- a/packages/api/src/record.ts +++ b/packages/api/src/record.ts @@ -247,6 +247,8 @@ export class Record implements RecordModel { /** Record's published status (true/false) */ get published() { return this._descriptor.published; } + get tags() { return this._descriptor.tags; } + /** * Returns a copy of the raw `RecordsWriteMessage` that was used to create the current `Record` instance. */ @@ -540,7 +542,8 @@ export class Record implements RecordModel { published : this.published, recipient : this.recipient, recordId : this.id, - schema : this.schema + schema : this.schema, + tags : this.tags, }; } diff --git a/packages/api/tests/dwn-api.spec.ts b/packages/api/tests/dwn-api.spec.ts index 3854c9313..b40dc0cec 100644 --- a/packages/api/tests/dwn-api.spec.ts +++ b/packages/api/tests/dwn-api.spec.ts @@ -205,7 +205,7 @@ describe('DwnApi', () => { const response = await dwnAlice.protocols.query({ from : bobDid.uri, message : { - permissionsGrantId : 'bafyreiduimprbncdo2oruvjrvmfmwuyz4xx3d5biegqd2qntlryvuuosem', + permissionGrantId : 'bafyreiduimprbncdo2oruvjrvmfmwuyz4xx3d5biegqd2qntlryvuuosem', filter : { protocol: 'https://doesnotexist.com/protocol' } @@ -238,6 +238,32 @@ describe('DwnApi', () => { expect(await result.record?.data.text()).to.equal(dataString); }); + it('creates a record with tags', async () => { + const result = await dwnAlice.records.create({ + data : 'some data', + message : { + schema : 'foo/bar', + dataFormat : 'text/plain', + tags: { + foo : 'bar', + count : 2, + bool : true + } + } + }); + + expect(result.status.code).to.equal(202); + expect(result.status.detail).to.equal('Accepted'); + expect(result.record).to.exist; + expect(result.record?.tags).to.exist; + expect(result.record?.tags).to.deep.equal({ + foo : 'bar', + count : 2, + bool : true + }); + + }); + it('creates a record with JSON data', async () => { const dataJson = { hello: 'world!'}; const result = await dwnAlice.records.create({ diff --git a/packages/dev-env/docker-compose.yaml b/packages/dev-env/docker-compose.yaml index f24672352..585e567d0 100644 --- a/packages/dev-env/docker-compose.yaml +++ b/packages/dev-env/docker-compose.yaml @@ -3,6 +3,6 @@ version: "3.98" services: dwn-server: container_name: dwn-server - image: ghcr.io/tbd54566975/dwn-server:dwn-sdk-0.2.22 + image: ghcr.io/tbd54566975/dwn-server:dwn-sdk-0.3.1 ports: - "3000:3000" diff --git a/packages/identity-agent/package.json b/packages/identity-agent/package.json index f4e5c8b79..5b4cc8dc4 100644 --- a/packages/identity-agent/package.json +++ b/packages/identity-agent/package.json @@ -72,7 +72,7 @@ "@web5/agent": "0.3.1", "@web5/common": "1.0.0", "@web5/crypto": "1.0.0", - "@web5/dids": "1.0.0" + "@web5/dids": "1.0.1" }, "devDependencies": { "@playwright/test": "1.40.1", diff --git a/packages/proxy-agent/package.json b/packages/proxy-agent/package.json index d723bba31..d3d549432 100644 --- a/packages/proxy-agent/package.json +++ b/packages/proxy-agent/package.json @@ -72,7 +72,7 @@ "@web5/agent": "0.3.1", "@web5/common": "1.0.0", "@web5/crypto": "1.0.0", - "@web5/dids": "1.0.0" + "@web5/dids": "1.0.1" }, "devDependencies": { "@playwright/test": "1.40.1", diff --git a/packages/user-agent/package.json b/packages/user-agent/package.json index 648f67815..e098e93c6 100644 --- a/packages/user-agent/package.json +++ b/packages/user-agent/package.json @@ -72,7 +72,7 @@ "@web5/agent": "0.3.1", "@web5/common": "1.0.0", "@web5/crypto": "1.0.0", - "@web5/dids": "1.0.0" + "@web5/dids": "1.0.1" }, "devDependencies": { "@playwright/test": "1.40.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da1977829..1da0d6cbf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,8 +24,8 @@ importers: specifier: 6.4.0 version: 6.4.0(@typescript-eslint/parser@6.4.0)(eslint@8.47.0)(typescript@5.1.6) '@web5/dwn-server': - specifier: 0.1.17 - version: 0.1.17 + specifier: 0.2.1 + version: 0.2.1 eslint-plugin-mocha: specifier: 10.1.0 version: 10.1.0(eslint@8.47.0) @@ -42,8 +42,8 @@ importers: specifier: 1.2.2 version: 1.2.2 '@tbd54566975/dwn-sdk-js': - specifier: 0.2.22 - version: 0.2.22 + specifier: 0.3.1 + version: 0.3.1 '@web5/common': specifier: 1.0.0 version: link:../common @@ -51,8 +51,8 @@ importers: specifier: 1.0.0 version: link:../crypto '@web5/dids': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: link:../dids abstract-level: specifier: 1.0.4 version: 1.0.4 @@ -157,8 +157,8 @@ importers: specifier: 1.0.0 version: link:../crypto '@web5/dids': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: link:../dids '@web5/user-agent': specifier: 0.3.1 version: link:../user-agent @@ -167,8 +167,8 @@ importers: specifier: 1.40.1 version: 1.40.1 '@tbd54566975/dwn-sdk-js': - specifier: 0.2.22 - version: 0.2.22 + specifier: 0.3.1 + version: 0.3.1 '@types/chai': specifier: 4.3.6 version: 4.3.6 @@ -691,8 +691,8 @@ importers: specifier: 1.0.0 version: link:../crypto '@web5/dids': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: link:../dids devDependencies: '@playwright/test': specifier: 1.40.1 @@ -779,8 +779,8 @@ importers: specifier: 1.0.0 version: link:../crypto '@web5/dids': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: link:../dids devDependencies: '@playwright/test': specifier: 1.40.1 @@ -864,8 +864,8 @@ importers: specifier: 1.0.0 version: link:../crypto '@web5/dids': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: link:../dids devDependencies: '@playwright/test': specifier: 1.40.1 @@ -2755,15 +2755,15 @@ packages: jwt-decode: 3.1.2 dev: true - /@tbd54566975/dwn-sdk-js@0.2.22: - resolution: {integrity: sha512-TBobNAWt09bsAKADiiWNcdgiuuWNkHAumPvuYM9d+V/Brcl99Q9jg3ssVQhMfhV3TN8zxCbAGWYALUfxgX4N3w==} + /@tbd54566975/dwn-sdk-js@0.3.1: + resolution: {integrity: sha512-G73ixUGieRBE4kYxLlYu/9wN36RUBGp5nvDrlAWZsNUXEXWnhCS3qTYFnUqzPEXjoE0z8EVRtpMU/eITM8ZcDA==} engines: {node: '>= 18'} dependencies: '@ipld/dag-cbor': 9.0.3 '@js-temporal/polyfill': 0.4.4 '@noble/ed25519': 2.0.0 '@noble/secp256k1': 2.0.0 - '@web5/dids': 1.0.0 + '@web5/dids': 1.0.1 abstract-level: 1.0.3 ajv: 8.12.0 blockstore-core: 4.2.0 @@ -2787,12 +2787,12 @@ packages: - encoding - supports-color - /@tbd54566975/dwn-sql-store@0.2.13: - resolution: {integrity: sha512-TYl16RwExcasH1UTNEQDwcGJbDA96hYQ4iVdpJc7xVfwGwtLzJWR1jSkCugP3BbLJ7lPPy0pywysZ1fW4b/bJg==} + /@tbd54566975/dwn-sql-store@0.4.1: + resolution: {integrity: sha512-ndslsbtNjkIuNu8ytNZnKjH4uWoxWFzt+L/8ok5giVmgrjTh/+XDU23LQYJjRHC/RusjpDNjlt77PhL2qbXxmQ==} engines: {node: '>=18'} dependencies: '@ipld/dag-cbor': 9.2.0 - '@tbd54566975/dwn-sdk-js': 0.2.22 + '@tbd54566975/dwn-sdk-js': 0.3.1 kysely: 0.26.3 multiformats: 12.0.1 readable-stream: 4.4.2 @@ -3463,13 +3463,28 @@ packages: buffer: 6.0.3 level: 8.0.0 ms: 2.1.3 + dev: false + + /@web5/dids@1.0.1: + resolution: {integrity: sha512-bAc+zwTDPvtFtd8T25XD0oUmSOBmeTpYSZyBz9w/EqZPKtZOFSc5oFS5qLtrh4YDkkcBqTG5ENQlE9fXs56zIQ==} + engines: {node: '>=18.0.0'} + dependencies: + '@decentralized-identity/ion-sdk': 1.0.1 + '@dnsquery/dns-packet': 6.1.1 + '@web5/common': 1.0.0 + '@web5/crypto': 1.0.0 + abstract-level: 1.0.4 + bencode: 4.0.0 + buffer: 6.0.3 + level: 8.0.0 + ms: 2.1.3 - /@web5/dwn-server@0.1.17: - resolution: {integrity: sha512-JkcZ4dQCZi6fCbEdyqqVqjDKE/AbaJj0auppgSnV5XxqjhKOtelvm+lLtppzyhcuPStlrem/9LkXR7yIgJ/JhQ==} + /@web5/dwn-server@0.2.1: + resolution: {integrity: sha512-RMt+YjVF3qru6zpjIdqB9vlCBfBt+H7QrrE/VsX8/VoQVRdrRPLnMIUVeYnfOn1pjXMWQs8cZjBypD04lMYZCA==} hasBin: true dependencies: - '@tbd54566975/dwn-sdk-js': 0.2.22 - '@tbd54566975/dwn-sql-store': 0.2.13 + '@tbd54566975/dwn-sdk-js': 0.3.1 + '@tbd54566975/dwn-sql-store': 0.4.1 better-sqlite3: 8.7.0 body-parser: 1.20.2 bytes: 3.1.2 @@ -3479,7 +3494,7 @@ packages: loglevel: 1.9.1 loglevel-plugin-prefix: 0.8.4 multiformats: 11.0.2 - mysql2: 3.9.1 + mysql2: 3.9.7 node-fetch: 3.3.1 pg: 8.11.3 pg-cursor: 2.10.3(pg@8.11.3) @@ -7070,8 +7085,8 @@ packages: resolution: {integrity: sha512-/sF3ee6zvScXMb1XFJ8gDsSnY+X8PbOyjIuBhtgis10W2Jx4ZjIhikUCIF9c4gpJxVnQIsPAFrSwTCuAjicP6g==} engines: {node: '>=8.0.0'} - /mysql2@3.9.1: - resolution: {integrity: sha512-3njoWAAhGBYy0tWBabqUQcLtczZUxrmmtc2vszQUekg3kTJyZ5/IeLC3Fo04u6y6Iy5Sba7pIIa2P/gs8D3ZeQ==} + /mysql2@3.9.7: + resolution: {integrity: sha512-KnJT8vYRcNAZv73uf9zpXqNbvBG7DJrs+1nACsjZP1HMJ1TgXEy8wnNilXAn/5i57JizXKtrUtwDB7HxT9DDpw==} engines: {node: '>= 8.0'} dependencies: denque: 2.1.0 From 922bc4292e91535fcf51525062f151023242fbf8 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Thu, 25 Apr 2024 19:29:06 -0400 Subject: [PATCH 2/9] update tags if new ones are passed, keep original tags if not --- packages/api/src/record.ts | 14 +++++++-- packages/api/tests/record.spec.ts | 48 +++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/packages/api/src/record.ts b/packages/api/src/record.ts index 06e7e9b02..20e947b33 100644 --- a/packages/api/src/record.ts +++ b/packages/api/src/record.ts @@ -9,7 +9,7 @@ import type { } from '@web5/agent'; import { DwnInterface } from '@web5/agent'; -import { Convert, NodeStream, removeUndefinedProperties, Stream } from '@web5/common'; +import { Convert, isEmptyObject, NodeStream, removeUndefinedProperties, Stream } from '@web5/common'; import { dataToBlob, SendCache } from './utils.js'; @@ -105,6 +105,9 @@ export type RecordUpdateParams = { /** The published status of the record. */ published?: DwnMessageDescriptor[DwnInterface.RecordsWrite]['published']; + + /** The tags associated with the updated record */ + tags?: DwnMessageDescriptor[DwnInterface.RecordsWrite]['tags']; } /** @@ -574,7 +577,7 @@ export class Record implements RecordModel { * * @beta */ - async update({ dateModified, data, ...params }: RecordUpdateParams): Promise { + async update({ dateModified, data, ...params }: RecordUpdateParams): Promise { // if there is a parentId, we remove it from the descriptor and set a parentContextId const { parentId, ...descriptor } = this._descriptor; @@ -599,9 +602,14 @@ export class Record implements RecordModel { ({ dataBlob } = dataToBlob(data, updateMessage.dataFormat)); } + if (isEmptyObject(updateMessage.tags)) { + delete updateMessage.tags; // Remove empty tags object from the updated message. + } + + // Throw an error if an attempt is made to modify immutable properties. // Note: `data` and `dateModified` have already been handled. - const mutableDescriptorProperties = new Set(['data', 'dataCid', 'dataSize', 'datePublished', 'messageTimestamp', 'published']); + const mutableDescriptorProperties = new Set(['data', 'dataCid', 'dataSize', 'datePublished', 'messageTimestamp', 'published', 'tags']); Record.verifyPermittedMutation(Object.keys(params), mutableDescriptorProperties); // If `published` is set to false, ensure that `datePublished` is undefined. Otherwise, DWN SDK's schema validation diff --git a/packages/api/tests/record.spec.ts b/packages/api/tests/record.spec.ts index feaa544ea..03a4660a9 100644 --- a/packages/api/tests/record.spec.ts +++ b/packages/api/tests/record.spec.ts @@ -2366,6 +2366,54 @@ describe('Record', () => { expect(error.message).to.include('is an immutable property. Its value cannot be changed.'); } }); + + it('should override tags on update', async () => { + const { status, record } = await dwnAlice.records.write({ + data : 'Hello, world!', + message : { + schema : 'foo/bar', + dataFormat : 'text/plain', + tags : { + tag1: 'value1', + tag2: 'value2' + } + } + }); + + expect(status.code).to.equal(202); + expect(record).to.not.be.undefined; + expect(await record.data.text()).to.equal('Hello, world!'); + expect(record.tags).to.deep.equal({ tag1: 'value1', tag2: 'value2'}) + + // if you do not modify the tags they do not change + const updateResultWithoutTags = await record!.update({ + data: 'hi', + }); + + expect(updateResultWithoutTags.status.code).to.equal(202); + expect(record.tags).to.deep.equal({ tag1: 'value1', tag2: 'value2'}); // unchanged + + // if you modify the tags they change + const updateResultWithTags = await record!.update({ + data: 'hi', + tags: { + tag1: 'value3', + tag3: 'value4' + } + }); + + expect(updateResultWithTags.status.code).to.equal(202); + expect(record.tags).to.deep.equal({ tag1: 'value3', tag3: 'value4'}); // changed + + // if you use an empty tags object it removes all tags + const updateResultWithEmptyTags = await record!.update({ + data: 'hi', + tags: {} + }); + + expect(updateResultWithEmptyTags.status.code).to.equal(202); + expect(record.tags).to.not.exist; // removed + }); }); describe('store()', () => { From 4fdf8deb131d05db056b5cf461ae8e398198cf38 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Thu, 25 Apr 2024 19:53:49 -0400 Subject: [PATCH 3/9] modify tests --- packages/api/tests/record.spec.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/api/tests/record.spec.ts b/packages/api/tests/record.spec.ts index 03a4660a9..4f49a6ac0 100644 --- a/packages/api/tests/record.spec.ts +++ b/packages/api/tests/record.spec.ts @@ -2392,10 +2392,10 @@ describe('Record', () => { expect(updateResultWithoutTags.status.code).to.equal(202); expect(record.tags).to.deep.equal({ tag1: 'value1', tag2: 'value2'}); // unchanged + expect(await record.data.text()).to.equal('hi'); // if you modify the tags they change const updateResultWithTags = await record!.update({ - data: 'hi', tags: { tag1: 'value3', tag3: 'value4' @@ -2404,15 +2404,16 @@ describe('Record', () => { expect(updateResultWithTags.status.code).to.equal(202); expect(record.tags).to.deep.equal({ tag1: 'value3', tag3: 'value4'}); // changed + expect(await record.data.text()).to.equal('hi'); // if you use an empty tags object it removes all tags const updateResultWithEmptyTags = await record!.update({ - data: 'hi', tags: {} }); expect(updateResultWithEmptyTags.status.code).to.equal(202); expect(record.tags).to.not.exist; // removed + expect(await record.data.text()).to.equal('hi'); }); }); From 1cc7592fffcddc0df6965d1f2cbc2e8df75475c0 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Fri, 26 Apr 2024 12:15:08 -0400 Subject: [PATCH 4/9] fix linting issues --- packages/api/tests/dwn-api.spec.ts | 4 ++-- packages/api/tests/record.spec.ts | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/api/tests/dwn-api.spec.ts b/packages/api/tests/dwn-api.spec.ts index b40dc0cec..07a90d99c 100644 --- a/packages/api/tests/dwn-api.spec.ts +++ b/packages/api/tests/dwn-api.spec.ts @@ -206,7 +206,7 @@ describe('DwnApi', () => { from : bobDid.uri, message : { permissionGrantId : 'bafyreiduimprbncdo2oruvjrvmfmwuyz4xx3d5biegqd2qntlryvuuosem', - filter : { + filter : { protocol: 'https://doesnotexist.com/protocol' } } @@ -244,7 +244,7 @@ describe('DwnApi', () => { message : { schema : 'foo/bar', dataFormat : 'text/plain', - tags: { + tags : { foo : 'bar', count : 2, bool : true diff --git a/packages/api/tests/record.spec.ts b/packages/api/tests/record.spec.ts index 4f49a6ac0..950987172 100644 --- a/packages/api/tests/record.spec.ts +++ b/packages/api/tests/record.spec.ts @@ -2374,8 +2374,8 @@ describe('Record', () => { schema : 'foo/bar', dataFormat : 'text/plain', tags : { - tag1: 'value1', - tag2: 'value2' + tag1 : 'value1', + tag2 : 'value2' } } }); @@ -2383,7 +2383,7 @@ describe('Record', () => { expect(status.code).to.equal(202); expect(record).to.not.be.undefined; expect(await record.data.text()).to.equal('Hello, world!'); - expect(record.tags).to.deep.equal({ tag1: 'value1', tag2: 'value2'}) + expect(record.tags).to.deep.equal({ tag1: 'value1', tag2: 'value2'}); // if you do not modify the tags they do not change const updateResultWithoutTags = await record!.update({ @@ -2397,8 +2397,8 @@ describe('Record', () => { // if you modify the tags they change const updateResultWithTags = await record!.update({ tags: { - tag1: 'value3', - tag3: 'value4' + tag1 : 'value3', + tag3 : 'value4' } }); From f8b1982e0b362d45913b6417879e6ed6f386c12e Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Fri, 26 Apr 2024 13:58:33 -0400 Subject: [PATCH 5/9] update tests and comments --- packages/api/src/record.ts | 11 ++++--- packages/api/tests/record.spec.ts | 49 +++++++++++++++++++++++++++---- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/packages/api/src/record.ts b/packages/api/src/record.ts index 20e947b33..04f63b1e0 100644 --- a/packages/api/src/record.ts +++ b/packages/api/src/record.ts @@ -592,6 +592,13 @@ export class Record implements RecordModel { recordId : this._recordId }; + // NOTE: The original Record's tags are copied to the update message, so that the tags are not lost. + // However if a user passes new tags in the `RecordUpdateParams` object, they will overwrite the original tags. + // If the updated tag object is empty or set to null, we remove the tags property to avoid schema validation errors in the DWN SDK. + if (isEmptyObject(updateMessage.tags) || updateMessage.tags === null) { + delete updateMessage.tags; + } + let dataBlob: Blob; if (data !== undefined) { // If `data` is being updated then `dataCid` and `dataSize` must be undefined and the `data` @@ -602,10 +609,6 @@ export class Record implements RecordModel { ({ dataBlob } = dataToBlob(data, updateMessage.dataFormat)); } - if (isEmptyObject(updateMessage.tags)) { - delete updateMessage.tags; // Remove empty tags object from the updated message. - } - // Throw an error if an attempt is made to modify immutable properties. // Note: `data` and `dateModified` have already been handled. diff --git a/packages/api/tests/record.spec.ts b/packages/api/tests/record.spec.ts index 950987172..148313b6b 100644 --- a/packages/api/tests/record.spec.ts +++ b/packages/api/tests/record.spec.ts @@ -2368,6 +2368,7 @@ describe('Record', () => { }); it('should override tags on update', async () => { + // create a record with tags const { status, record } = await dwnAlice.records.write({ data : 'Hello, world!', message : { @@ -2385,7 +2386,7 @@ describe('Record', () => { expect(await record.data.text()).to.equal('Hello, world!'); expect(record.tags).to.deep.equal({ tag1: 'value1', tag2: 'value2'}); - // if you do not modify the tags they do not change + // if you do not pass any tags they remain unchanged const updateResultWithoutTags = await record!.update({ data: 'hi', }); @@ -2394,7 +2395,7 @@ describe('Record', () => { expect(record.tags).to.deep.equal({ tag1: 'value1', tag2: 'value2'}); // unchanged expect(await record.data.text()).to.equal('hi'); - // if you modify the tags they change + // if you modify the tags they override the existing tags const updateResultWithTags = await record!.update({ tags: { tag1 : 'value3', @@ -2403,17 +2404,55 @@ describe('Record', () => { }); expect(updateResultWithTags.status.code).to.equal(202); - expect(record.tags).to.deep.equal({ tag1: 'value3', tag3: 'value4'}); // changed + expect(record.tags).to.deep.equal({ tag1: 'value3', tag3: 'value4'}); // changed to updated tags expect(await record.data.text()).to.equal('hi'); + }); + + it('should remove tags on update if tags are set to an empty object or null', async () => { + // create a record with tags + const { status, record } = await dwnAlice.records.write({ + data : 'Hello, world!', + message : { + schema : 'foo/bar', + dataFormat : 'text/plain', + tags : { + tag1 : 'value1', + tag2 : 'value2' + } + } + }); + + expect(status.code).to.equal(202); + expect(record).to.not.be.undefined; + expect(await record.data.text()).to.equal('Hello, world!'); + expect(record.tags).to.deep.equal({ tag1: 'value1', tag2: 'value2'}); - // if you use an empty tags object it removes all tags + // if you use an empty tags object it removes the tags const updateResultWithEmptyTags = await record!.update({ tags: {} }); expect(updateResultWithEmptyTags.status.code).to.equal(202); expect(record.tags).to.not.exist; // removed - expect(await record.data.text()).to.equal('hi'); + + // add tags to the record again + const updateResultWithTags = await record!.update({ + tags: { + tag1 : 'value3', + tag3 : 'value4' + } + }); + + expect(updateResultWithTags.status.code).to.equal(202); + expect(record.tags).to.deep.equal({ tag1: 'value3', tag3: 'value4'}); // added tags + + // if you use null it removes the tags + const updateResultWithNullTags = await record!.update({ + tags: null + }); + + expect(updateResultWithNullTags.status.code).to.equal(202); + expect(record.tags).to.not.exist; // removed }); }); From de96edaa01dc7469fb8605ee28e208e7e295140a Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Fri, 26 Apr 2024 14:14:56 -0400 Subject: [PATCH 6/9] remove empty line and uneeded space --- packages/api/src/record.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/api/src/record.ts b/packages/api/src/record.ts index 04f63b1e0..9358ddf8a 100644 --- a/packages/api/src/record.ts +++ b/packages/api/src/record.ts @@ -577,7 +577,7 @@ export class Record implements RecordModel { * * @beta */ - async update({ dateModified, data, ...params }: RecordUpdateParams): Promise { + async update({ dateModified, data, ...params }: RecordUpdateParams): Promise { // if there is a parentId, we remove it from the descriptor and set a parentContextId const { parentId, ...descriptor } = this._descriptor; @@ -609,7 +609,6 @@ export class Record implements RecordModel { ({ dataBlob } = dataToBlob(data, updateMessage.dataFormat)); } - // Throw an error if an attempt is made to modify immutable properties. // Note: `data` and `dateModified` have already been handled. const mutableDescriptorProperties = new Set(['data', 'dataCid', 'dataSize', 'datePublished', 'messageTimestamp', 'published', 'tags']); From a99755e65f7fc4ba4eb9cb6cc7ad900999c91244 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Fri, 26 Apr 2024 15:23:32 -0400 Subject: [PATCH 7/9] add tag query tests to the api --- packages/api/tests/dwn-api.spec.ts | 134 +++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/packages/api/tests/dwn-api.spec.ts b/packages/api/tests/dwn-api.spec.ts index 07a90d99c..7d18a9080 100644 --- a/packages/api/tests/dwn-api.spec.ts +++ b/packages/api/tests/dwn-api.spec.ts @@ -867,6 +867,69 @@ describe('DwnApi', () => { expect(publishedDescResults.records!.length).to.equal(3); expect(publishedDescResults.records.map(r => r.id)).to.eql([...publishedItems].reverse()); }); + + it('queries for records matching tags', async () => { + + // Write a record to the agent's local DWN that includes a tag `foo` with value `bar` + const { status, record } = await dwnAlice.records.write({ + data : 'Hello, world!', + message : { + schema : 'foo/bar', + dataFormat : 'text/plain', + tags : { + foo : 'bar', + } + } + }); + expect(status.code).to.equal(202); + + // Write a record to the agent's local DWN that includes a tag `foo` with value `baz` + const { status: status2 } = await dwnAlice.records.write({ + data : 'Hello, world!', + message : { + schema : 'foo/bar', + dataFormat : 'text/plain', + tags : { + foo : 'baz', + } + } + }); + expect(status2.code).to.equal(202); + + // Control: query the agent's local DWN for the record without any tag filters + const result = await dwnAlice.records.query({ + message: { + filter: { + schema: 'foo/bar' + } + } + }); + + // should return both records + expect(result.status.code).to.equal(200); + expect(result.records).to.exist; + expect(result.records!.length).to.equal(2); + + + // Query the agent's local DWN for the record using the tags. + const fooBarResult = await dwnAlice.records.query({ + message: { + filter: { + schema: 'foo/bar', + tags: { + foo : 'bar', + } + } + } + }); + + // should only return the record with the tag `foo` and value `bar` + expect(fooBarResult.status.code).to.equal(200); + expect(fooBarResult.records).to.exist; + expect(fooBarResult.records!.length).to.equal(1); + expect(fooBarResult.records![0].id).to.equal(record.id); + expect(fooBarResult.records![0].tags).to.deep.equal({ foo: 'bar' }); + }); }); describe('from: did', () => { @@ -973,6 +1036,77 @@ describe('DwnApi', () => { const [ recordOnBobsDwn ] = bobQueryResult.records; expect(recordOnBobsDwn.author).to.equal(aliceDid.uri); }); + + it('queries for records matching tags', async () => { + + // Write a record to alice's remote DWN that includes a tag `foo` with value `bar` + const { status, record } = await dwnAlice.records.write({ + store : false, + data : 'Hello, world!', + message : { + schema : 'foo/bar', + dataFormat : 'text/plain', + tags : { + foo : 'bar', + } + } + }); + expect(status.code).to.equal(202); + const { status: sendFooBarStatus } = await record.send(aliceDid.uri); + expect(sendFooBarStatus.code).to.equal(202); + + // Write a record to alice's remote DWN that includes a tag `foo` with value `baz` + const { status: status2, record: record2 } = await dwnAlice.records.write({ + store : false, + data : 'Hello, world!', + message : { + schema : 'foo/bar', + dataFormat : 'text/plain', + tags : { + foo : 'baz', + } + } + }); + expect(status2.code).to.equal(202); + const { status: sendFooBazStatus } = await record2.send(aliceDid.uri); + expect(sendFooBazStatus.code).to.equal(202); + + // Control: query the agent's local DWN for the record without any tag filters + const result = await dwnAlice.records.query({ + from : aliceDid.uri, + message : { + filter: { + schema: 'foo/bar' + } + } + }); + + // should return both records + expect(result.status.code).to.equal(200); + expect(result.records).to.exist; + expect(result.records!.length).to.equal(2); + + + // Query the agent's local DWN for the record using the tags. + const fooBarResult = await dwnAlice.records.query({ + from : aliceDid.uri, + message : { + filter: { + schema: 'foo/bar', + tags: { + foo : 'bar', + } + } + } + }); + + // should only return the record with the tag `foo` and value `bar` + expect(fooBarResult.status.code).to.equal(200); + expect(fooBarResult.records).to.exist; + expect(fooBarResult.records!.length).to.equal(1); + expect(fooBarResult.records![0].id).to.equal(record.id); + expect(fooBarResult.records![0].tags).to.deep.equal({ foo: 'bar' }); + }); }); }); From 74f8d55820a7935617f849869d4ac12ce48184eb Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Fri, 26 Apr 2024 15:23:51 -0400 Subject: [PATCH 8/9] fix linting --- packages/api/tests/dwn-api.spec.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/api/tests/dwn-api.spec.ts b/packages/api/tests/dwn-api.spec.ts index 7d18a9080..125d27e76 100644 --- a/packages/api/tests/dwn-api.spec.ts +++ b/packages/api/tests/dwn-api.spec.ts @@ -877,7 +877,7 @@ describe('DwnApi', () => { schema : 'foo/bar', dataFormat : 'text/plain', tags : { - foo : 'bar', + foo: 'bar', } } }); @@ -890,7 +890,7 @@ describe('DwnApi', () => { schema : 'foo/bar', dataFormat : 'text/plain', tags : { - foo : 'baz', + foo: 'baz', } } }); @@ -915,9 +915,9 @@ describe('DwnApi', () => { const fooBarResult = await dwnAlice.records.query({ message: { filter: { - schema: 'foo/bar', - tags: { - foo : 'bar', + schema : 'foo/bar', + tags : { + foo: 'bar', } } } @@ -1047,7 +1047,7 @@ describe('DwnApi', () => { schema : 'foo/bar', dataFormat : 'text/plain', tags : { - foo : 'bar', + foo: 'bar', } } }); @@ -1063,7 +1063,7 @@ describe('DwnApi', () => { schema : 'foo/bar', dataFormat : 'text/plain', tags : { - foo : 'baz', + foo: 'baz', } } }); @@ -1092,9 +1092,9 @@ describe('DwnApi', () => { from : aliceDid.uri, message : { filter: { - schema: 'foo/bar', - tags: { - foo : 'bar', + schema : 'foo/bar', + tags : { + foo: 'bar', } } } From 4eb53bd4cdfed3e845b9b246b16f1d09965e6b29 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Fri, 26 Apr 2024 16:15:12 -0400 Subject: [PATCH 9/9] add shamilovtim to CODEOWNERS for `agent` and `api` packages --- CODEOWNERS | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 1382d9388..5e71ff8b1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -18,9 +18,9 @@ # These are owners of any file in the `agent`, `user-agent`, `proxy-agent`, `identity-agent`, and # `api` packages and their sub-directories. -/packages/agent @lirancohen @frankhinek @csuwildcat @mistermoe -/packages/proxy-agent @lirancohen @frankhinek @csuwildcat @mistermoe -/packages/user-agent @lirancohen @frankhinek @csuwildcat @mistermoe -/packages/identity-agent @lirancohen @frankhinek @csuwildcat @mistermoe -/packages/api @lirancohen @frankhinek @csuwildcat @mistermoe - \ No newline at end of file +/packages/agent @lirancohen @frankhinek @csuwildcat @mistermoe @shamilovtim +/packages/proxy-agent @lirancohen @frankhinek @csuwildcat @mistermoe @shamilovtim +/packages/user-agent @lirancohen @frankhinek @csuwildcat @mistermoe @shamilovtim +/packages/identity-agent @lirancohen @frankhinek @csuwildcat @mistermoe @shamilovtim +/packages/api @lirancohen @frankhinek @csuwildcat @mistermoe @shamilovtim +