From 82b4892fda149a6bd97603f0902c14e437400efb Mon Sep 17 00:00:00 2001 From: Atridad Lahiji Date: Mon, 16 Jun 2025 10:03:41 -0600 Subject: [PATCH] fix --- package.json | 1 + pnpm-lock.yaml | 232 ++++++++++++++++++++++++++++ server.js | 257 +++++++++++++++---------------- todo-service.js | 394 ++++++++++++++++++++++++++---------------------- 4 files changed, 573 insertions(+), 311 deletions(-) diff --git a/package.json b/package.json index a80dce3..b52253b 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "type": "module", "dependencies": { + "@libsql/client": "^0.15.9", "@signalapp/libsignal-client": "^0.74.1", "dotenv": "^16.5.0", "express": "^5.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fdf310f..3458b5e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@libsql/client': + specifier: ^0.15.9 + version: 0.15.9 '@signalapp/libsignal-client': specifier: ^0.74.1 version: 0.74.1 @@ -26,9 +29,79 @@ importers: packages: + '@libsql/client@0.15.9': + resolution: {integrity: sha512-VT3do0a0vwYVaNcp/y05ikkKS3OrFR5UeEf5SUuYZVgKVl1Nc1k9ajoYSsOid8AD/vlhLDB5yFQaV4HmT/OB9w==} + + '@libsql/core@0.15.9': + resolution: {integrity: sha512-4OVdeAmuaCUq5hYT8NNn0nxlO9AcA/eTjXfUZ+QK8MT3Dz7Z76m73x7KxjU6I64WyXX98dauVH2b9XM+d84npw==} + + '@libsql/darwin-arm64@0.5.13': + resolution: {integrity: sha512-ASz/EAMLDLx3oq9PVvZ4zBXXHbz2TxtxUwX2xpTRFR4V4uSHAN07+jpLu3aK5HUBLuv58z7+GjaL5w/cyjR28Q==} + cpu: [arm64] + os: [darwin] + + '@libsql/darwin-x64@0.5.13': + resolution: {integrity: sha512-kzglniv1difkq8opusSXM7u9H0WoEPeKxw0ixIfcGfvlCVMJ+t9UNtXmyNHW68ljdllje6a4C6c94iPmIYafYA==} + cpu: [x64] + os: [darwin] + + '@libsql/hrana-client@0.7.0': + resolution: {integrity: sha512-OF8fFQSkbL7vJY9rfuegK1R7sPgQ6kFMkDamiEccNUvieQ+3urzfDFI616oPl8V7T9zRmnTkSjMOImYCAVRVuw==} + + '@libsql/isomorphic-fetch@0.3.1': + resolution: {integrity: sha512-6kK3SUK5Uu56zPq/Las620n5aS9xJq+jMBcNSOmjhNf/MUvdyji4vrMTqD7ptY7/4/CAVEAYDeotUz60LNQHtw==} + engines: {node: '>=18.0.0'} + + '@libsql/isomorphic-ws@0.1.5': + resolution: {integrity: sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==} + + '@libsql/linux-arm-gnueabihf@0.5.13': + resolution: {integrity: sha512-UEW+VZN2r0mFkfztKOS7cqfS8IemuekbjUXbXCwULHtusww2QNCXvM5KU9eJCNE419SZCb0qaEWYytcfka8qeA==} + cpu: [arm] + os: [linux] + + '@libsql/linux-arm-musleabihf@0.5.13': + resolution: {integrity: sha512-NMDgLqryYBv4Sr3WoO/m++XDjR5KLlw9r/JK4Ym6A1XBv2bxQQNhH0Lxx3bjLW8qqhBD4+0xfms4d2cOlexPyA==} + cpu: [arm] + os: [linux] + + '@libsql/linux-arm64-gnu@0.5.13': + resolution: {integrity: sha512-/wCxVdrwl1ee6D6LEjwl+w4SxuLm5UL9Kb1LD5n0bBGs0q+49ChdPPh7tp175iRgkcrTgl23emymvt1yj3KxVQ==} + cpu: [arm64] + os: [linux] + + '@libsql/linux-arm64-musl@0.5.13': + resolution: {integrity: sha512-xnVAbZIanUgX57XqeI5sNaDnVilp0Di5syCLSEo+bRyBobe/1IAeehNZpyVbCy91U2N6rH1C/mZU7jicVI9x+A==} + cpu: [arm64] + os: [linux] + + '@libsql/linux-x64-gnu@0.5.13': + resolution: {integrity: sha512-/mfMRxcQAI9f8t7tU3QZyh25lXgXKzgin9B9TOSnchD73PWtsVhlyfA6qOCfjQl5kr4sHscdXD5Yb3KIoUgrpQ==} + cpu: [x64] + os: [linux] + + '@libsql/linux-x64-musl@0.5.13': + resolution: {integrity: sha512-rdefPTpQCVwUjIQYbDLMv3qpd5MdrT0IeD0UZPGqhT9AWU8nJSQoj2lfyIDAWEz7PPOVCY4jHuEn7FS2sw9kRA==} + cpu: [x64] + os: [linux] + + '@libsql/win32-x64-msvc@0.5.13': + resolution: {integrity: sha512-aNcmDrD1Ws+dNZIv9ECbxBQumqB9MlSVEykwfXJpqv/593nABb8Ttg5nAGUPtnADyaGDTrGvPPP81d/KsKho4Q==} + cpu: [x64] + os: [win32] + + '@neon-rs/load@0.0.4': + resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} + '@signalapp/libsignal-client@0.74.1': resolution: {integrity: sha512-PEJou0yrBvxaAGg7JjONlRNM/t3PCBuY96wu7W6+57e38/7Mibo9kAMfE5B8DgVv+DUNMW9AgJhx5McCoIXYew==} + '@types/node@24.0.3': + resolution: {integrity: sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -65,6 +138,10 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + debug@4.4.1: resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} engines: {node: '>=6.0'} @@ -78,6 +155,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + detect-libc@2.0.2: + resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} + engines: {node: '>=8'} + dotenv@16.5.0: resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} engines: {node: '>=12'} @@ -116,10 +197,18 @@ packages: resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} engines: {node: '>= 18'} + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + finalhandler@2.1.0: resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} engines: {node: '>= 0.8'} + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -169,6 +258,14 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + js-base64@3.7.7: + resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==} + + libsql@0.5.13: + resolution: {integrity: sha512-5Bwoa/CqzgkTwySgqHA5TsaUDRrdLIbdM4egdPcaAnqO3aC+qAgS6BwdzuZwARA5digXwiskogZ8H7Yy4XfdOg==} + cpu: [x64, arm64, wasm32, arm] + os: [darwin, linux, win32] + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -196,6 +293,15 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-gyp-build@4.8.4: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true @@ -219,6 +325,9 @@ packages: resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} engines: {node: '>=16'} + promise-limit@2.7.0: + resolution: {integrity: sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -292,6 +401,9 @@ packages: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} + undici-types@7.8.0: + resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} + unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -304,6 +416,10 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -321,12 +437,84 @@ packages: snapshots: + '@libsql/client@0.15.9': + dependencies: + '@libsql/core': 0.15.9 + '@libsql/hrana-client': 0.7.0 + js-base64: 3.7.7 + libsql: 0.5.13 + promise-limit: 2.7.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@libsql/core@0.15.9': + dependencies: + js-base64: 3.7.7 + + '@libsql/darwin-arm64@0.5.13': + optional: true + + '@libsql/darwin-x64@0.5.13': + optional: true + + '@libsql/hrana-client@0.7.0': + dependencies: + '@libsql/isomorphic-fetch': 0.3.1 + '@libsql/isomorphic-ws': 0.1.5 + js-base64: 3.7.7 + node-fetch: 3.3.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@libsql/isomorphic-fetch@0.3.1': {} + + '@libsql/isomorphic-ws@0.1.5': + dependencies: + '@types/ws': 8.18.1 + ws: 8.18.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@libsql/linux-arm-gnueabihf@0.5.13': + optional: true + + '@libsql/linux-arm-musleabihf@0.5.13': + optional: true + + '@libsql/linux-arm64-gnu@0.5.13': + optional: true + + '@libsql/linux-arm64-musl@0.5.13': + optional: true + + '@libsql/linux-x64-gnu@0.5.13': + optional: true + + '@libsql/linux-x64-musl@0.5.13': + optional: true + + '@libsql/win32-x64-msvc@0.5.13': + optional: true + + '@neon-rs/load@0.0.4': {} + '@signalapp/libsignal-client@0.74.1': dependencies: node-gyp-build: 4.8.4 type-fest: 4.41.0 uuid: 11.1.0 + '@types/node@24.0.3': + dependencies: + undici-types: 7.8.0 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 24.0.3 + accepts@2.0.0: dependencies: mime-types: 3.0.1 @@ -368,12 +556,16 @@ snapshots: cookie@0.7.2: {} + data-uri-to-buffer@4.0.1: {} + debug@4.4.1: dependencies: ms: 2.1.3 depd@2.0.0: {} + detect-libc@2.0.2: {} + dotenv@16.5.0: {} dunder-proto@1.0.1: @@ -430,6 +622,11 @@ snapshots: transitivePeerDependencies: - supports-color + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + finalhandler@2.1.0: dependencies: debug: 4.4.1 @@ -441,6 +638,10 @@ snapshots: transitivePeerDependencies: - supports-color + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + forwarded@0.2.0: {} fresh@2.0.0: {} @@ -491,6 +692,23 @@ snapshots: is-promise@4.0.0: {} + js-base64@3.7.7: {} + + libsql@0.5.13: + dependencies: + '@neon-rs/load': 0.0.4 + detect-libc: 2.0.2 + optionalDependencies: + '@libsql/darwin-arm64': 0.5.13 + '@libsql/darwin-x64': 0.5.13 + '@libsql/linux-arm-gnueabihf': 0.5.13 + '@libsql/linux-arm-musleabihf': 0.5.13 + '@libsql/linux-arm64-gnu': 0.5.13 + '@libsql/linux-arm64-musl': 0.5.13 + '@libsql/linux-x64-gnu': 0.5.13 + '@libsql/linux-x64-musl': 0.5.13 + '@libsql/win32-x64-msvc': 0.5.13 + math-intrinsics@1.1.0: {} media-typer@1.1.0: {} @@ -507,6 +725,14 @@ snapshots: negotiator@1.0.0: {} + node-domexception@1.0.0: {} + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + node-gyp-build@4.8.4: {} object-inspect@1.13.4: {} @@ -523,6 +749,8 @@ snapshots: path-to-regexp@8.2.0: {} + promise-limit@2.7.0: {} + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -624,12 +852,16 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.1 + undici-types@7.8.0: {} + unpipe@1.0.0: {} uuid@11.1.0: {} vary@1.1.2: {} + web-streams-polyfill@3.3.3: {} + wrappy@1.0.2: {} ws@8.18.2: {} diff --git a/server.js b/server.js index 8571b2d..46c78a5 100644 --- a/server.js +++ b/server.js @@ -31,130 +31,129 @@ wss.on("connection", (ws, req) => { connections.delete(userId); console.log(`User ${userId} disconnected`); }); + + ws.on("error", (error) => { + console.error(`WebSocket error for user ${userId}:`, error); + connections.delete(userId); + }); } }); -// Broadcast to all connected clients +// Helper function to broadcast to all connected users function broadcast(message) { const messageStr = JSON.stringify(message); - connections.forEach((ws) => { + connections.forEach((ws, userId) => { if (ws.readyState === WebSocket.OPEN) { ws.send(messageStr); } }); } -// Broadcast to specific user +// Helper function to broadcast to a specific user function broadcastToUser(userId, message) { const ws = connections.get(userId); - console.log( - `broadcastToUser called for ${userId}, connection exists: ${!!ws}, readyState: ${ws ? ws.readyState : "N/A"}`, - ); if (ws && ws.readyState === WebSocket.OPEN) { - console.log( - `✓ Sending WebSocket message to ${userId}:`, - message.type, - JSON.stringify(message.data), - ); ws.send(JSON.stringify(message)); - } else { - console.log( - `✗ WebSocket not available for user ${userId} - connection: ${!!ws}, readyState: ${ws ? ws.readyState : "N/A"}`, - ); } } app.use(express.json()); app.use(express.static("public")); -// Create user -app.post("/api/users", (req, res) => { - try { - const { userId } = req.body; - const user = todoService.createUser(userId); - broadcast({ type: "users", data: todoService.getAllUsers() }); - res.json(user); - } catch (error) { - res.status(400).json({ error: error.message }); - } +// Middleware to handle async errors +const asyncHandler = (fn) => (req, res, next) => { + Promise.resolve(fn(req, res, next)).catch(next); +}; + +// Error handling middleware +app.use((err, req, res, next) => { + console.error("Error:", err); + res.status(500).json({ error: err.message || "Internal server error" }); }); +// Create a new user +app.post( + "/api/users", + asyncHandler(async (req, res) => { + const { userId } = req.body; + const user = await todoService.createUser(userId); + const users = await todoService.getAllUsers(); + broadcast({ type: "users", data: users }); + res.json(user); + }), +); + // Get all users -app.get("/api/users", (req, res) => { - try { - const users = todoService.getAllUsers(); +app.get( + "/api/users", + asyncHandler(async (req, res) => { + const users = await todoService.getAllUsers(); res.json(users); - } catch (error) { - res.status(400).json({ error: error.message }); - } -}); + }), +); -// Add todo -app.post("/api/users/:userId/todos", (req, res) => { - try { +// Add a new todo +app.post( + "/api/users/:userId/todos", + asyncHandler(async (req, res) => { const { userId } = req.params; const { text } = req.body; - const todoGroupId = todoService.addTodo(userId, text); - const todo = todoService.getTodo(userId, todoGroupId); + const todoGroupId = await todoService.addTodo(userId, text); + const todo = await todoService.getTodo(userId, todoGroupId); broadcastToUser(userId, { type: "todo_added", data: todo }); res.json({ id: todoGroupId }); - } catch (error) { - res.status(400).json({ error: error.message }); - } -}); + }), +); -// Get todos -app.get("/api/users/:userId/todos", (req, res) => { - try { +// Get todos for a user +app.get( + "/api/users/:userId/todos", + asyncHandler(async (req, res) => { const { userId } = req.params; - const todos = todoService.getTodos(userId); + const todos = await todoService.getTodos(userId); res.json(todos); - } catch (error) { - res.status(400).json({ error: error.message }); - } -}); + }), +); -// Get encrypted todos -app.get("/api/users/:userId/todos/encrypted", (req, res) => { - try { +// Get encrypted todos (for debugging) +app.get( + "/api/users/:userId/todos/encrypted", + asyncHandler(async (req, res) => { const { userId } = req.params; - const todos = todoService.getTodos(userId); - const encryptedTodos = todos.map((todo) => { - const row = todoService.db - .prepare( - "SELECT encrypted, createdAt FROM todos WHERE todoGroupId = ? AND userId = ?", - ) - .get(todo.id, userId); + const todos = await todoService.getTodos(userId); + const encryptedTodos = []; - if (!row) { + for (const todo of todos) { + const row = await todoService.db.execute( + "SELECT encrypted, createdAt FROM todos WHERE todoGroupId = ? AND userId = ?", + [todo.id, userId], + ); + + if (row.rows.length === 0) { console.warn( `No encrypted data found for todo ${todo.id} and user ${userId}`, ); - return { - id: todo.id, - encrypted: { body: "Encryption data not available" }, - createdAt: todo.createdAt, - }; + continue; } - return { + encryptedTodos.push({ id: todo.id, - encrypted: JSON.parse(row.encrypted), - createdAt: row.createdAt, - }; - }); - res.json(encryptedTodos); - } catch (error) { - console.error("Error in encrypted todos endpoint:", error); - res.status(400).json({ error: error.message }); - } -}); + encrypted: row.rows[0].encrypted, + createdAt: row.rows[0].createdAt, + participants: todo.participants, + }); + } -// Delete todo -app.delete("/api/users/:userId/todos/:todoGroupId", (req, res) => { - try { + res.json(encryptedTodos); + }), +); + +// Delete a todo +app.delete( + "/api/users/:userId/todos/:todoGroupId", + asyncHandler(async (req, res) => { const { userId, todoGroupId } = req.params; - const { deletedIds, affectedUsers } = todoService.deleteTodo( + const { deletedIds, affectedUsers } = await todoService.deleteTodo( userId, todoGroupId, ); @@ -163,9 +162,6 @@ app.delete("/api/users/:userId/todos/:todoGroupId", (req, res) => { console.log(`Broadcasting deletion to users: ${affectedUsers.join(", ")}`); affectedUsers.forEach((affectedUserId) => { deletedIds.forEach((deletedId) => { - console.log( - `Sending todo_deleted event for todo ${deletedId} to user ${affectedUserId}`, - ); broadcastToUser(affectedUserId, { type: "todo_deleted", data: { id: deletedId, deletedBy: userId }, @@ -173,43 +169,41 @@ app.delete("/api/users/:userId/todos/:todoGroupId", (req, res) => { }); }); - res.json({ success: true }); - } catch (error) { - res.status(400).json({ error: error.message }); - } -}); + res.json({ deletedIds, affectedUsers }); + }), +); -// Share todo -app.post("/api/users/:userId/todos/:todoGroupId/share", (req, res) => { - try { +// Share a todo with another user +app.post( + "/api/users/:userId/todos/:todoGroupId/share", + asyncHandler(async (req, res) => { const { userId, todoGroupId } = req.params; const { recipientId } = req.body; if (!recipientId) { return res.status(400).json({ error: "Recipient ID is required" }); } - const sharedTodoGroupId = todoService.shareTodo( + const sharedTodoGroupId = await todoService.shareTodo( userId, todoGroupId, recipientId, ); - const sharedTodo = todoService.getTodo(recipientId, sharedTodoGroupId); + const sharedTodo = await todoService.getTodo( + recipientId, + sharedTodoGroupId, + ); broadcastToUser(recipientId, { type: "todo_shared", data: sharedTodo }); - // Also notify the original user that the todo was shared - broadcastToUser(userId, { - type: "todo_share_confirmed", - data: { todoGroupId, sharedWith: recipientId }, + res.json({ + sharedTodoGroupId, + message: `Todo shared with ${recipientId}`, }); - - res.json({ success: true }); - } catch (error) { - res.status(400).json({ error: error.message }); - } -}); + }), +); // Update todo completion status -app.patch("/api/users/:userId/todos/:todoGroupId", (req, res) => { - try { +app.patch( + "/api/users/:userId/todos/:todoGroupId", + asyncHandler(async (req, res) => { const { userId, todoGroupId } = req.params; const { completed } = req.body; @@ -217,41 +211,38 @@ app.patch("/api/users/:userId/todos/:todoGroupId", (req, res) => { `PATCH request: user ${userId} updating todo ${todoGroupId} to completed=${completed}`, ); - const affectedUsers = todoService.updateTodoStatus( + const affectedUsers = await todoService.updateTodoStatus( userId, todoGroupId, completed, ); - console.log(`updateTodoStatus returned affected users:`, affectedUsers); + console.log(`Broadcasting update to users: ${affectedUsers.join(", ")}`); - // Broadcast update to all affected users - console.log( - `Broadcasting todo update to users: ${affectedUsers.join(", ")}`, - ); - affectedUsers.forEach((affectedUserId) => { - console.log( - `Sending todo_updated event for todo ${todoGroupId} to user ${affectedUserId}`, - ); - const message = { - type: "todo_updated", - data: { id: todoGroupId, completed, updatedBy: userId }, - }; - console.log( - `Message being sent to ${affectedUserId}:`, - JSON.stringify(message), - ); - broadcastToUser(affectedUserId, message); - }); + // Broadcast the update to all affected users + for (const affectedUserId of affectedUsers) { + try { + const todo = await todoService.getTodo(affectedUserId, todoGroupId); + const message = { + type: "todo_updated", + data: { id: todoGroupId, completed, updatedBy: userId }, + }; + broadcastToUser(affectedUserId, message); + console.log(`Sent update to user ${affectedUserId}:`, message); + } catch (error) { + console.error( + `Error getting todo for user ${affectedUserId}:`, + error.message, + ); + } + } - res.json({ success: true }); - } catch (error) { - console.error(`Error updating todo status:`, error); - res.status(400).json({ error: error.message }); - } -}); + res.json({ message: "Todo updated successfully", affectedUsers }); + }), +); + +const PORT = process.env.APP_PORT || 3000; -const PORT = 3000; server.listen(PORT, () => { - console.log(`Server running on port 3000`); + console.log(`Server running on port ${PORT}`); }); diff --git a/todo-service.js b/todo-service.js index 5b45f3b..fe33872 100644 --- a/todo-service.js +++ b/todo-service.js @@ -1,18 +1,28 @@ -import { DatabaseSync } from "node:sqlite"; +import { createClient } from "@libsql/client"; import "dotenv/config"; import { SignalClient } from "./signal-crypto.js"; import { v4 as uuidv4 } from "uuid"; export class EncryptedTodoService { constructor() { - this.db = new DatabaseSync(process.env.SQLITE_DB_PATH); + this.db = createClient({ + url: `file:${process.env.SQLITE_DB_PATH}`, + }); this.users = new Map(); + // Initialize database and migrate + this.initializeDatabase(); + } + + async initializeDatabase() { // Create tables with new schema - this.db.exec(` + await this.db.execute(` CREATE TABLE IF NOT EXISTS users ( userId TEXT PRIMARY KEY ); + `); + + await this.db.execute(` CREATE TABLE IF NOT EXISTS todos ( id TEXT PRIMARY KEY, todoGroupId TEXT, @@ -25,12 +35,18 @@ export class EncryptedTodoService { originalTodoId TEXT, FOREIGN KEY(userId) REFERENCES users(userId) ); + `); + + await this.db.execute(` CREATE TABLE IF NOT EXISTS todo_groups ( todoGroupId TEXT PRIMARY KEY, createdBy TEXT, createdAt TEXT, FOREIGN KEY(createdBy) REFERENCES users(userId) ); + `); + + await this.db.execute(` CREATE TABLE IF NOT EXISTS todo_participants ( todoGroupId TEXT, userId TEXT, @@ -39,6 +55,9 @@ export class EncryptedTodoService { FOREIGN KEY(todoGroupId) REFERENCES todo_groups(todoGroupId), FOREIGN KEY(userId) REFERENCES users(userId) ); + `); + + await this.db.execute(` CREATE TABLE IF NOT EXISTS todo_shares ( todoId TEXT, sharedWith TEXT, @@ -48,84 +67,90 @@ export class EncryptedTodoService { `); // Migrate existing data to new schema - this.migrateToNewSchema(); + await this.migrateToNewSchema(); // Load existing users from database on startup - this.loadExistingUsers(); + await this.loadExistingUsers(); } - migrateToNewSchema() { + async migrateToNewSchema() { // Check if we need to migrate by looking for todos without todoGroupId - const oldTodos = this.db - .prepare("SELECT * FROM todos WHERE todoGroupId IS NULL") - .all(); + const oldTodos = await this.db.execute( + "SELECT * FROM todos WHERE todoGroupId IS NULL", + ); - if (oldTodos.length > 0) { - console.log(`Migrating ${oldTodos.length} todos to new schema...`); + if (oldTodos.rows.length > 0) { + console.log(`Migrating ${oldTodos.rows.length} todos to new schema...`); - for (const todo of oldTodos) { + for (const todo of oldTodos.rows) { const todoGroupId = uuidv4(); const now = new Date().toISOString(); // If this is an original todo (not shared) if (!todo.sharedBy && !todo.originalTodoId) { // Create todo group - this.db - .prepare( - "INSERT OR IGNORE INTO todo_groups (todoGroupId, createdBy, createdAt) VALUES (?, ?, ?)", - ) - .run(todoGroupId, todo.userId, todo.createdAt || now); + await this.db.execute( + "INSERT OR IGNORE INTO todo_groups (todoGroupId, createdBy, createdAt) VALUES (?, ?, ?)", + [todoGroupId, todo.userId, todo.createdAt || now], + ); // Add creator as participant - this.db - .prepare( - "INSERT OR IGNORE INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)", - ) - .run(todoGroupId, todo.userId, todo.createdAt || now); + await this.db.execute( + "INSERT OR IGNORE INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)", + [todoGroupId, todo.userId, todo.createdAt || now], + ); // Update the todo with todoGroupId - this.db - .prepare("UPDATE todos SET todoGroupId = ? WHERE id = ?") - .run(todoGroupId, todo.id); + await this.db.execute( + "UPDATE todos SET todoGroupId = ? WHERE id = ?", + [todoGroupId, todo.id], + ); // Find and migrate shared copies - const sharedCopies = this.db - .prepare("SELECT * FROM todos WHERE originalTodoId = ?") - .all(todo.id); - for (const sharedTodo of sharedCopies) { + const sharedCopies = await this.db.execute( + "SELECT * FROM todos WHERE originalTodoId = ?", + [todo.id], + ); + + for (const sharedTodo of sharedCopies.rows) { // Add shared user as participant - this.db - .prepare( - "INSERT OR IGNORE INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)", - ) - .run(todoGroupId, sharedTodo.userId, sharedTodo.createdAt || now); + await this.db.execute( + "INSERT OR IGNORE INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)", + [todoGroupId, sharedTodo.userId, sharedTodo.createdAt || now], + ); // Update shared todo with same todoGroupId - this.db - .prepare("UPDATE todos SET todoGroupId = ? WHERE id = ?") - .run(todoGroupId, sharedTodo.id); + await this.db.execute( + "UPDATE todos SET todoGroupId = ? WHERE id = ?", + [todoGroupId, sharedTodo.id], + ); } } // If this is a shared todo without a group (shouldn't happen after above, but just in case) else if (todo.originalTodoId && !todo.todoGroupId) { - const originalTodo = this.db - .prepare("SELECT todoGroupId FROM todos WHERE id = ?") - .get(todo.originalTodoId); - if (originalTodo && originalTodo.todoGroupId) { - this.db - .prepare("UPDATE todos SET todoGroupId = ? WHERE id = ?") - .run(originalTodo.todoGroupId, todo.id); + const originalTodo = await this.db.execute( + "SELECT todoGroupId FROM todos WHERE id = ?", + [todo.originalTodoId], + ); + + if ( + originalTodo.rows.length > 0 && + originalTodo.rows[0].todoGroupId + ) { + await this.db.execute( + "UPDATE todos SET todoGroupId = ? WHERE id = ?", + [originalTodo.rows[0].todoGroupId, todo.id], + ); // Add as participant if not already - this.db - .prepare( - "INSERT OR IGNORE INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)", - ) - .run( - originalTodo.todoGroupId, + await this.db.execute( + "INSERT OR IGNORE INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)", + [ + originalTodo.rows[0].todoGroupId, todo.userId, todo.createdAt || now, - ); + ], + ); } } } @@ -134,9 +159,9 @@ export class EncryptedTodoService { } } - loadExistingUsers() { - const existingUsers = this.db.prepare("SELECT userId FROM users").all(); - for (const user of existingUsers) { + async loadExistingUsers() { + const existingUsers = await this.db.execute("SELECT userId FROM users"); + for (const user of existingUsers.rows) { if (!this.users.has(user.userId)) { this.users.set(user.userId, SignalClient.create(user.userId)); console.log(`Loaded existing user: ${user.userId}`); @@ -144,24 +169,22 @@ export class EncryptedTodoService { } } - createUser(userId) { - this.db - .prepare("INSERT OR IGNORE INTO users (userId) VALUES (?)") - .run(userId); + async createUser(userId) { + await this.db.execute("INSERT OR IGNORE INTO users (userId) VALUES (?)", [ + userId, + ]); if (!this.users.has(userId)) { this.users.set(userId, SignalClient.create(userId)); } return { userId }; } - getAllUsers() { - return this.db - .prepare("SELECT userId FROM users") - .all() - .map((u) => u.userId); + async getAllUsers() { + const result = await this.db.execute("SELECT userId FROM users"); + return result.rows.map((u) => u.userId); } - addTodo(userId, todoText) { + async addTodo(userId, todoText) { const todoGroupId = uuidv4(); const client = this.users.get(userId); const todoId = uuidv4(); @@ -169,25 +192,21 @@ export class EncryptedTodoService { const now = new Date().toISOString(); // Create the todo group - this.db - .prepare( - "INSERT INTO todo_groups (todoGroupId, createdBy, createdAt) VALUES (?, ?, ?)", - ) - .run(todoGroupId, userId, now); + await this.db.execute( + "INSERT INTO todo_groups (todoGroupId, createdBy, createdAt) VALUES (?, ?, ?)", + [todoGroupId, userId, now], + ); // Add the creator as a participant - this.db - .prepare( - "INSERT INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)", - ) - .run(todoGroupId, userId, now); + await this.db.execute( + "INSERT INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)", + [todoGroupId, userId, now], + ); // Create the todo entry for the creator - this.db - .prepare( - "INSERT INTO todos (id, todoGroupId, userId, encrypted, createdAt, originalText, completed) VALUES (?, ?, ?, ?, ?, ?, ?)", - ) - .run( + await this.db.execute( + "INSERT INTO todos (id, todoGroupId, userId, encrypted, createdAt, originalText, completed) VALUES (?, ?, ?, ?, ?, ?, ?)", + [ todoId, todoGroupId, userId, @@ -195,24 +214,25 @@ export class EncryptedTodoService { now, todoText, 0, - ); + ], + ); return todoGroupId; } - getTodos(userId) { + async getTodos(userId) { const client = this.users.get(userId); if (!client) { console.error(`No client found for user ${userId}`); return []; } - const rows = this.db - .prepare("SELECT * FROM todos WHERE userId = ?") - .all(userId); + const rows = await this.db.execute("SELECT * FROM todos WHERE userId = ?", [ + userId, + ]); const todos = []; - for (const row of rows) { + for (const row of rows.rows) { try { // Skip todos without todoGroupId (migration didn't work) if (!row.todoGroupId) { @@ -223,24 +243,28 @@ export class EncryptedTodoService { const decryptedText = client.decrypt(JSON.parse(row.encrypted)); // Get all participants in this todo group - const participants = this.db - .prepare("SELECT userId FROM todo_participants WHERE todoGroupId = ?") - .all(row.todoGroupId) - .map((p) => p.userId); + const participants = await this.db.execute( + "SELECT userId FROM todo_participants WHERE todoGroupId = ?", + [row.todoGroupId], + ); // Get group creator - const groupInfo = this.db - .prepare("SELECT createdBy FROM todo_groups WHERE todoGroupId = ?") - .get(row.todoGroupId); + const groupInfo = await this.db.execute( + "SELECT createdBy FROM todo_groups WHERE todoGroupId = ?", + [row.todoGroupId], + ); todos.push({ id: row.todoGroupId, // Use todoGroupId as the primary identifier text: decryptedText, createdAt: row.createdAt, completed: !!row.completed, - participants: participants.length > 0 ? participants : [userId], // Fallback to current user - createdBy: groupInfo?.createdBy || userId, // Fallback to current user - isCreator: (groupInfo?.createdBy || userId) === userId, + participants: + participants.rows.length > 0 + ? participants.rows.map((p) => p.userId) + : [userId], // Fallback to current user + createdBy: groupInfo.rows[0]?.createdBy || userId, // Fallback to current user + isCreator: (groupInfo.rows[0]?.createdBy || userId) === userId, }); } catch (e) { console.error(`Error processing todo ${row.id}:`, e); @@ -249,183 +273,197 @@ export class EncryptedTodoService { return todos.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); } - getTodo(userId, todoGroupId) { + async getTodo(userId, todoGroupId) { const client = this.users.get(userId); - const row = this.db - .prepare("SELECT * FROM todos WHERE todoGroupId = ? AND userId = ?") - .get(todoGroupId, userId); - if (!row) throw new Error("Todo not found"); + const row = await this.db.execute( + "SELECT * FROM todos WHERE todoGroupId = ? AND userId = ?", + [todoGroupId, userId], + ); - const decryptedText = client.decrypt(JSON.parse(row.encrypted)); + if (row.rows.length === 0) throw new Error("Todo not found"); + + const todoRow = row.rows[0]; + const decryptedText = client.decrypt(JSON.parse(todoRow.encrypted)); // Get all participants in this todo group - const participants = this.db - .prepare("SELECT userId FROM todo_participants WHERE todoGroupId = ?") - .all(todoGroupId) - .map((p) => p.userId); + const participants = await this.db.execute( + "SELECT userId FROM todo_participants WHERE todoGroupId = ?", + [todoGroupId], + ); // Get group creator - const groupInfo = this.db - .prepare("SELECT createdBy FROM todo_groups WHERE todoGroupId = ?") - .get(todoGroupId); + const groupInfo = await this.db.execute( + "SELECT createdBy FROM todo_groups WHERE todoGroupId = ?", + [todoGroupId], + ); return { id: todoGroupId, text: decryptedText, - createdAt: row.createdAt, - completed: !!row.completed, - participants: participants, - createdBy: groupInfo?.createdBy, - isCreator: groupInfo?.createdBy === userId, + createdAt: todoRow.createdAt, + completed: !!todoRow.completed, + participants: participants.rows.map((p) => p.userId), + createdBy: groupInfo.rows[0]?.createdBy, + isCreator: groupInfo.rows[0]?.createdBy === userId, }; } - updateTodoStatus(userId, todoGroupId, completed) { + async updateTodoStatus(userId, todoGroupId, completed) { console.log( `updateTodoStatus called: userId=${userId}, todoGroupId=${todoGroupId}, completed=${completed}`, ); // Get all participants in this todo group - const participants = this.db - .prepare("SELECT userId FROM todo_participants WHERE todoGroupId = ?") - .all(todoGroupId) - .map((p) => p.userId); + const participants = await this.db.execute( + "SELECT userId FROM todo_participants WHERE todoGroupId = ?", + [todoGroupId], + ); - if (!participants.includes(userId)) { + const participantIds = participants.rows.map((p) => p.userId); + + if (!participantIds.includes(userId)) { throw new Error("User is not a participant in this todo group"); } - console.log(`Updating todo for all participants:`, participants); + console.log(`Updating todo for all participants:`, participantIds); // Update todos for all participants - for (const participantId of participants) { - this.db - .prepare( - "UPDATE todos SET completed = ? WHERE todoGroupId = ? AND userId = ?", - ) - .run(completed ? 1 : 0, todoGroupId, participantId); + for (const participantId of participantIds) { + await this.db.execute( + "UPDATE todos SET completed = ? WHERE todoGroupId = ? AND userId = ?", + [completed ? 1 : 0, todoGroupId, participantId], + ); } - console.log(`Affected users for todo update:`, participants); - return participants; + console.log(`Affected users for todo update:`, participantIds); + return participantIds; } - deleteTodo(userId, todoGroupId) { + async deleteTodo(userId, todoGroupId) { // Get all participants in this todo group - const participants = this.db - .prepare("SELECT userId FROM todo_participants WHERE todoGroupId = ?") - .all(todoGroupId) - .map((p) => p.userId); + const participants = await this.db.execute( + "SELECT userId FROM todo_participants WHERE todoGroupId = ?", + [todoGroupId], + ); - if (!participants.includes(userId)) { + const participantIds = participants.rows.map((p) => p.userId); + + if (!participantIds.includes(userId)) { throw new Error("User is not a participant in this todo group"); } // Check if user is the creator of this todo group - const groupInfo = this.db - .prepare("SELECT createdBy FROM todo_groups WHERE todoGroupId = ?") - .get(todoGroupId); + const groupInfo = await this.db.execute( + "SELECT createdBy FROM todo_groups WHERE todoGroupId = ?", + [todoGroupId], + ); - if (!groupInfo) { + if (groupInfo.rows.length === 0) { throw new Error("Todo group not found"); } - if (groupInfo.createdBy !== userId) { + if (groupInfo.rows[0].createdBy !== userId) { throw new Error("Only the creator can delete this todo"); } console.log( `Deleting todo group ${todoGroupId} for all participants:`, - participants, + participantIds, ); // Delete all todo entries for this group - this.db.prepare("DELETE FROM todos WHERE todoGroupId = ?").run(todoGroupId); + await this.db.execute("DELETE FROM todos WHERE todoGroupId = ?", [ + todoGroupId, + ]); // Delete all participants - this.db - .prepare("DELETE FROM todo_participants WHERE todoGroupId = ?") - .run(todoGroupId); + await this.db.execute( + "DELETE FROM todo_participants WHERE todoGroupId = ?", + [todoGroupId], + ); // Delete the group itself - this.db - .prepare("DELETE FROM todo_groups WHERE todoGroupId = ?") - .run(todoGroupId); + await this.db.execute("DELETE FROM todo_groups WHERE todoGroupId = ?", [ + todoGroupId, + ]); return { deletedIds: [todoGroupId], - affectedUsers: participants, + affectedUsers: participantIds, }; } - shareTodo(userId, todoGroupId, recipientId) { + async shareTodo(userId, todoGroupId, recipientId) { // Ensure recipient user exists if (!this.users.has(recipientId)) { this.users.set(recipientId, SignalClient.create(recipientId)); } // Check if user is a participant in this todo group - const participants = this.db - .prepare("SELECT userId FROM todo_participants WHERE todoGroupId = ?") - .all(todoGroupId) - .map((p) => p.userId); + const participants = await this.db.execute( + "SELECT userId FROM todo_participants WHERE todoGroupId = ?", + [todoGroupId], + ); - if (!participants.includes(userId)) { + const participantIds = participants.rows.map((p) => p.userId); + + if (!participantIds.includes(userId)) { throw new Error("User is not a participant in this todo group"); } // Check if user is the creator of this todo group - const groupInfo = this.db - .prepare("SELECT createdBy FROM todo_groups WHERE todoGroupId = ?") - .get(todoGroupId); + const groupInfo = await this.db.execute( + "SELECT createdBy FROM todo_groups WHERE todoGroupId = ?", + [todoGroupId], + ); - if (!groupInfo) { + if (groupInfo.rows.length === 0) { throw new Error("Todo group not found"); } - if (groupInfo.createdBy !== userId) { + if (groupInfo.rows[0].createdBy !== userId) { throw new Error("Only the creator can add participants to this todo"); } - if (participants.includes(recipientId)) { + if (participantIds.includes(recipientId)) { throw new Error("User is already a participant in this todo group"); } // Get the original text from the sender's todo - const senderTodo = this.db - .prepare("SELECT * FROM todos WHERE todoGroupId = ? AND userId = ?") - .get(todoGroupId, userId); + const senderTodo = await this.db.execute( + "SELECT * FROM todos WHERE todoGroupId = ? AND userId = ?", + [todoGroupId, userId], + ); - if (!senderTodo) throw new Error("Todo not found"); + if (senderTodo.rows.length === 0) throw new Error("Todo not found"); + const senderTodoRow = senderTodo.rows[0]; const recipientClient = this.users.get(recipientId); const encryptedForRecipient = recipientClient.encrypt( - senderTodo.originalText, + senderTodoRow.originalText, ); const recipientTodoId = uuidv4(); const now = new Date().toISOString(); // Add recipient as a participant - this.db - .prepare( - "INSERT INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)", - ) - .run(todoGroupId, recipientId, now); + await this.db.execute( + "INSERT INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)", + [todoGroupId, recipientId, now], + ); // Create todo entry for the recipient - this.db - .prepare( - "INSERT INTO todos (id, todoGroupId, userId, encrypted, createdAt, originalText, completed) VALUES (?, ?, ?, ?, ?, ?, ?)", - ) - .run( + await this.db.execute( + "INSERT INTO todos (id, todoGroupId, userId, encrypted, createdAt, originalText, completed) VALUES (?, ?, ?, ?, ?, ?, ?)", + [ recipientTodoId, todoGroupId, recipientId, JSON.stringify(encryptedForRecipient), now, - senderTodo.originalText, - senderTodo.completed, - ); + senderTodoRow.originalText, + senderTodoRow.completed, + ], + ); return todoGroupId; }