From 95d91ac616a02708f6dac8c1db13dd2c19711214 Mon Sep 17 00:00:00 2001 From: Max Stevens <38756142+markusgeert@users.noreply.github.com> Date: Wed, 9 Apr 2025 01:54:49 +0200 Subject: [PATCH] feat: upgrade to zero v0.18 (#52) --- package.json | 3 +- playground/src/db/schema.ts | 2 +- pnpm-lock.yaml | 302 ++++---- src/index.ts | 2 +- src/query.test.ts | 222 ++++++ src/query.ts | 55 +- src/view.test.ts | 1318 +++++++++++++++++++++++++++++++++++ src/view.ts | 44 +- test/index.test.ts | 12 +- 9 files changed, 1776 insertions(+), 184 deletions(-) create mode 100644 src/query.test.ts create mode 100644 src/view.test.ts diff --git a/package.json b/package.json index 4c1ad00..f0c4d86 100644 --- a/package.json +++ b/package.json @@ -35,10 +35,11 @@ "vue": "^3.5.13" }, "dependencies": { - "@rocicorp/zero": "^0.17.0" + "@rocicorp/zero": "^0.18.0" }, "devDependencies": { "@antfu/eslint-config": "latest", + "@rocicorp/resolver": "^1.0.2", "@vitest/coverage-v8": "latest", "bumpp": "latest", "changelogithub": "13.13.0", diff --git a/playground/src/db/schema.ts b/playground/src/db/schema.ts index 078ff26..9419533 100644 --- a/playground/src/db/schema.ts +++ b/playground/src/db/schema.ts @@ -55,7 +55,7 @@ const messageRelationships = relationships(message, ({ one }) => ({ }), })) -export const schema = createSchema(1, { +export const schema = createSchema({ tables: [user, medium, message], relationships: [messageRelationships], }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bb47f45..23102b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,12 +12,15 @@ importers: .: dependencies: '@rocicorp/zero': - specifier: ^0.17.0 - version: 0.17.2025030701 + specifier: ^0.18.0 + version: 0.18.2025040300 devDependencies: '@antfu/eslint-config': specifier: latest version: 4.11.0(@typescript-eslint/utils@8.27.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.13.10)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) + '@rocicorp/resolver': + specifier: ^1.0.2 + version: 1.0.2 '@vitest/coverage-v8': specifier: latest version: 3.1.1(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.13.10)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) @@ -197,9 +200,19 @@ packages: '@databases/validate-unicode@1.0.0': resolution: {integrity: sha512-dLKqxGcymeVwEb/6c44KjOnzaAafFf0Wxa8xcfEjx/qOl3rdijsKYBAtIGhtVtOlpPf/PFKfgTuFurSPn/3B/g==} + '@dotenvx/dotenvx@1.39.0': + resolution: {integrity: sha512-qGfDpL/3S17MQYXpR3HkBS5xNQ7wiFlqLdpr+iIQzv17aMRcSlgL4EjMIsYFZ540Dq17J+y5FVElA1AkVoXiUA==} + hasBin: true + '@drdgvhbh/postgres-error-codes@0.0.6': resolution: {integrity: sha512-tAz0Xp+qhq90x0r/3VW96iRdHFw72cYQqXa65u0eFVhSMC27bc2gZ8Ky5WXEmshrl/bCe7QTYBNEF0U5zeSQjw==} + '@ecies/ciphers@0.2.3': + resolution: {integrity: sha512-tapn6XhOueMwht3E2UzY0ZZjYokdaw9XtL9kEyjhQ/Fb9vL9xTFbOaI+fV0AWvTpYu4BNloC6getKW6NtSg4mA==} + engines: {bun: '>=1', deno: '>=2', node: '>=16'} + peerDependencies: + '@noble/ciphers': ^1.0.0 + '@emnapi/core@1.3.1': resolution: {integrity: sha512-pVGjBIt1Y6gg3EJN8jTcfpP/+uuRksIo055oE/OBkDNcjZqVbfkWCksG1Jp4yZnj3iKWyWX8fdG/j6UDYPbFog==} @@ -667,6 +680,18 @@ packages: '@napi-rs/wasm-runtime@0.2.7': resolution: {integrity: sha512-5yximcFK5FNompXfJFoWanu5l8v1hNGqNHh9du1xETp9HWk/B/PzvchX55WYOPaIeNglG8++68AAiauBAtbnzw==} + '@noble/ciphers@1.2.1': + resolution: {integrity: sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.8.1': + resolution: {integrity: sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.7.1': + resolution: {integrity: sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==} + engines: {node: ^14.21.3 || >=16} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -948,8 +973,8 @@ packages: resolution: {integrity: sha512-bm+VUdF4CnKVjUj/dSCmVu0hjcyXaF/nKkw2rNhZUjGeBOMRy/hh8z/32h311es4dxCVvcZ3+QHQHMxF2YG5Kw==} hasBin: true - '@rocicorp/zero@0.17.2025030701': - resolution: {integrity: sha512-ywskUYhbmNZyBH8LkLrL9Zbz7g0zrCv62ZtyxH5ls5KWcMmUNRcw1nqfuqhzUfy1GXOYzEJQHVCc0hQMkvBGng==} + '@rocicorp/zero@0.18.2025040300': + resolution: {integrity: sha512-4RRHLdqhofF353gHqaQLffEdFPDHJldpdGfSdjXd+ncby6PPUXsF8eDTXAhzJbdwiSGq3jtXDgS9qrq1aKzLwQ==} engines: {node: '>=20'} hasBin: true @@ -1727,6 +1752,10 @@ packages: resolution: {integrity: sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==} engines: {node: '>=12.20.0'} + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + commander@13.1.0: resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} engines: {node: '>=18'} @@ -1931,6 +1960,10 @@ packages: easy-table@1.2.0: resolution: {integrity: sha512-OFzVOv03YpvtcWGe5AayU5G2hgybsg3iqA6drU8UaoZyB9jLGMTrz9+asnLp/E+6qPh88yEI1gvyZFZ41dmgww==} + eciesjs@0.4.14: + resolution: {integrity: sha512-eJAgf9pdv214Hn98FlUzclRMYWF7WfoLlkS9nWMTm1qcCwn6Ad4EGD9lr9HXMBfSrZhYQujRE+p0adPRkctC6A==} + engines: {bun: '>=1', deno: '>=2', node: '>=16'} + electron-to-chromium@1.5.113: resolution: {integrity: sha512-wjT2O4hX+wdWPJ76gWSkMhcHAV2PTMX+QetUCPYEdCIe+cxmgzzSSiGRCKW8nuh4mwKZlpv0xvoW7OF2X+wmHg==} @@ -2179,12 +2212,13 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - eventemitter2@6.4.9: - resolution: {integrity: sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==} - eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + execa@8.0.1: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} @@ -2326,6 +2360,10 @@ packages: resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} engines: {node: '>=18'} + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + get-stream@8.0.1: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} @@ -2400,6 +2438,10 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + human-signals@5.0.0: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} @@ -2513,6 +2555,10 @@ packages: is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + is-stream@3.0.0: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2532,6 +2578,10 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isexe@3.1.1: + resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} + engines: {node: '>=16'} + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -2854,6 +2904,10 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + mimic-fn@4.0.0: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} @@ -2991,6 +3045,10 @@ packages: resolution: {integrity: sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + npm-run-path@5.3.0: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -3012,6 +3070,10 @@ packages: engines: {node: ^14.16.0 || >=16.10.0} hasBin: true + object-treeify@1.1.33: + resolution: {integrity: sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==} + engines: {node: '>= 10'} + obliterator@2.0.5: resolution: {integrity: sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==} @@ -3031,6 +3093,10 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + onetime@6.0.0: resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} engines: {node: '>=12'} @@ -3123,48 +3189,10 @@ packages: perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} - pg-cloudflare@1.1.1: - resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==} - - pg-connection-string@2.7.0: - resolution: {integrity: sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==} - pg-format-fix@1.0.5: resolution: {integrity: sha512-HcXVy9Zk4kn87P0+U9XSxGtenNyknbPB87NreixSBk0lYJy89u+d/zQbS+f/aTTxABQ/B6FH1KdBB5EsGzRS2w==} engines: {node: '>=4.0'} - pg-int8@1.0.1: - resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} - engines: {node: '>=4.0.0'} - - pg-logical-replication@2.0.7: - resolution: {integrity: sha512-G6fyOYN9jIz4RdKC2t2NrYp5nw8aZ7cMfH0BOISYwl4M16DWg8+LYQZbY8LMyGurmms6+igALbsgpnCOS9a5Fw==} - engines: {node: '>= 14.0.0'} - - pg-pool@3.7.1: - resolution: {integrity: sha512-xIOsFoh7Vdhojas6q3596mXFsR8nwBQBXX5JiV7p9buEVAGqYL4yFzclON5P9vFrpu1u7Zwl2oriyDa89n0wbw==} - peerDependencies: - pg: '>=8.0' - - pg-protocol@1.7.1: - resolution: {integrity: sha512-gjTHWGYWsEgy9MsY0Gp6ZJxV24IjDqdpTW7Eh0x+WfJLFsm/TJx1MzL6T0D88mBvkpxotCQ6TwW6N+Kko7lhgQ==} - - pg-types@2.2.0: - resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} - engines: {node: '>=4'} - - pg@8.13.3: - resolution: {integrity: sha512-P6tPt9jXbL9HVu/SSRERNYaYG++MjnscnegFh9pPHihfoBSujsrka0hyuymMzeJKFWrcG8wvCKy8rCe8e5nDUQ==} - engines: {node: '>= 8.0.0'} - peerDependencies: - pg-native: '>=3.0.1' - peerDependenciesMeta: - pg-native: - optional: true - - pgpass@1.0.5: - resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3391,26 +3419,6 @@ packages: resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} engines: {node: ^10 || ^12 || >=14} - postgres-array@2.0.0: - resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} - engines: {node: '>=4'} - - postgres-array@3.0.4: - resolution: {integrity: sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==} - engines: {node: '>=12'} - - postgres-bytea@1.0.0: - resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} - engines: {node: '>=0.10.0'} - - postgres-date@1.0.7: - resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} - engines: {node: '>=0.10.0'} - - postgres-interval@1.2.0: - resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} - engines: {node: '>=0.10.0'} - postgres@3.4.5: resolution: {integrity: sha512-cDWgoah1Gez9rN3H4165peY9qfpEo+SA61oQv65O3cRUE1pOEoJWwddwcqKE8XZYjbblOJlYDlLV4h67HrEVDg==} engines: {node: '>=12'} @@ -3424,6 +3432,11 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier@3.5.3: + resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} + engines: {node: '>=14'} + hasBin: true + pretty-bytes@6.1.1: resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} engines: {node: ^14.13.1 || >=16.0.0} @@ -3617,6 +3630,9 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -3718,6 +3734,10 @@ packages: resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} engines: {node: '>=8'} + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + strip-final-newline@3.0.0: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} @@ -4037,6 +4057,11 @@ packages: engines: {node: '>= 8'} hasBin: true + which@4.0.0: + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} @@ -4081,10 +4106,6 @@ packages: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} - xtend@4.0.2: - resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} - engines: {node: '>=0.4'} - y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -4230,8 +4251,24 @@ snapshots: '@databases/validate-unicode@1.0.0': {} + '@dotenvx/dotenvx@1.39.0': + dependencies: + commander: 11.1.0 + dotenv: 16.4.7 + eciesjs: 0.4.14 + execa: 5.1.1 + fdir: 6.4.3(picomatch@4.0.2) + ignore: 5.3.2 + object-treeify: 1.1.33 + picomatch: 4.0.2 + which: 4.0.0 + '@drdgvhbh/postgres-error-codes@0.0.6': {} + '@ecies/ciphers@0.2.3(@noble/ciphers@1.2.1)': + dependencies: + '@noble/ciphers': 1.2.1 + '@emnapi/core@1.3.1': dependencies: '@emnapi/wasi-threads': 1.0.1 @@ -4582,6 +4619,14 @@ snapshots: '@tybys/wasm-util': 0.9.0 optional: true + '@noble/ciphers@1.2.1': {} + + '@noble/curves@1.8.1': + dependencies: + '@noble/hashes': 1.7.1 + + '@noble/hashes@1.7.1': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -4899,11 +4944,12 @@ snapshots: bindings: 1.5.0 prebuild-install: 7.1.3 - '@rocicorp/zero@0.17.2025030701': + '@rocicorp/zero@0.18.2025040300': dependencies: '@badrap/valita': 0.3.11 '@databases/escape-identifier': 1.0.3 '@databases/sql': 3.3.0 + '@dotenvx/dotenvx': 1.39.0 '@drdgvhbh/postgres-error-codes': 0.0.6 '@fastify/cors': 10.1.0 '@fastify/websocket': 11.0.2 @@ -4924,7 +4970,6 @@ snapshots: command-line-usage: 7.0.3 compare-utf8: 0.1.1 defu: 6.1.4 - dotenv: 16.4.7 eventemitter3: 5.0.1 fastify: 5.2.1 jose: 5.10.0 @@ -4932,12 +4977,9 @@ snapshots: json-custom-numbers: 3.1.1 kasi: 1.1.1 nanoid: 5.1.3 - pg: 8.13.3 pg-format: pg-format-fix@1.0.5 - pg-logical-replication: 2.0.7 - pg-protocol: 1.7.1 postgres: 3.4.5 - postgres-array: 3.0.4 + prettier: 3.5.3 semver: 7.7.1 tsx: 4.19.3 url-pattern: 1.0.3 @@ -4945,7 +4987,6 @@ snapshots: transitivePeerDependencies: - '@75lb/nature' - bufferutil - - pg-native - supports-color - utf-8-validate @@ -5796,6 +5837,8 @@ snapshots: table-layout: 4.1.1 typical: 7.3.0 + commander@11.1.0: {} + commander@13.1.0: {} commander@4.1.1: {} @@ -5997,6 +6040,13 @@ snapshots: optionalDependencies: wcwidth: 1.0.1 + eciesjs@0.4.14: + dependencies: + '@ecies/ciphers': 0.2.3(@noble/ciphers@1.2.1) + '@noble/ciphers': 1.2.1 + '@noble/curves': 1.8.1 + '@noble/hashes': 1.7.1 + electron-to-chromium@1.5.113: {} emoji-regex@10.4.0: {} @@ -6376,10 +6426,20 @@ snapshots: esutils@2.0.3: {} - eventemitter2@6.4.9: {} - eventemitter3@5.0.1: {} + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + execa@8.0.1: dependencies: cross-spawn: 7.0.6 @@ -6538,6 +6598,8 @@ snapshots: get-east-asian-width@1.3.0: {} + get-stream@6.0.1: {} + get-stream@8.0.1: {} get-stream@9.0.1: @@ -6613,6 +6675,8 @@ snapshots: html-escaper@2.0.2: {} + human-signals@2.1.0: {} + human-signals@5.0.0: {} human-signals@8.0.0: {} @@ -6706,6 +6770,8 @@ snapshots: dependencies: '@types/estree': 1.0.6 + is-stream@2.0.1: {} + is-stream@3.0.0: {} is-stream@4.0.1: {} @@ -6718,6 +6784,8 @@ snapshots: isexe@2.0.0: {} + isexe@3.1.1: {} + istanbul-lib-coverage@3.2.2: {} istanbul-lib-report@3.0.1: @@ -7227,6 +7295,8 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mimic-fn@2.1.0: {} + mimic-fn@4.0.0: {} mimic-function@5.0.1: {} @@ -7335,6 +7405,10 @@ snapshots: npm-normalize-package-bin@3.0.1: {} + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + npm-run-path@5.3.0: dependencies: path-key: 4.0.0 @@ -7365,6 +7439,8 @@ snapshots: pkg-types: 2.1.0 tinyexec: 0.3.2 + object-treeify@1.1.33: {} + obliterator@2.0.5: {} ofetch@1.4.1: @@ -7383,6 +7459,10 @@ snapshots: dependencies: wrappy: 1.0.2 + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + onetime@6.0.0: dependencies: mimic-fn: 4.0.0 @@ -7469,50 +7549,8 @@ snapshots: perfect-debounce@1.0.0: {} - pg-cloudflare@1.1.1: - optional: true - - pg-connection-string@2.7.0: {} - pg-format-fix@1.0.5: {} - pg-int8@1.0.1: {} - - pg-logical-replication@2.0.7: - dependencies: - eventemitter2: 6.4.9 - pg: 8.13.3 - transitivePeerDependencies: - - pg-native - - pg-pool@3.7.1(pg@8.13.3): - dependencies: - pg: 8.13.3 - - pg-protocol@1.7.1: {} - - pg-types@2.2.0: - dependencies: - pg-int8: 1.0.1 - postgres-array: 2.0.0 - postgres-bytea: 1.0.0 - postgres-date: 1.0.7 - postgres-interval: 1.2.0 - - pg@8.13.3: - dependencies: - pg-connection-string: 2.7.0 - pg-pool: 3.7.1(pg@8.13.3) - pg-protocol: 1.7.1 - pg-types: 2.2.0 - pgpass: 1.0.5 - optionalDependencies: - pg-cloudflare: 1.1.1 - - pgpass@1.0.5: - dependencies: - split2: 4.2.0 - picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -7733,18 +7771,6 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - postgres-array@2.0.0: {} - - postgres-array@3.0.4: {} - - postgres-bytea@1.0.0: {} - - postgres-date@1.0.7: {} - - postgres-interval@1.2.0: - dependencies: - xtend: 4.0.2 - postgres@3.4.5: {} prebuild-install@7.1.3: @@ -7764,6 +7790,8 @@ snapshots: prelude-ls@1.2.1: {} + prettier@3.5.3: {} + pretty-bytes@6.1.1: {} pretty-ms@9.2.0: @@ -7989,6 +8017,8 @@ snapshots: siginfo@2.0.0: {} + signal-exit@3.0.7: {} + signal-exit@4.1.0: {} simple-concat@1.0.1: {} @@ -8086,6 +8116,8 @@ snapshots: strip-bom@4.0.0: {} + strip-final-newline@2.0.0: {} + strip-final-newline@3.0.0: {} strip-final-newline@4.0.0: {} @@ -8446,6 +8478,10 @@ snapshots: dependencies: isexe: 2.0.0 + which@4.0.0: + dependencies: + isexe: 3.1.1 + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 @@ -8479,8 +8515,6 @@ snapshots: xml-name-validator@4.0.0: {} - xtend@4.0.2: {} - y18n@5.0.8: {} yallist@4.0.0: {} diff --git a/src/index.ts b/src/index.ts index 345d709..4ae50f5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1 @@ -export { useQuery } from './query' +export { useQuery, type UseQueryOptions } from './query' diff --git a/src/query.test.ts b/src/query.test.ts new file mode 100644 index 0000000..0d63221 --- /dev/null +++ b/src/query.test.ts @@ -0,0 +1,222 @@ +import type { TTL } from '@rocicorp/zero' +import { createSchema, number, string, table, Zero } from '@rocicorp/zero' +import { describe, expect, it, vi } from 'vitest' +import { ref, watchEffect } from 'vue' +import { useQuery } from './query' +import { vueViewFactory } from './view' + +async function setupTestEnvironment() { + const schema = createSchema({ + tables: [ + table('table') + .columns({ + a: number(), + b: string(), + }) + .primaryKey('a'), + ], + }) + + const z = new Zero({ + userID: 'asdf', + server: null, + schema, + // This is often easier to develop with if you're frequently changing + // the schema. Switch to 'idb' for local-persistence. + kvStore: 'mem', + }) + + await z.mutate.table.insert({ a: 1, b: 'a' }) + await z.mutate.table.insert({ a: 2, b: 'b' }) + + const tableQuery = z.query.table + + return { z, tableQuery } +} + +describe('useQuery', () => { + it('useQuery', async () => { + const { z, tableQuery } = await setupTestEnvironment() + + const { data: rows, status } = useQuery(() => tableQuery) + expect(rows.value).toMatchInlineSnapshot(`[ + { + "a": 1, + "b": "a", + Symbol(rc): 1, + }, + { + "a": 2, + "b": "b", + Symbol(rc): 1, + }, +]`) + expect(status.value).toEqual('unknown') + + await z.mutate.table.insert({ a: 3, b: 'c' }) + await 1 + + expect(rows.value).toMatchInlineSnapshot(`[ + { + "a": 1, + "b": "a", + Symbol(rc): 1, + }, + { + "a": 2, + "b": "b", + Symbol(rc): 1, + }, + { + "a": 3, + "b": "c", + Symbol(rc): 1, + }, +]`) + + // TODO: this is not working at the moment, possibly because we don't have a server connection in test + // expect(resultType.value).toEqual("complete"); + + z.close() + }) + + it('useQuery with ttl', async () => { + const { z, tableQuery } = await setupTestEnvironment() + const ttl = ref('1m') + + const materializeSpy = vi.spyOn(tableQuery, 'materialize') + const updateTTLSpy = vi.spyOn(tableQuery, 'updateTTL') + const queryGetter = vi.fn(() => tableQuery) + + useQuery(queryGetter, () => ({ ttl: ttl.value })) + + expect(queryGetter).toHaveBeenCalledTimes(1) + expect(updateTTLSpy).toHaveBeenCalledTimes(0) + expect(materializeSpy).toHaveBeenCalledExactlyOnceWith( + vueViewFactory, + '1m', + ) + materializeSpy.mockClear() + + ttl.value = '10m' + await 1 + + expect(materializeSpy).toHaveBeenCalledTimes(0) + expect(updateTTLSpy).toHaveBeenCalledExactlyOnceWith('10m') + + z.close() + }) + + it('useQuery deps change', async () => { + const { z, tableQuery } = await setupTestEnvironment() + + const a = ref(1) + + const { data: rows, status } = useQuery(() => + tableQuery.where('a', a.value), + ) + + const rowLog: unknown[] = [] + const resultDetailsLog: unknown[] = [] + const resetLogs = () => { + rowLog.length = 0 + resultDetailsLog.length = 0 + } + + watchEffect(() => { + rowLog.push(rows.value) + }) + + watchEffect(() => { + resultDetailsLog.push(status.value) + }) + + expect(rowLog).toMatchInlineSnapshot(`[ + [ + { + "a": 1, + "b": "a", + Symbol(rc): 1, + }, + ], +]`) + // expect(resultDetailsLog).toEqual(["unknown"]); + resetLogs() + + expect(rowLog).toEqual([]) + // expect(resultDetailsLog).toEqual(["complete"]); + resetLogs() + + a.value = 2 + await 1 + + expect(rowLog).toMatchInlineSnapshot(`[ + [ + { + "a": 2, + "b": "b", + Symbol(rc): 1, + }, + ], +]`) + // expect(resultDetailsLog).toEqual(["unknown"]); + resetLogs() + + expect(rowLog).toEqual([]) + // expect(resultDetailsLog).toEqual(["complete"]); + + z.close() + }) + + it('useQuery deps change watchEffect', async () => { + const { z, tableQuery } = await setupTestEnvironment() + const a = ref(1) + const { data: rows } = useQuery(() => tableQuery.where('a', a.value)) + + let run = 0 + + await new Promise((resolve) => { + watchEffect(() => { + if (run === 0) { + expect(rows.value).toMatchInlineSnapshot( + `[ + { + "a": 1, + "b": "a", + Symbol(rc): 1, + }, +]`, + ) + z.mutate.table.update({ a: 1, b: 'a2' }) + } + else if (run === 1) { + expect(rows.value).toMatchInlineSnapshot( + `[ + { + "a": 1, + "b": "a2", + Symbol(rc): 1, + }, +]`, + ) + a.value = 2 + } + else if (run === 2) { + expect(rows.value).toMatchInlineSnapshot( + `[ + { + "a": 2, + "b": "b", + Symbol(rc): 1, + }, +]`, + ) + resolve(true) + } + run++ + }) + }) + + z.close() + }) +}) diff --git a/src/query.ts b/src/query.ts index a8c236b..e4b7c3b 100644 --- a/src/query.ts +++ b/src/query.ts @@ -1,12 +1,24 @@ // based on https://github.com/rocicorp/mono/tree/main/packages/zero-solid -import type { ResultType, Schema } from '@rocicorp/zero' -import type { AdvancedQuery, HumanReadable, Query } from '@rocicorp/zero/advanced' +import type { HumanReadable, Query, ResultType, Schema, TTL } from '@rocicorp/zero' import type { ComputedRef, MaybeRefOrGetter } from 'vue' +import type { VueView } from './view' -import { computed, getCurrentInstance, isRef, onUnmounted, shallowRef, toValue, watch } from 'vue' +import { DEFAULT_TTL } from '@rocicorp/zero' +import { + computed, + getCurrentInstance, + onUnmounted, + shallowRef, + toValue, + watch, +} from 'vue' import { vueViewFactory } from './view' +export interface UseQueryOptions { + ttl?: TTL | undefined +} + interface QueryResult { data: ComputedRef> status: ComputedRef @@ -16,23 +28,34 @@ export function useQuery< TSchema extends Schema, TTable extends keyof TSchema['tables'] & string, TReturn, ->(_query: MaybeRefOrGetter>): QueryResult { - const query = toValue(_query) as AdvancedQuery - const view = shallowRef(query.materialize(vueViewFactory)) - - if (isRef(_query) || typeof _query === 'function') { - watch(_query, (query) => { - view.value.destroy() - view.value = (query as AdvancedQuery).materialize(vueViewFactory) - }) - } +>( + query: MaybeRefOrGetter>, + options?: MaybeRefOrGetter, +): QueryResult { + const ttl = computed(() => { + return toValue(options)?.ttl ?? DEFAULT_TTL + }) + const view = shallowRef> | null>(null) + + watch( + () => toValue(query), + (q) => { + view.value?.destroy() + view.value = q.materialize(vueViewFactory, ttl.value) + }, + { immediate: true }, + ) + + watch(ttl, (ttl) => { + toValue(query).updateTTL(ttl) + }) if (getCurrentInstance()) { - onUnmounted(() => view.value.destroy()) + onUnmounted(() => view.value!.destroy()) } return { - data: computed(() => view.value.data), - status: computed(() => view.value.status), + data: computed(() => view.value!.data), + status: computed(() => view.value!.status), } } diff --git a/src/view.test.ts b/src/view.test.ts new file mode 100644 index 0000000..4e13fd7 --- /dev/null +++ b/src/view.test.ts @@ -0,0 +1,1318 @@ +import type { Query, Schema } from '@rocicorp/zero' + +import { resolver } from '@rocicorp/resolver' +import { + createSchema, + number, + relationships, + string, + table, + Zero, +} from '@rocicorp/zero' +import { describe, expect, it, vi } from 'vitest' +import { VueView, vueViewFactory } from './view' + +const simpleSchema = createSchema({ + tables: [ + table('table') + .columns({ + a: number(), + b: string(), + }) + .primaryKey('a'), + ], +}) + +const recursiveTable = table('table') + .columns({ + id: number(), + name: string(), + data: string().optional(), + childID: number().optional(), + }) + .primaryKey('id') +const treeSchema = createSchema({ + tables: [recursiveTable], + relationships: [ + relationships(recursiveTable, ({ many }) => ({ + children: many({ + sourceField: ['childID'], + destSchema: recursiveTable, + destField: ['id'], + }), + })), + ], +}) + +const issue = table('issue') + .columns({ + id: number(), + name: string(), + }) + .primaryKey('id') + +const label = table('label') + .columns({ + id: number(), + name: string(), + }) + .primaryKey('id') + +const issueLabel = table('issueLabel') + .columns({ + id: number(), + issueID: number(), + labelID: number(), + extra: string(), + }) + .primaryKey('id') + +const collapseSchema = createSchema({ + tables: [issue, label, issueLabel], + relationships: [ + relationships(issue, ({ many }) => ({ + labels: many( + { + sourceField: ['id'], + destSchema: issueLabel, + destField: ['issueID'], + }, + { + sourceField: ['labelID'], + destSchema: label, + destField: ['id'], + }, + ), + })), + ], +}) + +async function setupTestEnvironment(schema: S) { + const z = new Zero({ + userID: 'asdf', + server: null, + schema, + // This is often easier to develop with if you're frequently changing + // the schema. Switch to 'idb' for local-persistence. + kvStore: 'mem', + }) + + return { z } +} + +describe('vueView', () => { + it('basics', async () => { + const { z } = await setupTestEnvironment(simpleSchema) + const tableQuery = z.query.table + + await z.mutate.table.insert({ a: 1, b: 'a' }) + await z.mutate.table.insert({ a: 2, b: 'b' }) + + const view = tableQuery.materialize(vueViewFactory) + + expect(view.data).toMatchInlineSnapshot(` + [ + { + "a": 1, + "b": "a", + Symbol(rc): 1, + }, + { + "a": 2, + "b": "b", + Symbol(rc): 1, + }, + ] + `) + + // TODO: Test with a real resolver + // expect(view.status).toEqual("complete"); + + await z.mutate.table.insert({ a: 3, b: 'c' }) + + expect(view.data).toMatchInlineSnapshot(` + [ + { + "a": 1, + "b": "a", + Symbol(rc): 1, + }, + { + "a": 2, + "b": "b", + Symbol(rc): 1, + }, + { + "a": 3, + "b": "c", + Symbol(rc): 1, + }, + ] + `) + + await z.mutate.table.delete({ a: 1 }) + await z.mutate.table.delete({ a: 2 }) + + expect(view.data).toMatchInlineSnapshot(` + [ + { + "a": 3, + "b": "c", + Symbol(rc): 1, + }, + ] + `) + + await z.mutate.table.delete({ a: 3 }) + + expect(view.data).toEqual([]) + + z.close() + }) + + it.skip('basics-perf', async () => { + const iterations = 10_000 + + const { z } = await setupTestEnvironment(simpleSchema) + const tableQuery = z.query.table + + const view = tableQuery.materialize(vueViewFactory) + expect(view.data.length).toBe(0) + + for (const i in [...Array.from({ length: iterations }).keys()]) { + await z.mutate.table.insert({ a: Number(i), b: 'a' }) + } + + expect(view.data.length).toBe(iterations) + + z.close() + }) + + it('hydrate-empty', async () => { + const { z } = await setupTestEnvironment(simpleSchema) + const tableQuery = z.query.table + + const view = tableQuery.materialize(vueViewFactory) + + expect(view.data).toEqual([]) + + z.close() + }) + + it('tree', async () => { + const { z } = await setupTestEnvironment(treeSchema) + + await z.mutate.table.insert({ id: 1, name: 'foo', data: null, childID: 2 }) + await z.mutate.table.insert({ + id: 2, + name: 'foobar', + data: null, + childID: null, + }) + await z.mutate.table.insert({ id: 3, name: 'mon', data: null, childID: 4 }) + await z.mutate.table.insert({ + id: 4, + name: 'monkey', + data: null, + childID: null, + }) + + const query = z.query.table.related('children') + const view = query.materialize(vueViewFactory) + + expect(view.data).toMatchInlineSnapshot(` + [ + { + "childID": 2, + "children": [ + { + "childID": null, + "data": null, + "id": 2, + "name": "foobar", + Symbol(rc): 1, + }, + ], + "data": null, + "id": 1, + "name": "foo", + Symbol(rc): 1, + }, + { + "childID": null, + "children": [], + "data": null, + "id": 2, + "name": "foobar", + Symbol(rc): 1, + }, + { + "childID": 4, + "children": [ + { + "childID": null, + "data": null, + "id": 4, + "name": "monkey", + Symbol(rc): 1, + }, + ], + "data": null, + "id": 3, + "name": "mon", + Symbol(rc): 1, + }, + { + "childID": null, + "children": [], + "data": null, + "id": 4, + "name": "monkey", + Symbol(rc): 1, + }, + ] + `) + + await z.mutate.table.insert({ id: 5, name: 'chocolate', childID: 2 }) + expect(view.data).toMatchInlineSnapshot(` + [ + { + "childID": 2, + "children": [ + { + "childID": null, + "data": null, + "id": 2, + "name": "foobar", + Symbol(rc): 1, + }, + ], + "data": null, + "id": 1, + "name": "foo", + Symbol(rc): 1, + }, + { + "childID": null, + "children": [], + "data": null, + "id": 2, + "name": "foobar", + Symbol(rc): 1, + }, + { + "childID": 4, + "children": [ + { + "childID": null, + "data": null, + "id": 4, + "name": "monkey", + Symbol(rc): 1, + }, + ], + "data": null, + "id": 3, + "name": "mon", + Symbol(rc): 1, + }, + { + "childID": null, + "children": [], + "data": null, + "id": 4, + "name": "monkey", + Symbol(rc): 1, + }, + { + "childID": 2, + "children": [ + { + "childID": null, + "data": null, + "id": 2, + "name": "foobar", + Symbol(rc): 1, + }, + ], + "data": null, + "id": 5, + "name": "chocolate", + Symbol(rc): 1, + }, + ] + `) + + await z.mutate.table.delete({ id: 2 }) + expect(view.data).toMatchInlineSnapshot(` + [ + { + "childID": 2, + "children": [], + "data": null, + "id": 1, + "name": "foo", + Symbol(rc): 1, + }, + { + "childID": 4, + "children": [ + { + "childID": null, + "data": null, + "id": 4, + "name": "monkey", + Symbol(rc): 1, + }, + ], + "data": null, + "id": 3, + "name": "mon", + Symbol(rc): 1, + }, + { + "childID": null, + "children": [], + "data": null, + "id": 4, + "name": "monkey", + Symbol(rc): 1, + }, + { + "childID": 2, + "children": [], + "data": null, + "id": 5, + "name": "chocolate", + Symbol(rc): 1, + }, + ] + `) + + await z.mutate.table.insert({ + id: 2, + name: 'foobaz', + childID: null, + }) + + expect(view.data).toMatchInlineSnapshot(` + [ + { + "childID": 2, + "children": [ + { + "childID": null, + "data": null, + "id": 2, + "name": "foobaz", + Symbol(rc): 1, + }, + ], + "data": null, + "id": 1, + "name": "foo", + Symbol(rc): 1, + }, + { + "childID": null, + "children": [], + "data": null, + "id": 2, + "name": "foobaz", + Symbol(rc): 1, + }, + { + "childID": 4, + "children": [ + { + "childID": null, + "data": null, + "id": 4, + "name": "monkey", + Symbol(rc): 1, + }, + ], + "data": null, + "id": 3, + "name": "mon", + Symbol(rc): 1, + }, + { + "childID": null, + "children": [], + "data": null, + "id": 4, + "name": "monkey", + Symbol(rc): 1, + }, + { + "childID": 2, + "children": [ + { + "childID": null, + "data": null, + "id": 2, + "name": "foobaz", + Symbol(rc): 1, + }, + ], + "data": null, + "id": 5, + "name": "chocolate", + Symbol(rc): 1, + }, + ] + `) + }) + + it('tree-single', async () => { + const { z } = await setupTestEnvironment(treeSchema) + + await z.mutate.table.insert({ id: 1, name: 'foo', childID: 2 }) + await z.mutate.table.insert({ id: 2, name: 'foobar', childID: null }) + + const query = z.query.table.related('children').one() + const view = query.materialize(vueViewFactory) + + expect(view.data).toMatchInlineSnapshot(` + { + "childID": 2, + "children": [ + { + "childID": null, + "data": null, + "id": 2, + "name": "foobar", + Symbol(rc): 1, + }, + ], + "data": null, + "id": 1, + "name": "foo", + Symbol(rc): 1, + } + `) + + // remove the child + await z.mutate.table.delete({ id: 2 }) + + expect(view.data).toMatchInlineSnapshot(` + { + "childID": 2, + "children": [], + "data": null, + "id": 1, + "name": "foo", + Symbol(rc): 1, + } + `) + + // remove the parent + await z.mutate.table.delete({ id: 1 }) + expect(view.data).toEqual(undefined) + + z.close() + }) + + it('collapse', async () => { + const { z } = await setupTestEnvironment(collapseSchema) + const query = z.query.issue.related('labels') + const view = query.materialize(vueViewFactory) + + expect(view.data).toEqual([]) + + const changeSansType = { + node: { + row: { + id: 1, + name: 'issue', + }, + relationships: { + labels: () => [ + { + row: { + id: 1, + issueId: 1, + labelId: 1, + extra: 'a', + }, + relationships: { + labels: () => [ + { + row: { + id: 1, + name: 'label', + }, + relationships: {}, + }, + ], + }, + }, + ], + }, + }, + } as const + + view.push({ + type: 'add', + ...changeSansType, + }) + + expect(view.data).toMatchInlineSnapshot(` + [ + { + "id": 1, + "labels": [ + { + "id": 1, + "name": "label", + Symbol(rc): 1, + }, + ], + "name": "issue", + Symbol(rc): 1, + }, + ] + `) + + view.push({ + type: 'remove', + ...changeSansType, + }) + expect(view.data).toEqual([]) + + view.push({ + type: 'add', + ...changeSansType, + }) + + view.push({ + type: 'child', + node: { + row: { + id: 1, + name: 'issue', + }, + relationships: { + labels: () => [ + { + row: { + id: 1, + issueId: 1, + labelId: 1, + extra: 'a', + }, + relationships: { + labels: () => [ + { + row: { + id: 1, + name: 'label', + }, + relationships: {}, + }, + ], + }, + }, + { + row: { + id: 2, + issueId: 1, + labelId: 2, + extra: 'b', + }, + relationships: { + labels: () => [ + { + row: { + id: 2, + name: 'label2', + }, + relationships: {}, + }, + ], + }, + }, + ], + }, + }, + child: { + relationshipName: 'labels', + change: { + type: 'add', + node: { + row: { + id: 2, + issueId: 1, + labelId: 2, + extra: 'b', + }, + relationships: { + labels: () => [ + { + row: { + id: 2, + name: 'label2', + }, + relationships: {}, + }, + ], + }, + }, + }, + }, + }) + + expect(view.data).toMatchInlineSnapshot(` + [ + { + "id": 1, + "labels": [ + { + "id": 1, + "name": "label", + Symbol(rc): 1, + }, + { + "id": 2, + "name": "label2", + Symbol(rc): 1, + }, + ], + "name": "issue", + Symbol(rc): 1, + }, + ] + `) + + // edit the hidden row + view.push({ + type: 'child', + node: { + row: { + id: 1, + name: 'issue', + }, + relationships: { + labels: () => [ + { + row: { + id: 1, + issueId: 1, + labelId: 1, + extra: 'a', + }, + relationships: { + labels: () => [ + { + row: { + id: 1, + name: 'label', + }, + relationships: {}, + }, + ], + }, + }, + { + row: { + id: 2, + issueId: 1, + labelId: 2, + extra: 'b2', + }, + relationships: { + labels: () => [ + { + row: { + id: 2, + name: 'label2', + }, + relationships: {}, + }, + ], + }, + }, + ], + }, + }, + child: { + relationshipName: 'labels', + change: { + type: 'edit', + oldNode: { + row: { + id: 2, + issueId: 1, + labelId: 2, + extra: 'b', + }, + relationships: { + labels: () => [ + { + row: { + id: 2, + name: 'label2', + }, + relationships: {}, + }, + ], + }, + }, + node: { + row: { + id: 2, + issueId: 1, + labelId: 2, + extra: 'b2', + }, + relationships: { + labels: () => [ + { + row: { + id: 2, + name: 'label2', + }, + relationships: {}, + }, + ], + }, + }, + }, + }, + }) + + expect(view.data).toMatchInlineSnapshot(` + [ + { + "id": 1, + "labels": [ + { + "id": 1, + "name": "label", + Symbol(rc): 1, + }, + { + "id": 2, + "name": "label2", + Symbol(rc): 1, + }, + ], + "name": "issue", + Symbol(rc): 1, + }, + ] + `) + + // edit the leaf + view.push({ + type: 'child', + node: { + row: { + id: 1, + name: 'issue', + }, + relationships: { + labels: () => [ + { + row: { + id: 1, + issueId: 1, + labelId: 1, + extra: 'a', + }, + relationships: { + labels: () => [ + { + row: { + id: 1, + name: 'label', + }, + relationships: {}, + }, + ], + }, + }, + { + row: { + id: 2, + issueId: 1, + labelId: 2, + extra: 'b2', + }, + relationships: { + labels: () => [ + { + row: { + id: 2, + name: 'label2x', + }, + relationships: {}, + }, + ], + }, + }, + ], + }, + }, + child: { + relationshipName: 'labels', + change: { + type: 'child', + node: { + row: { + id: 2, + issueId: 1, + labelId: 2, + extra: 'b2', + }, + relationships: { + labels: () => [ + { + row: { + id: 2, + name: 'label2x', + }, + relationships: {}, + }, + ], + }, + }, + child: { + relationshipName: 'labels', + change: { + type: 'edit', + oldNode: { + row: { + id: 2, + name: 'label2', + }, + relationships: {}, + }, + node: { + row: { + id: 2, + name: 'label2x', + }, + relationships: {}, + }, + }, + }, + }, + }, + }) + + expect(view.data).toMatchInlineSnapshot(` + [ + { + "id": 1, + "labels": [ + { + "id": 1, + "name": "label", + Symbol(rc): 1, + }, + { + "id": 2, + "name": "label2x", + Symbol(rc): 1, + }, + ], + "name": "issue", + Symbol(rc): 1, + }, + ] + `) + + z.close() + }) + + it('collapse-single', async () => { + const { z } = await setupTestEnvironment(collapseSchema) + const query = z.query.issue.related('labels') + const view = query.materialize(vueViewFactory) + + expect(view.data).toEqual([]) + + const changeSansType = { + node: { + row: { + id: 1, + name: 'issue', + }, + relationships: { + labels: () => [ + { + row: { + id: 1, + issueId: 1, + labelId: 1, + }, + relationships: { + labels: () => [ + { + row: { + id: 1, + name: 'label', + }, + relationships: {}, + }, + ], + }, + }, + ], + }, + }, + } as const + view.push({ + type: 'add', + ...changeSansType, + }) + + expect(view.data).toMatchInlineSnapshot(` + [ + { + "id": 1, + "labels": [ + { + "id": 1, + "name": "label", + Symbol(rc): 1, + }, + ], + "name": "issue", + Symbol(rc): 1, + }, + ] + `) + + z.close() + }) + + it('basic with edit pushes', async () => { + const { z } = await setupTestEnvironment(simpleSchema) + await z.mutate.table.insert({ a: 1, b: 'a' }) + await z.mutate.table.insert({ a: 2, b: 'b' }) + + const query = z.query.table + const view = query.materialize(vueViewFactory) + expect(view.data).toMatchInlineSnapshot(` + [ + { + "a": 1, + "b": "a", + Symbol(rc): 1, + }, + { + "a": 2, + "b": "b", + Symbol(rc): 1, + }, + ] + `) + + await z.mutate.table.update({ a: 2, b: 'b2' }) + + expect(view.data).toMatchInlineSnapshot(` + [ + { + "a": 1, + "b": "a", + Symbol(rc): 1, + }, + { + "a": 2, + "b": "b2", + Symbol(rc): 1, + }, + ] + `) + + await z.mutate.table.insert({ a: 3, b: 'b3' }) + await z.mutate.table.delete({ a: 2 }) + + expect(view.data).toMatchInlineSnapshot(` + [ + { + "a": 1, + "b": "a", + Symbol(rc): 1, + }, + { + "a": 3, + "b": "b3", + Symbol(rc): 1, + }, + ] + `) + + z.close() + }) + + it('tree edit', async () => { + const { z } = await setupTestEnvironment(treeSchema) + + await z.mutate.table.insert({ id: 1, name: 'foo', data: 'a', childID: 2 }) + await z.mutate.table.insert({ + id: 2, + name: 'foobar', + data: 'b', + childID: null, + }) + await z.mutate.table.insert({ id: 3, name: 'mon', data: 'c', childID: 4 }) + await z.mutate.table.insert({ + id: 4, + name: 'monkey', + data: 'd', + childID: null, + }) + + const query = z.query.table.related('children') + const view = query.materialize(vueViewFactory) + + expect(view.data).toMatchInlineSnapshot(` + [ + { + "childID": 2, + "children": [ + { + "childID": null, + "data": "b", + "id": 2, + "name": "foobar", + Symbol(rc): 1, + }, + ], + "data": "a", + "id": 1, + "name": "foo", + Symbol(rc): 1, + }, + { + "childID": null, + "children": [], + "data": "b", + "id": 2, + "name": "foobar", + Symbol(rc): 1, + }, + { + "childID": 4, + "children": [ + { + "childID": null, + "data": "d", + "id": 4, + "name": "monkey", + Symbol(rc): 1, + }, + ], + "data": "c", + "id": 3, + "name": "mon", + Symbol(rc): 1, + }, + { + "childID": null, + "children": [], + "data": "d", + "id": 4, + "name": "monkey", + Symbol(rc): 1, + }, + ] + `) + + // Edit root + await z.mutate.table.update({ id: 1, data: 'a2' }) + + expect(view.data).toMatchInlineSnapshot(` + [ + { + "childID": 2, + "children": [ + { + "childID": null, + "data": "b", + "id": 2, + "name": "foobar", + Symbol(rc): 1, + }, + ], + "data": "a2", + "id": 1, + "name": "foo", + Symbol(rc): 1, + }, + { + "childID": null, + "children": [], + "data": "b", + "id": 2, + "name": "foobar", + Symbol(rc): 1, + }, + { + "childID": 4, + "children": [ + { + "childID": null, + "data": "d", + "id": 4, + "name": "monkey", + Symbol(rc): 1, + }, + ], + "data": "c", + "id": 3, + "name": "mon", + Symbol(rc): 1, + }, + { + "childID": null, + "children": [], + "data": "d", + "id": 4, + "name": "monkey", + Symbol(rc): 1, + }, + ] + `) + + // Edit leaf + await z.mutate.table.update({ id: 4, data: 'd2' }) + expect(view.data).toMatchInlineSnapshot(` + [ + { + "childID": 2, + "children": [ + { + "childID": null, + "data": "b", + "id": 2, + "name": "foobar", + Symbol(rc): 1, + }, + ], + "data": "a2", + "id": 1, + "name": "foo", + Symbol(rc): 1, + }, + { + "childID": null, + "children": [], + "data": "b", + "id": 2, + "name": "foobar", + Symbol(rc): 1, + }, + { + "childID": 4, + "children": [ + { + "childID": null, + "data": "d2", + "id": 4, + "name": "monkey", + Symbol(rc): 1, + }, + ], + "data": "c", + "id": 3, + "name": "mon", + Symbol(rc): 1, + }, + { + "childID": null, + "children": [], + "data": "d2", + "id": 4, + "name": "monkey", + Symbol(rc): 1, + }, + ] + `) + + z.close() + }) + + it('queryComplete promise', async () => { + const { z } = await setupTestEnvironment(simpleSchema) + await z.mutate.table.insert({ a: 1, b: 'a' }) + await z.mutate.table.insert({ a: 2, b: 'b' }) + + const queryCompleteResolver = resolver() + + const onTransactionCommit = () => {} + + const query = z.query.table + const view = query.materialize((_, input) => { + return new VueView( + input, + onTransactionCommit, + { singular: false, relationships: {} }, + () => {}, + queryCompleteResolver.promise, + ) + }) + + expect(view.data).toMatchInlineSnapshot(` + [ + { + "a": 1, + "b": "a", + Symbol(rc): 1, + }, + { + "a": 2, + "b": "b", + Symbol(rc): 1, + }, + ] + `) + expect(view.status).toEqual('unknown') + + queryCompleteResolver.resolve(true) + await 1 + expect(view.status).toEqual('complete') + + z.close() + }) +}) + +describe('vueViewFactory', () => { + interface TestReturn { + a: number + b: string + } + + it('correctly calls corresponding handlers', async () => { + const { z } = await setupTestEnvironment(simpleSchema) + await z.mutate.table.insert({ a: 1, b: 'a' }) + await z.mutate.table.insert({ a: 2, b: 'b' }) + + const onDestroy = vi.fn() + const onTransactionCommit = vi.fn() + + const query = z.query.table + const view = query.materialize((_, input) => { + return vueViewFactory( + undefined as unknown as Query, + input, + { singular: false, relationships: {} }, + onDestroy, + onTransactionCommit, + true, + ) + }) + + expect(view).toBeDefined() + expect(onTransactionCommit).not.toHaveBeenCalled() + expect(onDestroy).not.toHaveBeenCalled() + view.destroy() + expect(onDestroy).toHaveBeenCalledTimes(1) + + z.close() + }) +}) diff --git a/src/view.ts b/src/view.ts index ecd0ebf..c4d0dbe 100644 --- a/src/view.ts +++ b/src/view.ts @@ -1,9 +1,19 @@ // based on https://github.com/rocicorp/mono/tree/main/packages/zero-solid -import type { ResultType, Schema } from '@rocicorp/zero' -import type { Change, Entry, Format, HumanReadable, Input, Output, Query, ViewFactory } from '@rocicorp/zero/advanced' -import { applyChange } from '@rocicorp/zero/advanced' -import { reactive, toRaw } from 'vue' +import type { + Change, + Entry, + Format, + HumanReadable, + Input, + Output, + Query, + ResultType, + Schema, + ViewFactory, +} from '@rocicorp/zero' +import { applyChange } from '@rocicorp/zero' +import { reactive } from 'vue' interface QueryResultDetails { readonly type: ResultType @@ -14,33 +24,10 @@ type State = [Entry, QueryResultDetails] const complete = { type: 'complete' } as const const unknown = { type: 'unknown' } as const -interface RefCountMap { - get: (entry: Entry) => number | undefined - set: (entry: Entry, refCount: number) => void - delete: (entry: Entry) => boolean -} - -class VueRefCountMap implements RefCountMap { - readonly #map = new WeakMap() - - get(entry: Entry) { - return this.#map.get(toRaw(entry)) - } - - set(entry: Entry, refCount: number) { - this.#map.set(toRaw(entry), refCount) - } - - delete(entry: Entry) { - return this.#map.delete(toRaw(entry)) - } -} - -class VueView implements Output { +export class VueView implements Output { readonly #input: Input readonly #format: Format readonly #onDestroy: () => void - readonly #refCountMap = new VueRefCountMap() #state: State @@ -90,7 +77,6 @@ class VueView implements Output { this.#input.getSchema(), '', this.#format, - this.#refCountMap, ) } diff --git a/test/index.test.ts b/test/index.test.ts index 10214ff..5be2cd1 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -12,7 +12,7 @@ describe('zero-vue', () => { }) .primaryKey('id') - const schema = createSchema(1, { + const schema = createSchema({ tables: [user], }) @@ -34,6 +34,14 @@ describe('zero-vue', () => { expect(users.value).toEqual([]) await new Promise(resolve => setTimeout(resolve, 0)) - expect(users.value).toEqual([{ id: 'asdf', name: 'Alice' }]) + expect(users.value).toMatchInlineSnapshot(` + [ + { + "id": "asdf", + "name": "Alice", + Symbol(rc): 1, + }, + ] + `) }) }) -- 2.51.2