diff --git a/package.json b/package.json index fa90b25..65bb239 100644 --- a/package.json +++ b/package.json @@ -29,12 +29,13 @@ "release": "semantic-release" }, "dependencies": { + "pinia": "^2.0.25", "sift": "^15.0.0" }, "devDependencies": { - "@feathersjs/adapter-commons": "5.0.0-pre.15", - "@feathersjs/errors": "5.0.0-pre.15", - "@feathersjs/feathers": "5.0.0-pre.15", + "@feathersjs/adapter-commons": "5.0.0-pre.17", + "@feathersjs/errors": "5.0.0-pre.17", + "@feathersjs/feathers": "5.0.0-pre.17", "@geprog/eslint-config": "1.1.0", "@geprog/semantic-release-config": "1.0.0", "@vue/test-utils": "2.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f7aa332..09bb771 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,15 +1,16 @@ lockfileVersion: 5.4 specifiers: - '@feathersjs/adapter-commons': 5.0.0-pre.15 - '@feathersjs/errors': 5.0.0-pre.15 - '@feathersjs/feathers': 5.0.0-pre.15 + '@feathersjs/adapter-commons': 5.0.0-pre.17 + '@feathersjs/errors': 5.0.0-pre.17 + '@feathersjs/feathers': 5.0.0-pre.17 '@geprog/eslint-config': 1.1.0 '@geprog/semantic-release-config': 1.0.0 '@vue/test-utils': 2.0.0 c8: 7.11.3 eslint: 8.8.0 jsdom: 20.0.0 + pinia: ^2.0.25 prettier: 2.5.1 semantic-release: 19.0.3 sift: ^15.0.0 @@ -20,12 +21,13 @@ specifiers: vue: 3.2.37 dependencies: + pinia: 2.0.25_khqcrm3nbfs7aq4aok36jjwtje sift: 15.1.3 devDependencies: - '@feathersjs/adapter-commons': 5.0.0-pre.15 - '@feathersjs/errors': 5.0.0-pre.15 - '@feathersjs/feathers': 5.0.0-pre.15 + '@feathersjs/adapter-commons': 5.0.0-pre.17 + '@feathersjs/errors': 5.0.0-pre.17 + '@feathersjs/feathers': 5.0.0-pre.17 '@geprog/eslint-config': 1.1.0_4guzhfrlitqfeskhcqimwf3bxa '@geprog/semantic-release-config': 1.0.0_semantic-release@19.0.3 '@vue/test-utils': 2.0.0_vue@3.2.37 @@ -52,7 +54,6 @@ packages: /@babel/helper-validator-identifier/7.16.7: resolution: {integrity: sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==} engines: {node: '>=6.9.0'} - dev: true /@babel/highlight/7.16.10: resolution: {integrity: sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==} @@ -69,7 +70,6 @@ packages: hasBin: true dependencies: '@babel/types': 7.16.8 - dev: true /@babel/types/7.16.8: resolution: {integrity: sha512-smN2DQc5s4M7fntyjGtyIPbRJv6wW4rU/94fmYJ7PKQuZkC0qGMHXJbg6sNGt12JmVr4k5YaptI/XtiLJBnmIg==} @@ -77,7 +77,6 @@ packages: dependencies: '@babel/helper-validator-identifier': 7.16.7 to-fast-properties: 2.0.0 - dev: true /@bcoe/v8-coverage/0.2.3: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} @@ -100,42 +99,51 @@ packages: - supports-color dev: true - /@feathersjs/adapter-commons/5.0.0-pre.15: - resolution: {integrity: sha512-93PsxOKqfeQNGQ0RcdgvkIS33hU7oKYkJTDH2gQw+afzCmE6Ypb137bDM9CVZbLIAFd/VIIOARL3VKtWC3acOg==} + /@feathersjs/adapter-commons/5.0.0-pre.17: + resolution: {integrity: sha512-Pf6RNr2cQrTdX0E/y2aQv9pfgD6TWxJoFwOdsOTfSIzop4UBp83O2MRjfvSygpbHMiSIgEV0QzrHGGh1KbA+sg==} engines: {node: '>= 12'} dependencies: - '@feathersjs/commons': 5.0.0-pre.16 - '@feathersjs/errors': 5.0.0-pre.16 - '@feathersjs/feathers': 5.0.0-pre.15 + '@feathersjs/commons': 5.0.0-pre.33 + '@feathersjs/errors': 5.0.0-pre.33 + '@feathersjs/feathers': 5.0.0-pre.33 dev: true - /@feathersjs/commons/5.0.0-pre.16: - resolution: {integrity: sha512-sTrOuE50qNRk0AsGaPRh90XxK1nUDib4/gyoqYxFewB5Z0xh2uyEFSppeD0oty+HfPL5XyZVMMzxbH+vUPL39w==} + /@feathersjs/commons/5.0.0-pre.33: + resolution: {integrity: sha512-F0T94zN7/hWVejGOYrXd0WCNrudeA1saGhRxmTFJVXRyclRMjM+5lohxeX14h3IxF+eYnrTa5V8I5/UHWcwtLA==} engines: {node: '>= 12'} dev: true - /@feathersjs/errors/5.0.0-pre.15: - resolution: {integrity: sha512-MuNZWN9FI8J2zBx+02fp3UlRxcZwvEaUrshTEiyFiEyb0PPY4m7h53K/0ps0IINimSha03LGwnNf1gxDJWkyDQ==} + /@feathersjs/errors/5.0.0-pre.17: + resolution: {integrity: sha512-ZExh5PsK3On6uOoxZ2tUNqt1wfMT088Lgu3i4RpELF+fXrkswu/u4uW6g7Rd4Z0VQgxnY7YMjHFd3OJX9TlmZQ==} engines: {node: '>= 12'} dev: true - /@feathersjs/errors/5.0.0-pre.16: - resolution: {integrity: sha512-hp41eSglMpRsPuwWGAk7H3j2M80RIbn65/QOWsfu8ZmIQDQIEAcmqDWnzu80lPzXoh01deOdcBdz3+6YQEZBQA==} + /@feathersjs/errors/5.0.0-pre.33: + resolution: {integrity: sha512-nCTX9bGO4KgLREFz+tMRkVggrrL5+7DIYUL5FqVesLl2jSeS4BN70Jn9G7AKTKepdqWZV1e/R/ZgOty+pFlGCw==} engines: {node: '>= 12'} dev: true - /@feathersjs/feathers/5.0.0-pre.15: - resolution: {integrity: sha512-53ToWckg6csoaguLw0t3iG2rO109lZsbTrPv85RBKNnhd+ENyjW6+Kkw60Um17PC03n8uyb+5mGJr8paIVRBhg==} + /@feathersjs/feathers/5.0.0-pre.17: + resolution: {integrity: sha512-ljwx9jOPoySOflsshe2l0z6RdqV5YHFqiUQBuYZQwUvP/zvHixPVWgESEiauIevfw03KxN1EaZ1oTL8zsSVXVw==} engines: {node: '>= 12'} dependencies: - '@feathersjs/commons': 5.0.0-pre.16 - '@feathersjs/hooks': 0.6.5 + '@feathersjs/commons': 5.0.0-pre.33 + '@feathersjs/hooks': 0.7.5 events: 3.3.0 dev: true - /@feathersjs/hooks/0.6.5: - resolution: {integrity: sha512-WtcEoG/imdHRvC3vofGi/OcgH+cjHHhO0AfEeTlsnrKLjVKKBXV6aoIrB2nHZPpE7iW5sA7AZMR6bPD8ytxN+w==} - engines: {node: '>= 10'} + /@feathersjs/feathers/5.0.0-pre.33: + resolution: {integrity: sha512-nGTQBd/QgWLCnQTyErS180Up0m9znX9vqmJzLfnKWOrr9Zao7/wfpPxK+2/p/pKxjt1iJVV02xPwGk9bXZ9Qtw==} + engines: {node: '>= 12'} + dependencies: + '@feathersjs/commons': 5.0.0-pre.33 + '@feathersjs/hooks': 0.7.5 + events: 3.3.0 + dev: true + + /@feathersjs/hooks/0.7.5: + resolution: {integrity: sha512-xHbWPCJ5upr9eDdRSuJgWSKgCLN07YLvVjS7PdDqWrEz/iz5YBqoIQOzHhNPsWXOA6T7v+ArifrTUitG+ZzIRA==} + engines: {node: '>= 14'} dev: true /@geprog/eslint-config/1.1.0_4guzhfrlitqfeskhcqimwf3bxa: @@ -761,14 +769,12 @@ packages: '@vue/shared': 3.2.37 estree-walker: 2.0.2 source-map: 0.6.1 - dev: true /@vue/compiler-dom/3.2.37: resolution: {integrity: sha512-yxJLH167fucHKxaqXpYk7x8z7mMEnXOw3G2q62FTkmsvNxu4FQSu5+3UMb+L7fjKa26DEzhrmCxAgFLLIzVfqQ==} dependencies: '@vue/compiler-core': 3.2.37 '@vue/shared': 3.2.37 - dev: true /@vue/compiler-sfc/3.2.37: resolution: {integrity: sha512-+7i/2+9LYlpqDv+KTtWhOZH+pa8/HnX/905MdVmAcI/mPQOBwkHHIzrsEsucyOIZQYMkXUiTkmZq5am/NyXKkg==} @@ -783,14 +789,16 @@ packages: magic-string: 0.25.7 postcss: 8.4.5 source-map: 0.6.1 - dev: true /@vue/compiler-ssr/3.2.37: resolution: {integrity: sha512-7mQJD7HdXxQjktmsWp/J67lThEIcxLemz1Vb5I6rYJHR5vI+lON3nPGOH3ubmbvYGt8xEUaAr1j7/tIFWiEOqw==} dependencies: '@vue/compiler-dom': 3.2.37 '@vue/shared': 3.2.37 - dev: true + + /@vue/devtools-api/6.4.5: + resolution: {integrity: sha512-JD5fcdIuFxU4fQyXUu3w2KpAJHzTVdN+p4iOX2lMWSHMOoQdMAcpFLZzm9Z/2nmsoZ1a96QEhZ26e50xLBsgOQ==} + dev: false /@vue/reactivity-transform/3.2.37: resolution: {integrity: sha512-IWopkKEb+8qpu/1eMKVeXrK0NLw9HicGviJzhJDEyfxTR9e1WtpnnbYkJWurX6WwoFP0sz10xQg8yL8lgskAZg==} @@ -800,20 +808,17 @@ packages: '@vue/shared': 3.2.37 estree-walker: 2.0.2 magic-string: 0.25.7 - dev: true /@vue/reactivity/3.2.37: resolution: {integrity: sha512-/7WRafBOshOc6m3F7plwzPeCu/RCVv9uMpOwa/5PiY1Zz+WLVRWiy0MYKwmg19KBdGtFWsmZ4cD+LOdVPcs52A==} dependencies: '@vue/shared': 3.2.37 - dev: true /@vue/runtime-core/3.2.37: resolution: {integrity: sha512-JPcd9kFyEdXLl/i0ClS7lwgcs0QpUAWj+SKX2ZC3ANKi1U4DOtiEr6cRqFXsPwY5u1L9fAjkinIdB8Rz3FoYNQ==} dependencies: '@vue/reactivity': 3.2.37 '@vue/shared': 3.2.37 - dev: true /@vue/runtime-dom/3.2.37: resolution: {integrity: sha512-HimKdh9BepShW6YozwRKAYjYQWg9mQn63RGEiSswMbW+ssIht1MILYlVGkAGGQbkhSh31PCdoUcfiu4apXJoPw==} @@ -821,7 +826,6 @@ packages: '@vue/runtime-core': 3.2.37 '@vue/shared': 3.2.37 csstype: 2.6.19 - dev: true /@vue/server-renderer/3.2.37_vue@3.2.37: resolution: {integrity: sha512-kLITEJvaYgZQ2h47hIzPh2K3jG8c1zCVbp/o/bzQOyvzaKiCquKS7AaioPI28GNxIsE/zSx+EwWYsNxDCX95MA==} @@ -831,11 +835,9 @@ packages: '@vue/compiler-ssr': 3.2.37 '@vue/shared': 3.2.37 vue: 3.2.37 - dev: true /@vue/shared/3.2.37: resolution: {integrity: sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw==} - dev: true /@vue/test-utils/2.0.0_vue@3.2.37: resolution: {integrity: sha512-zL5kygNq7hONrO1CzaUGprEAklAX+pH8J1MPMCU3Rd2xtSYkZ+PmKU3oEDRg8VAGdL5lNJHzDgrud5amFPtirw==} @@ -1286,8 +1288,8 @@ packages: engines: {node: '>=10'} hasBin: true dependencies: - is-text-path: 1.0.1 JSONStream: 1.3.5 + is-text-path: 1.0.1 lodash: 4.17.21 meow: 8.1.2 split2: 3.2.2 @@ -1360,7 +1362,6 @@ packages: /csstype/2.6.19: resolution: {integrity: sha512-ZVxXaNy28/k3kJg0Fou5MiYpp88j7H9hLZp8PDC3jV0WFjfH5E9xHb56L0W59cPbKbcHXeP4qyT8PrHp8t6LcQ==} - dev: true /data-urls/3.0.2: resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} @@ -2291,7 +2292,6 @@ packages: /estree-walker/2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - dev: true /esutils/2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} @@ -3207,7 +3207,6 @@ packages: resolution: {integrity: sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==} dependencies: sourcemap-codec: 1.4.8 - dev: true /make-dir/3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} @@ -3369,7 +3368,6 @@ packages: resolution: {integrity: sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - dev: true /nanoid/3.3.4: resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} @@ -3836,7 +3834,6 @@ packages: /picocolors/1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} - dev: true /picomatch/2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} @@ -3848,6 +3845,24 @@ packages: engines: {node: '>=4'} dev: true + /pinia/2.0.25_khqcrm3nbfs7aq4aok36jjwtje: + resolution: {integrity: sha512-3reAkjJ6bW2D5hZKRMS0c9rUbHVlsVyZd037xO0PJr2AuF/09RRSBnFLlJgmHF4Jx6dEoW/jZBOHTushY7IMlw==} + peerDependencies: + '@vue/composition-api': ^1.4.0 + typescript: '>=4.4.4' + vue: ^2.6.14 || ^3.2.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + typescript: + optional: true + dependencies: + '@vue/devtools-api': 6.4.5 + typescript: 4.5.5 + vue: 3.2.37 + vue-demi: 0.13.11_vue@3.2.37 + dev: false + /pirates/4.0.5: resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==} engines: {node: '>= 6'} @@ -3929,7 +3944,6 @@ packages: nanoid: 3.2.0 picocolors: 1.0.0 source-map-js: 1.0.2 - dev: true /prelude-ls/1.1.2: resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} @@ -4272,7 +4286,6 @@ packages: /source-map-js/1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} - dev: true /source-map-resolve/0.6.0: resolution: {integrity: sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==} @@ -4285,7 +4298,6 @@ packages: /source-map/0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - dev: true /source-map/0.7.3: resolution: {integrity: sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==} @@ -4294,7 +4306,6 @@ packages: /sourcemap-codec/1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} - dev: true /spawn-error-forwarder/1.0.0: resolution: {integrity: sha512-gRjMgK5uFjbCvdibeGJuy3I5OYz6VLoVdsOJdA6wV0WlfQVLFueoqMxwwYD9RODdgb6oUIvlRlsyFSiQkMKu0g==} @@ -4553,7 +4564,6 @@ packages: /to-fast-properties/2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} - dev: true /to-regex-range/5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} @@ -4705,7 +4715,6 @@ packages: resolution: {integrity: sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==} engines: {node: '>=4.2.0'} hasBin: true - dev: true /uglify-js/3.15.0: resolution: {integrity: sha512-x+xdeDWq7FiORDvyIJ0q/waWd4PhjBNOm5dQUOq2AKC0IEjxOS66Ha9tctiVDGcRQuh69K7fgU5oRuTK4cysSg==} @@ -4850,6 +4859,21 @@ packages: - terser dev: true + /vue-demi/0.13.11_vue@3.2.37: + resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + dependencies: + vue: 3.2.37 + dev: false + /vue-eslint-parser/8.2.0_eslint@8.8.0: resolution: {integrity: sha512-hvl8OVT8imlKk/lQyhkshqwQQChzHETcBd5abiO4ePw7ib7QUZLfW+2TUrJHKUvFOCFRJrDin5KJO9OHzB5bRQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4876,7 +4900,6 @@ packages: '@vue/runtime-dom': 3.2.37 '@vue/server-renderer': 3.2.37_vue@3.2.37 '@vue/shared': 3.2.37 - dev: true /w3c-hr-time/1.0.2: resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} diff --git a/src/demo.ts b/src/demo.ts new file mode 100644 index 0000000..204a507 --- /dev/null +++ b/src/demo.ts @@ -0,0 +1,34 @@ +import { Application } from '@feathersjs/feathers'; +import { ref } from 'vue'; + +import { useFeathers } from './useFeathers'; + +type FS = { + users: { + _id: string; + name: string; + }; + messages: { + _id: string; + text: string; + }; +}; + +const feathersMock = { + service: () => ({ + find() {}, + get() {}, + create() {}, + patch() {}, + update() {}, + remove() {}, + on() {}, + off() {}, + }), + on() {}, + off() {}, +} as unknown as Application; + +const app = useFeathers(feathersMock); +const { data } = app('users').get(ref('1')); +const { data } = app('messages').find(); diff --git a/src/index.ts b/src/index.ts index aa8fe73..5190770 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,9 @@ -import useFind, { UseFind, UseFindFunc } from './useFind'; -import useGet, { UseGet, UseGetFunc } from './useGet'; +import { UseFeathers, useFeathers } from './useFeathers'; +import { UseFind, UseFindFunc } from './useFind'; +import { UseGet, UseGetFunc } from './useGet'; import { ServiceModel, ServiceTypes } from './utils'; -export { ServiceModel, ServiceTypes, UseFind, useFind, UseFindFunc, UseGet, useGet, UseGetFunc }; +export { ServiceModel, ServiceTypes, UseFeathers, useFeathers }; + +// legacy +export { UseFind, UseFindFunc, UseGet, UseGetFunc }; diff --git a/src/realtime.ts b/src/realtime.ts new file mode 100644 index 0000000..4a49955 --- /dev/null +++ b/src/realtime.ts @@ -0,0 +1,44 @@ +import { Application, FeathersService } from '@feathersjs/feathers'; + +import { Store } from './store'; +import { getId, ServiceTypes } from './utils'; + +export function loadServiceEventHandlers< + CustomApplication extends Application, + T extends keyof ServiceTypes, + M, +>(service: FeathersService[T]>, store: Store): () => void { + const onCreated = (createdItem: M): void => { + store.setRecord(getId(createdItem), createdItem); + }; + + const onRemoved = (item: M): void => { + store.removeRecord(getId(item)); + }; + + const onItemChanged = (changedItem: M): void => { + store.setRecord(getId(changedItem), changedItem); + }; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + service.on('created', onCreated); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + service.on('removed', onRemoved); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + service.on('patched', onItemChanged); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + service.on('updated', onItemChanged); + + const unloadEventHandlers = () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + service.off('created', onCreated); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + service.off('removed', onRemoved); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + service.off('patched', onItemChanged); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + service.off('updated', onItemChanged); + }; + + return unloadEventHandlers; +} diff --git a/src/store.ts b/src/store.ts new file mode 100644 index 0000000..fa28323 --- /dev/null +++ b/src/store.ts @@ -0,0 +1,65 @@ +import { defineStore } from 'pinia'; +import { computed, reactive, Ref } from 'vue'; + +export interface Store { + getRecord(id: Ref): Ref; + setRecord(id: ID, record: T): void; + removeRecord(id: ID): void; + getRecords(): Ref; + getFilteredRecords(filter: Ref<(record: T) => boolean>): Ref; +} + +export class BasicStore implements Store { + records = reactive>(new Map()); + + setRecord(id: ID, record: T): void { + this.records.set(id, record); + } + + removeRecord(id: ID): void { + this.records.delete(id); + } + + getRecord(id: Ref): Ref { + return computed(() => (id.value ? this.records.get(id.value) : undefined)); + } + + getRecords(): Ref { + return computed(() => Array.from(this.records.values())); + } + + getFilteredRecords(filter: Ref<(record: T) => boolean>): Ref { + return computed(() => Array.from(this.records.values()).filter(filter.value)); + } +} + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export const PiniaStore = (serviceName: string) => { + const s = defineStore(serviceName, () => { + const records = reactive>(new Map()); + + function setRecord(id: ID, record: T): void { + records.set(id, record); + } + + function removeRecord(id: ID): void { + records.delete(id); + } + + function getRecord(id: Ref): Ref { + return computed(() => (id.value ? records.get(id.value) : undefined)); + } + + function getRecords(): Ref { + return computed(() => Array.from(records.values())); + } + + function getFilteredRecords(filter: Ref<(record: T) => boolean>): Ref { + return computed(() => Array.from(records.values()).filter(filter.value)); + } + + return { records, recordsList: getRecords(), setRecord, removeRecord, getRecord, getRecords, getFilteredRecords }; + }); + + return s; +}; diff --git a/src/useFeathers.ts b/src/useFeathers.ts new file mode 100644 index 0000000..d5456a9 --- /dev/null +++ b/src/useFeathers.ts @@ -0,0 +1,65 @@ +import { Application, FeathersService } from '@feathersjs/feathers'; +import { reactive } from 'vue'; + +import { loadServiceEventHandlers } from './realtime'; +import { PiniaStore, Store } from './store'; +import useFind, { UseFindFunc } from './useFind'; +import useGet, { UseGetFunc } from './useGet'; +import { ServiceModel, ServiceTypes } from './utils'; + +type Service = { + find: UseFindFunc; + get: UseGetFunc; + create: FeathersService['create']; + update: FeathersService['update']; + patch: FeathersService['patch']; + remove: FeathersService['remove']; +}; + +export type UseFeathers = < + T extends keyof ServiceTypes, + M = ServiceModel, +>( + serviceName: T, +) => Service; + +const loadedServices = reactive(new Map>()); + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export function useFeathers( + feathers: CustomApplication, + createStore?: , S = ServiceTypes[T]>( + serviceName: T, + ) => Store, +) { + type ServiceName = keyof ServiceTypes; + + return , M = ServiceModel>( + _serviceName: T, + ): Service => { + const serviceName = _serviceName as keyof ServiceName; // TODO: fix this + + // reuse existing service + if (loadedServices.has(serviceName)) { + return loadedServices.get(serviceName) as Service; + } + + const service = feathers.service(serviceName); + const store = createStore ? createStore(serviceName) : PiniaStore(serviceName)(); + + loadServiceEventHandlers(service, store); + + const _service = { + find: useFind(feathers, store, serviceName) as UseFindFunc, + get: useGet(feathers, store, serviceName) as UseGetFunc, + create: service.create.bind(service), + patch: service.patch.bind(service), + update: service.update.bind(service), + remove: service.remove.bind(service), + }; + + loadedServices.set(serviceName, _service); + + return _service; + }; +} diff --git a/src/useFind.ts b/src/useFind.ts index 0255ca7..fa2c6b4 100644 --- a/src/useFind.ts +++ b/src/useFind.ts @@ -1,76 +1,10 @@ -import { AdapterService } from '@feathersjs/adapter-commons/lib'; import type { FeathersError } from '@feathersjs/errors'; -import type { Application, FeathersService, Paginated, Params, Query, ServiceMethods } from '@feathersjs/feathers'; +import type { Application, Params, ServiceMethods } from '@feathersjs/feathers'; import sift from 'sift'; -import { getCurrentInstance, onBeforeUnmount, Ref, ref, watch } from 'vue'; +import { computed, getCurrentInstance, onBeforeUnmount, Ref, ref, watch } from 'vue'; -import { getId, isPaginated, ServiceModel, ServiceTypes } from './utils'; - -function loadServiceEventHandlers< - CustomApplication extends Application, - T extends keyof ServiceTypes, - M, ->( - service: FeathersService[T]>, - params: Ref, - data: Ref, -): () => void { - const onCreated = (createdItem: M): void => { - // ignore items not matching the query or when no params are set - if (!params.value || (params.value.query !== undefined && !sift(params.value.query)(createdItem))) { - return; - } - - // ignore items that already exist - if (data.value.find((item) => getId(createdItem) === getId(item)) !== undefined) { - return; - } - - data.value = [...data.value, createdItem]; - }; - - const onRemoved = (item: M): void => { - data.value = data.value.filter((_item) => getId(_item) !== getId(item)); - }; - - const onItemChanged = (changedItem: M): void => { - // ignore items not matching the query or when no params are set - if (!params.value || (params.value.query !== undefined && !sift(params.value.query)(changedItem))) { - // remove item from the list if they have been on it before - data.value = data.value.filter((item) => getId(item) !== getId(changedItem)); - return; - } - - const itemIndex = data.value.findIndex((item) => getId(item) === getId(changedItem)); - if (itemIndex === -1) { - data.value = [...data.value, changedItem]; - } else { - data.value = [...data.value.slice(0, itemIndex), changedItem, ...data.value.slice(itemIndex + 1)]; - } - }; - - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - service.on('created', onCreated); - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - service.on('removed', onRemoved); - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - service.on('patched', onItemChanged); - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - service.on('updated', onItemChanged); - - const unloadEventHandlers = () => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - service.off('created', onCreated); - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - service.off('removed', onRemoved); - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - service.off('patched', onItemChanged); - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - service.off('updated', onItemChanged); - }; - - return unloadEventHandlers; -} +import { Store } from './store'; +import { getId, ServiceModel, ServiceTypes } from './utils'; export type UseFind = { data: Ref; @@ -80,87 +14,45 @@ export type UseFind = { }; // TODO: workaround, since extracting the type with ReturnType does not work for generic functions. See https://stackoverflow.com/a/52964723 -export type UseFindFunc = < - T extends keyof ServiceTypes, - M = ServiceModel, ->( - serviceName: T, +export type UseFindFunc = ( params?: Ref, + options?: { disableUnloadingEventHandlers: boolean }, ) => UseFind; -type Options = { - disableUnloadingEventHandlers: boolean; - loadAllPages: boolean; -}; - -const defaultOptions: Options = { disableUnloadingEventHandlers: false, loadAllPages: false }; - -export default (feathers: CustomApplication) => - , M = ServiceModel>( +export default < + CustomApplication extends Application, + T extends keyof ServiceTypes, + M = ServiceModel, + >( + feathers: CustomApplication, + store: Store, serviceName: T, + ) => + ( params: Ref = ref({ paginate: false, query: {} }), - options: Partial = {}, + options = { disableUnloadingEventHandlers: false }, ): UseFind => { - const { disableUnloadingEventHandlers, loadAllPages } = { ...defaultOptions, ...options }; - // type cast is fine here (source: https://github.com/vuejs/vue-next/issues/2136#issuecomment-693524663) - const data = ref([]) as Ref; const isLoading = ref(false); const error = ref(); const service = feathers.service(serviceName as string); - const unloadEventHandlers = loadServiceEventHandlers(service, params, data); - let unloaded = false; - - const currentFindCall = ref(0); - const find = async (call: number) => { + const find = async () => { isLoading.value = true; error.value = undefined; if (!params.value) { - data.value = []; isLoading.value = false; return; } try { - const originalParams: Params = params.value; - const originalQuery: Query & { $limit?: number } = originalParams.query || {}; // TODO: the typecast below is necessary due to the prerelease state of feathers v5. The problem there is // that the AdapterService interface is not yet updated and is not compatible with the ServiceMethods interface. - const res = await (service as unknown as ServiceMethods | AdapterService).find(originalParams); - if (call !== currentFindCall.value) { - // stop handling response since there already is a new find call running within this composition - return; - } - if (isPaginated(res) && !loadAllPages) { - data.value = [...res.data]; - } else if (!isPaginated(res)) { - data.value = Array.isArray(res) ? res : [res]; - } else { - // extract data from page response - let loadedPage: Paginated = res; - let loadedItemsCount = loadedPage.data.length; - data.value = [...loadedPage.data]; - // limit might not be specified in the original query if default pagination from backend is applied, that's why we use this fallback pattern - const limit: number = originalQuery.$limit || loadedPage.data.length; - // if chunking is enabled we go on requesting all following pages until all data have been received - while (!unloaded && loadedPage.total > loadedItemsCount) { - // skip can be a string in cases where key based chunking/pagination is done e.g. in DynamoDb via `LastEvaluatedKey` - const skip: string | number = - typeof loadedPage.skip === 'string' ? loadedPage.skip : loadedPage.skip + limit; - // request next page - loadedPage = (await (service as unknown as ServiceMethods | AdapterService).find({ - ...originalParams, - query: { ...originalQuery, $skip: skip, $limit: limit }, - })) as Paginated; - if (call !== currentFindCall.value) { - // stop handling/requesting further pages since there already is a new find call running within this composition - return; - } - loadedItemsCount += loadedPage.data.length; - data.value = [...data.value, ...loadedPage.data]; - } + const res = await (service as unknown as ServiceMethods).find(params.value); + const items = Array.isArray(res) ? res : [res]; + for (const item of items) { + store.setRecord(getId(item), item); } } catch (_error) { error.value = _error as FeathersError; @@ -170,22 +62,24 @@ export default (feathers: CustomApplicati }; const load = () => { - currentFindCall.value = currentFindCall.value + 1; - void find(currentFindCall.value); + void find(); }; const unload = () => { - unloaded = true; - unloadEventHandlers(); feathers.off('connect', load); }; watch(params, load, { immediate: true }); feathers.on('connect', load); - if (disableUnloadingEventHandlers === false && getCurrentInstance()) { + if (options.disableUnloadingEventHandlers === false && getCurrentInstance()) { onBeforeUnmount(unload); } + const filter = computed( + () => (record: M) => !params.value || (params.value.query !== undefined && !sift(params.value.query)(record)), + ); + const data = store.getFilteredRecords(filter); + return { data, isLoading, unload, error }; }; diff --git a/src/useGet.ts b/src/useGet.ts index 3d5b475..b0c2678 100644 --- a/src/useGet.ts +++ b/src/useGet.ts @@ -1,59 +1,10 @@ import type { FeathersError } from '@feathersjs/errors'; -import type { Application, FeathersService, Id, Params, ServiceMethods } from '@feathersjs/feathers'; +import type { Application, Id, Params, ServiceMethods } from '@feathersjs/feathers'; import { getCurrentInstance, onBeforeUnmount, Ref, ref, watch } from 'vue'; +import { Store } from './store'; import { getId, ServiceModel, ServiceTypes } from './utils'; -function loadServiceEventHandlers< - CustomApplication extends Application, - T extends keyof ServiceTypes, - M, ->( - service: FeathersService[T]>, - _id: Ref, - data: Ref, -): () => void { - const onCreated = (item: M): void => { - if (_id.value === getId(item)) { - data.value = item; - } - }; - - const onRemoved = (item: M): void => { - if (_id.value === getId(item)) { - data.value = undefined; - } - }; - - const onItemChanged = (item: M): void => { - if (_id.value === getId(item)) { - data.value = item; - } - }; - - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - service.on('created', onCreated); - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - service.on('removed', onRemoved); - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - service.on('patched', onItemChanged); - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - service.on('updated', onItemChanged); - - const unloadEventHandlers = () => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - service.off('created', onCreated); - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - service.off('removed', onRemoved); - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - service.off('patched', onItemChanged); - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - service.off('updated', onItemChanged); - }; - - return unloadEventHandlers; -} - export type UseGet = { data: Ref; isLoading: Ref; @@ -62,35 +13,36 @@ export type UseGet = { }; // TODO: workaround, since extracting the type with ReturnType does not work for generic functions. See https://stackoverflow.com/a/52964723 -export type UseGetFunc = < - T extends keyof ServiceTypes, - M = ServiceModel, ->( - serviceName: T, +export type UseGetFunc = ( _id: Ref, + params?: Ref, + options?: { disableUnloadingEventHandlers: boolean }, ) => UseGet; -export default (feathers: CustomApplication) => - , M = ServiceModel>( +export default < + CustomApplication extends Application, + T extends keyof ServiceTypes, + M = ServiceModel, + >( + feathers: CustomApplication, + store: Store, serviceName: T, + ) => + ( _id: Ref, params: Ref = ref(), { disableUnloadingEventHandlers } = { disableUnloadingEventHandlers: false }, ): UseGet => { - const data = ref(); const isLoading = ref(false); const error = ref(); const service = feathers.service(serviceName as string); - const unloadEventHandlers = loadServiceEventHandlers(service, _id, data); - const get = async () => { isLoading.value = true; error.value = undefined; if (!_id.value) { - data.value = undefined; isLoading.value = false; return; } @@ -98,7 +50,8 @@ export default (feathers: CustomApplicati try { // TODO: the typecast below is necessary due to the prerelease state of feathers v5. The problem there is // that the AdapterService interface is not yet updated and is not compatible with the ServiceMethods interface. - data.value = await (service as unknown as ServiceMethods).get(_id.value, params.value); + const item = await (service as unknown as ServiceMethods).get(_id.value, params.value); + store.setRecord(getId(item), item); } catch (_error) { error.value = _error as FeathersError; } @@ -111,7 +64,6 @@ export default (feathers: CustomApplicati }; const unload = () => { - unloadEventHandlers(); feathers.off('connect', load); }; @@ -122,5 +74,7 @@ export default (feathers: CustomApplicati onBeforeUnmount(unload); } + const data = store.getRecord(_id); + return { isLoading, data, error, unload }; }; diff --git a/src/utils.ts b/src/utils.ts index e32ccd5..1303e27 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ -import type { AdapterService } from '@feathersjs/adapter-commons'; +// import type { AdapterService } from '@feathersjs/adapter-commons'; import type { Application, Id, Paginated, ServiceMethods } from '@feathersjs/feathers'; export type PotentialIds = { @@ -6,12 +6,13 @@ export type PotentialIds = { _id?: Id; }; -export function getId(item: PotentialIds): Id { +export function getId(_item: unknown): Id { + const item: PotentialIds = _item as PotentialIds; if (item.id) { - return item.id; + return item.id.toString(); } if (item._id) { - return item._id; + return item._id.toString(); } throw new Error('Unable to retrieve id from item'); } @@ -24,11 +25,7 @@ export type ServiceTypes = CustomApplication extends Applicat export type ServiceModel< CustomApplication, T extends keyof ServiceTypes, -> = ServiceTypes[T] extends AdapterService - ? M1 - : ServiceTypes[T] extends ServiceMethods - ? M2 - : never; +> = ServiceTypes[T] extends ServiceMethods ? M2 : never; export function isPaginated(response: T | T[] | Paginated): response is Paginated { const { total, limit, skip, data } = response as Paginated;