From d6670908ce88a95c0c37c28685e7497a3bdaf4f8 Mon Sep 17 00:00:00 2001 From: Henry Tsai Date: Fri, 4 Oct 2024 15:15:32 -0700 Subject: [PATCH] Updated RecordsRead to return RecordsDelete and initial RecordsWrite when record is deleted (#814) Prior to this change, a 404 status is returned without any data when a RecordsRead is performed against a deleted record. But 404 status alone prevents a the RecordsDelete from being imported into the caller's DWN for the purpose updating the record's state. This PR updated RecordsRead to return RecordsDelete and initial RecordsWrite when record is deleted. --------- Co-authored-by: Liran Cohen --- package-lock.json | 429 +++++++++++++++--- package.json | 10 +- src/handlers/records-read.ts | 19 +- src/interfaces/records-delete.ts | 10 +- src/interfaces/records-write.ts | 23 +- src/types/records-types.ts | 13 +- tests/scenarios/deleted-record.spec.ts | 117 +++++ tests/test-suite.ts | 2 + .../protocol-definitions/free-for-all.json | 2 +- 9 files changed, 542 insertions(+), 83 deletions(-) create mode 100644 tests/scenarios/deleted-record.spec.ts diff --git a/package-lock.json b/package-lock.json index c2ec79454..a3a7b72d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,7 +54,7 @@ "@types/varint": "6.0.0", "@typescript-eslint/eslint-plugin": "^7.9.0", "@typescript-eslint/parser": "^7.9.0", - "c8": "^8.0.0", + "c8": "^10.1.2", "chai": "4.3.6", "chai-as-promised": "7.1.1", "cross-env": "7.0.3", @@ -64,7 +64,7 @@ "eslint-plugin-todo-plz": "1.3.0", "events": "3.3.0", "istanbul-badges-readme": "1.8.1", - "karma": "6.4.1", + "karma": "^6.4.4", "karma-chai": "0.1.0", "karma-chrome-launcher": "3.1.1", "karma-esbuild": "2.2.5", @@ -334,6 +334,102 @@ "npm": ">=7.0.0" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@isaacs/ttlcache": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", @@ -519,6 +615,16 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -669,9 +775,9 @@ "dev": true }, "node_modules/@types/cors": { - "version": "2.8.13", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", - "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", "dev": true, "dependencies": { "@types/node": "*" @@ -1989,27 +2095,75 @@ } }, "node_modules/c8": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/c8/-/c8-8.0.0.tgz", - "integrity": "sha512-XHA5vSfCLglAc0Xt8eLBZMv19lgiBSjnb1FLAQgnwkuhJYEonpilhEB4Ea3jPAbm0FhD6VVJrc0z73jPe7JyGQ==", + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.2.tgz", + "integrity": "sha512-Qr6rj76eSshu5CgRYvktW0uM0CFY0yi4Fd5D0duDXO6sYinyopmftUiJVuzBQxQcwQLor7JWDVRP+dUfCmzgJw==", "dev": true, "dependencies": { "@bcoe/v8-coverage": "^0.2.3", "@istanbuljs/schema": "^0.1.3", "find-up": "^5.0.0", - "foreground-child": "^2.0.0", + "foreground-child": "^3.1.1", "istanbul-lib-coverage": "^3.2.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-reports": "^3.1.4", - "rimraf": "^3.0.2", - "test-exclude": "^6.0.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "test-exclude": "^7.0.1", "v8-to-istanbul": "^9.0.0", - "yargs": "^16.2.0", - "yargs-parser": "^20.2.9" + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" }, "bin": { "c8": "bin/c8.js" }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "monocart-coverage-reports": "^2" + }, + "peerDependenciesMeta": { + "monocart-coverage-reports": { + "optional": true + } + } + }, + "node_modules/c8/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/c8/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/c8/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, "engines": { "node": ">=12" } @@ -2336,9 +2490,9 @@ "dev": true }, "node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "dev": true, "engines": { "node": ">= 0.6" @@ -2800,6 +2954,12 @@ "url": "https://bevry.me/fund" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "node_modules/eciesjs": { "version": "0.4.5", "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.5.tgz", @@ -2864,9 +3024,9 @@ } }, "node_modules/engine.io": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", - "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.1.tgz", + "integrity": "sha512-NEpDCw9hrvBW+hVEOK4T7v0jFJ++KgtPl4jKFwsZVfG1XhS0dCrSb3VMb9gPAd7VAdW52VT1EnaNiU2vM8C0og==", "dev": true, "dependencies": { "@types/cookie": "^0.4.1", @@ -2885,9 +3045,9 @@ } }, "node_modules/engine.io-parser": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", - "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", "dev": true, "engines": { "node": ">=10.0.0" @@ -3463,16 +3623,19 @@ } }, "node_modules/foreground-child": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", - "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", "dev": true, "dependencies": { "cross-spawn": "^7.0.0", - "signal-exit": "^3.0.2" + "signal-exit": "^4.0.1" }, "engines": { - "node": ">=8.0.0" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/form-data-encoder": { @@ -4501,32 +4664,32 @@ } }, "node_modules/istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, "engines": { "node": ">=8" } }, "node_modules/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, "dependencies": { "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", + "make-dir": "^4.0.0", "supports-color": "^7.1.0" }, "engines": { - "node": ">=8" + "node": ">=10" } }, "node_modules/istanbul-reports": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", - "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, "dependencies": { "html-escaper": "^2.0.0", @@ -4676,6 +4839,21 @@ "npm": ">=7.0.0" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -4726,9 +4904,9 @@ "dev": true }, "node_modules/karma": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.1.tgz", - "integrity": "sha512-Cj57NKOskK7wtFWSlMvZf459iX+kpYIPXmkNUzP2WAFcA7nhr/ALn5R7sw3w+1udFDcpMx/tuB8d5amgm3ijaA==", + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", + "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", "dev": true, "dependencies": { "@colors/colors": "1.5.0", @@ -4750,7 +4928,7 @@ "qjobs": "^1.2.0", "range-parser": "^1.2.1", "rimraf": "^3.0.2", - "socket.io": "^4.4.1", + "socket.io": "^4.7.2", "source-map": "^0.6.1", "tmp": "^0.2.1", "ua-parser-js": "^0.7.30", @@ -5451,6 +5629,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -5985,6 +6172,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -6064,6 +6257,28 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, "node_modules/path-to-regexp": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.1.0.tgz", @@ -6793,10 +7008,16 @@ } }, "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/sinon": { "version": "18.0.1", @@ -6841,21 +7062,21 @@ } }, "node_modules/socket.io": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.1.tgz", - "integrity": "sha512-W+utHys2w//dhFjy7iQQu9sGd3eokCjGbl2r59tyLqNiJJBdIebn3GAKEXBr3osqHTObJi2die/25bCx2zsaaw==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz", + "integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==", "dev": true, "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.3.2", - "engine.io": "~6.5.0", + "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" }, "engines": { - "node": ">=10.0.0" + "node": ">=10.2.0" } }, "node_modules/socket.io-adapter": { @@ -7022,6 +7243,21 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -7034,6 +7270,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -7117,17 +7366,61 @@ } }, "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", "dev": true, "dependencies": { "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" + "glob": "^10.4.1", + "minimatch": "^9.0.4" }, "engines": { - "node": ">=8" + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/text-table": { @@ -7612,6 +7905,24 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index 43ba8d8af..a1b2e2bf8 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,7 @@ "@types/varint": "6.0.0", "@typescript-eslint/eslint-plugin": "^7.9.0", "@typescript-eslint/parser": "^7.9.0", - "c8": "^8.0.0", + "c8": "^10.1.2", "chai": "4.3.6", "chai-as-promised": "7.1.1", "cross-env": "7.0.3", @@ -116,7 +116,7 @@ "eslint-plugin-todo-plz": "1.3.0", "events": "3.3.0", "istanbul-badges-readme": "1.8.1", - "karma": "6.4.1", + "karma": "^6.4.4", "karma-chai": "0.1.0", "karma-chrome-launcher": "3.1.1", "karma-esbuild": "2.2.5", @@ -138,11 +138,7 @@ "util": "0.12.4" }, "overrides": { - "c8": { - "istanbul-lib-report": { - "make-dir": "^4.0.0" - } - }, + "cookie": "^0.7.1", "@typescript-eslint/eslint-plugin": { "eslint": "^9.2.0" } diff --git a/src/handlers/records-read.ts b/src/handlers/records-read.ts index 144855637..2c3553eb6 100644 --- a/src/handlers/records-read.ts +++ b/src/handlers/records-read.ts @@ -3,7 +3,7 @@ import type { DidResolver } from '@web5/dids'; import type { Filter } from '../types/query-types.js'; import type { MessageStore } from '../types//message-store.js'; import type { MethodHandler } from '../types/method-handler.js'; -import type { RecordsQueryReplyEntry, RecordsReadMessage, RecordsReadReply } from '../types/records-types.js'; +import type { RecordsDeleteMessage, RecordsQueryReplyEntry, RecordsReadMessage, RecordsReadReply } from '../types/records-types.js'; import { authenticate } from '../core/auth.js'; import { DataStream } from '../utils/data-stream.js'; @@ -45,8 +45,8 @@ export class RecordsReadHandler implements MethodHandler { } // get the latest active messages matching the supplied filter - // only RecordsWrite messages will be returned due to 'isLatestBaseState' being set to true. const query: Filter = { + // NOTE: we don't filter by `method` so that we get both RecordsWrite and RecordsDelete messages interface : DwnInterfaceName.Records, isLatestBaseState : true, ...Records.convertFilter(message.descriptor.filter) @@ -63,7 +63,20 @@ export class RecordsReadHandler implements MethodHandler { ), 400); } - const matchedRecordsWrite = existingMessages[0] as RecordsQueryReplyEntry; + const matchedMessage = existingMessages[0]; + + if (matchedMessage.descriptor.method === DwnMethodName.Delete) { + const recordsDeleteMessage = matchedMessage as RecordsDeleteMessage; + const initialWrite = await RecordsWrite.fetchInitialRecordsWriteMessage(this.messageStore, tenant, recordsDeleteMessage.descriptor.recordId); + return { + status : { code: 404, detail: 'Not Found' }, + delete : recordsDeleteMessage, + initialWrite + }; + } + + const matchedRecordsWrite = matchedMessage as RecordsQueryReplyEntry; + try { await RecordsReadHandler.authorizeRecordsRead(tenant, recordsRead, await RecordsWrite.parse(matchedRecordsWrite), this.messageStore); } catch (error) { diff --git a/src/interfaces/records-delete.ts b/src/interfaces/records-delete.ts index 3416eeee9..033c6f606 100644 --- a/src/interfaces/records-delete.ts +++ b/src/interfaces/records-delete.ts @@ -87,15 +87,11 @@ export class RecordsDelete extends AbstractMessage { // we add the immutable properties from the initial RecordsWrite message in order to use them when querying relevant deletes. const { protocol, protocolPath, recipient, schema, parentId, dateCreated } = initialWrite.descriptor; - // NOTE: the "trick" not may not be apparent on how a query is able to omit deleted records: - // we intentionally not add index for `isLatestBaseState` at all, this means that upon a successful delete, - // no messages with the record ID will match any query because queries by design filter by `isLatestBaseState = true`, - // `isLatestBaseState` for the initial delete would have been toggled to `false` const indexes: { [key:string]: string | boolean | undefined } = { - // isLatestBaseState : "true", // intentionally showing that this index is omitted + isLatestBaseState : true, protocol, protocolPath, recipient, schema, parentId, dateCreated, - contextId : initialWrite.contextId, - author : this.author!, + contextId : initialWrite.contextId, + author : this.author!, ...descriptor }; removeUndefinedProperties(indexes); diff --git a/src/interfaces/records-write.ts b/src/interfaces/records-write.ts index 49af333d8..5cdb5c6cc 100644 --- a/src/interfaces/records-write.ts +++ b/src/interfaces/records-write.ts @@ -1034,7 +1034,7 @@ export class RecordsWrite implements MessageInterface { /** * Fetches the initial RecordsWrite of a record. - * @returns The initial RecordsWrite if found; `undefined` if the record is not found. + * @returns The initial RecordsWrite if found; `undefined` otherwise. */ public static async fetchInitialRecordsWrite( messageStore: MessageStore, @@ -1042,6 +1042,24 @@ export class RecordsWrite implements MessageInterface { recordId: string ): Promise { + const initialRecordsWriteMessage = await RecordsWrite.fetchInitialRecordsWriteMessage(messageStore, tenant, recordId); + if (initialRecordsWriteMessage === undefined) { + return undefined; + } + + const initialRecordsWrite = await RecordsWrite.parse(initialRecordsWriteMessage); + return initialRecordsWrite; + } + + /** + * Fetches the initial RecordsWrite message of a record. + * @returns The initial RecordsWriteMessage if found; `undefined` otherwise. + */ + public static async fetchInitialRecordsWriteMessage( + messageStore: MessageStore, + tenant: string, + recordId: string + ): Promise { const query = { entryId: recordId }; const { messages } = await messageStore.query(tenant, [query]); @@ -1049,7 +1067,6 @@ export class RecordsWrite implements MessageInterface { return undefined; } - const initialRecordsWrite = await RecordsWrite.parse(messages[0] as RecordsWriteMessage); - return initialRecordsWrite; + return messages[0] as RecordsWriteMessage; } } diff --git a/src/types/records-types.ts b/src/types/records-types.ts index a2abe2934..8dd1b1b26 100644 --- a/src/types/records-types.ts +++ b/src/types/records-types.ts @@ -203,10 +203,17 @@ export type RecordsReadMessage = { }; export type RecordsReadReply = GenericMessageReply & { + /** + * The RecordsDelete if the record is deleted. + */ + delete?: RecordsDeleteMessage; + + /** + * The initial write of the record if the returned RecordsWrite message itself is not the initial write or if a RecordsDelete is returned. + */ + initialWrite?: RecordsWriteMessage; + record?: RecordsWriteMessage & { - /** - * The initial write of the record if the returned RecordsWrite message itself is not the initial write. - */ initialWrite?: RecordsWriteMessage; data: Readable; }; diff --git a/tests/scenarios/deleted-record.spec.ts b/tests/scenarios/deleted-record.spec.ts new file mode 100644 index 000000000..159b43354 --- /dev/null +++ b/tests/scenarios/deleted-record.spec.ts @@ -0,0 +1,117 @@ +import type { DidResolver } from '@web5/dids'; +import type { EventStream } from '../../src/types/subscriptions.js'; +import type { DataStore, EventLog, MessageStore, ResumableTaskStore } from '../../src/index.js'; + +import chaiAsPromised from 'chai-as-promised'; +import freeForAllProtocolDefinition from '../vectors/protocol-definitions/free-for-all.json' assert { type: 'json' }; +import sinon from 'sinon'; + +import { TestDataGenerator } from '../utils/test-data-generator.js'; +import { TestEventStream } from '../test-event-stream.js'; +import { TestStores } from '../test-stores.js'; +import { TestStubGenerator } from '../utils/test-stub-generator.js'; + +import chai, { expect } from 'chai'; +import { DataStream, Dwn, Jws, ProtocolsConfigure, RecordsRead } from '../../src/index.js'; +import { DidKey, UniversalResolver } from '@web5/dids'; +import { Encoder, RecordsDelete, RecordsWrite } from '../../src/index.js'; + +chai.use(chaiAsPromised); + +export function testDeletedRecordScenarios(): void { + describe('End-to-end Scenarios Spanning Features', () => { + let didResolver: DidResolver; + let messageStore: MessageStore; + let dataStore: DataStore; + let resumableTaskStore: ResumableTaskStore; + let eventLog: EventLog; + let eventStream: EventStream; + let dwn: Dwn; + + // important to follow the `before` and `after` pattern to initialize and clean the stores in tests + // so that different test suites can reuse the same backend store for testing + before(async () => { + didResolver = new UniversalResolver({ didResolvers: [DidKey] }); + + const stores = TestStores.get(); + messageStore = stores.messageStore; + dataStore = stores.dataStore; + resumableTaskStore = stores.resumableTaskStore; + eventLog = stores.eventLog; + eventStream = TestEventStream.get(); + + dwn = await Dwn.create({ didResolver, messageStore, dataStore, eventLog, eventStream, resumableTaskStore }); + }); + + beforeEach(async () => { + sinon.restore(); // wipe all previous stubs/spies/mocks/fakes + + // clean up before each test rather than after so that a test does not depend on other tests to do the clean up + await messageStore.clear(); + await dataStore.clear(); + await resumableTaskStore.clear(); + await eventLog.clear(); + }); + + after(async () => { + await dwn.close(); + }); + + it('should return the RecordsDelete and initial RecordsWrite when reading a deleted record', async () => { + // Scenario: + // 1. Alice deletes an existing record. + // 2. Alice attempts to read the deleted record. + // Expected outcome: Alice should get a 404 error with the reply containing the deleted record and the initial write of the record. + + // 0. Setting up a protocol and write a record + const alice = await TestDataGenerator.generatePersona(); + TestStubGenerator.stubDidResolver(didResolver, [alice]); + + const protocolDefinition = freeForAllProtocolDefinition; + const protocolsConfigure = await ProtocolsConfigure.create({ + signer : Jws.createSigner(alice), + definition : protocolDefinition + }); + const protocolsConfigureForAliceReply = await dwn.processMessage( + alice.did, + protocolsConfigure.message + ); + expect(protocolsConfigureForAliceReply.status.code).to.equal(202); + + const data = Encoder.stringToBytes('some post content'); + const { message: recordsWriteMessage } = await RecordsWrite.create({ + signer : Jws.createSigner(alice), + protocol : protocolDefinition.protocol, + protocolPath : 'post', + schema : protocolDefinition.types.post.schema, + dataFormat : protocolDefinition.types.post.dataFormats[0], + data, + }); + const writeReply = await dwn.processMessage(alice.did, recordsWriteMessage, { dataStream: DataStream.fromBytes(data) }); + expect(writeReply.status.code).to.equal(202); + + // 1. Alice deletes an existing record. + const recordsDelete = await RecordsDelete.create({ + signer : Jws.createSigner(alice), + recordId : recordsWriteMessage.recordId + }); + + const deleteReply = await dwn.processMessage(alice.did, recordsDelete.message); + expect(deleteReply.status.code).to.equal(202); + + // 2. Alice attempts to read the deleted record. + const readData = await RecordsRead.create({ + signer : Jws.createSigner(alice), + filter : { recordId: recordsWriteMessage.recordId } + }); + const readReply = await dwn.processMessage(alice.did, readData.message); + + // Expected outcome: Alice should get a 404 error with the reply containing the deleted record and the initial write of the record. + expect(readReply.status.code).to.equal(404); + expect(readReply.delete).to.exist; + expect(readReply.delete).to.deep.equal(recordsDelete.message); + expect(readReply.initialWrite).to.exist; + expect(readReply.initialWrite).to.deep.equal(recordsWriteMessage); + }); + }); +} \ No newline at end of file diff --git a/tests/test-suite.ts b/tests/test-suite.ts index 9e73f0e46..c567c9277 100644 --- a/tests/test-suite.ts +++ b/tests/test-suite.ts @@ -1,6 +1,7 @@ import type { DataStore, EventLog, EventStream, MessageStore, ResumableTaskStore } from '../src/index.js'; import { testAuthorDelegatedGrant } from './features/author-delegated-grant.spec.js'; +import { testDeletedRecordScenarios } from './scenarios/deleted-record.spec.js'; import { testDwnClass } from './dwn.spec.js'; import { testEndToEndScenarios } from './scenarios/end-to-end-tests.spec.js'; import { testEventLog } from './event-log/event-log.spec.js'; @@ -83,6 +84,7 @@ export class TestSuite { testResumableTasks(); // scenario tests + testDeletedRecordScenarios(); testEndToEndScenarios(); testMessagesQueryScenarios(); testNestedRoleScenarios(); diff --git a/tests/vectors/protocol-definitions/free-for-all.json b/tests/vectors/protocol-definitions/free-for-all.json index 278e48ca0..5d3f71c13 100644 --- a/tests/vectors/protocol-definitions/free-for-all.json +++ b/tests/vectors/protocol-definitions/free-for-all.json @@ -3,7 +3,7 @@ "published": true, "types": { "post": { - "schema": "eph", + "schema": "post", "dataFormats": [ "application/json" ]