├── .eslintrc ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .markdownlint.jsonc ├── .npmrc ├── .nvmrc ├── .vscode └── settings.json ├── README.md ├── __unconfig_vite.config.ts ├── cspell.json ├── docs ├── .vitepress │ └── config.js ├── components │ ├── Badge.vue │ └── BlockQuote.vue ├── data-stores │ ├── index.md │ ├── instances.md │ └── querying-data.md ├── feathers-pinia-bird.svg ├── guide │ ├── auth-stores.md │ ├── auto-imports.md │ ├── base-model.md │ ├── common-patterns.md │ ├── create-pinia-client.md │ ├── custom-query-filters.md │ ├── data-modeling.md │ ├── handle-clones.md │ ├── hooks.md │ ├── index.md │ ├── model-classes.md │ ├── model-instances.md │ ├── module-overview.md │ ├── nuxt-module.md │ ├── ofetch.md │ ├── service-stores.md │ ├── setup.md │ ├── storage-sync.md │ ├── store-associated.md │ ├── troubleshooting.md │ ├── use-auth.md │ ├── use-backup.md │ ├── use-clones.md │ ├── use-find.md │ ├── use-get.md │ ├── use-instance-defaults.md │ ├── use-pagination.md │ ├── utilities.md │ ├── whats-new-v3.md │ └── whats-new.md ├── index.md ├── migrate │ ├── from-feathers-vuex.md │ ├── from-v0.md │ ├── handle-clones.md │ └── models.md ├── partials │ ├── assess-your-auth-risk.md │ ├── auto-imports-overview.md │ ├── notification-access-token.md │ ├── notification-feathers-client.md │ ├── nuxt-config.md │ ├── nuxt-feathers-client-example.md │ ├── patch-diffing.md │ └── service-interface.md ├── services │ ├── hybrid-queries.md │ ├── index.md │ ├── instances.md │ ├── stores.md │ ├── use-find.md │ └── use-get.md ├── setup │ ├── example-apps.md │ ├── index.md │ ├── install.md │ ├── nuxt3.md │ ├── other.md │ ├── quasar.md │ └── vite.md └── styles.postcss ├── index.html ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── favicon.ico └── training │ ├── declarative-vs-imperative.md │ ├── model-associations.md │ ├── model-instances.md │ ├── modeling.md │ └── pinia-stores.md ├── src ├── composables │ ├── index.ts │ └── use-backup.ts ├── create-pinia-client.ts ├── create-pinia-service.ts ├── custom-operators │ ├── fuzzy-search-with-ufuzzy.ts │ ├── index.ts │ └── sql-like.ts ├── feathers-ofetch.ts ├── hooks │ ├── 0-prepare-query.ts │ ├── 1-set-pending.ts │ ├── 2-event-locks.ts │ ├── 3-sync-store.ts │ ├── 4-model-instances.ts │ ├── 5-handle-find-ssr.ts │ ├── 6-normalize-find.ts │ ├── 7-skip-get-if-exists.ts │ ├── 8-patch-diffs.ts │ ├── 9-ssr-qid-cache.ts │ └── index.ts ├── index.ts ├── isomorphic-mongo-objectid.d.ts ├── localstorage │ ├── clear-storage.ts │ ├── index.ts │ └── storage-sync.ts ├── modeling │ ├── index.ts │ ├── store-associated.ts │ ├── types.ts │ ├── use-feathers-instance.ts │ └── use-model-instance.ts ├── stores │ ├── all-storage-types.ts │ ├── clones.ts │ ├── event-locks.ts │ ├── event-queue-promise.ts │ ├── events.ts │ ├── filter-query.ts │ ├── index.ts │ ├── local-queries.ts │ ├── pagination.ts │ ├── pending.ts │ ├── ssr-query-cache.ts │ ├── storage.ts │ ├── temps.ts │ ├── types.ts │ ├── use-data-store.ts │ └── use-service-store.ts ├── types.ts ├── ufuzzy.ts ├── unplugin-auto-import-preset.ts ├── use-auth │ ├── index.ts │ └── use-auth.ts ├── use-find-get │ ├── index.ts │ ├── types.ts │ ├── use-find.ts │ ├── use-get.ts │ ├── utils-pagination.ts │ └── utils.ts └── utils │ ├── convert-data.ts │ ├── deep-unref.ts │ ├── define-properties.ts │ ├── index.ts │ ├── service-utils.ts │ ├── use-counter.ts │ ├── use-instance-defaults.ts │ └── utils.ts ├── tailwind.config.cjs ├── tests ├── composables │ └── use-backup.test.ts ├── fixtures │ ├── data.ts │ ├── feathers.ts │ ├── index.ts │ └── schemas │ │ ├── authors.ts │ │ ├── comments.ts │ │ ├── contacts.ts │ │ ├── index.ts │ │ ├── posts.ts │ │ ├── tasks.ts │ │ └── users.ts ├── instance-api │ ├── instance-clones.test.ts │ ├── instance-defaults.test.ts │ ├── instance-patch-diffing.test.ts │ └── instance-temps.test.ts ├── localstorage │ ├── clear-storage.test.ts │ └── storage-sync.test.ts ├── modeling │ ├── feathers-instances.test.ts │ ├── push-to-store.test.ts │ ├── store-associated.test.ts │ ├── use-data-store-modeling.test.ts │ └── use-feathers-instance.test.ts ├── stores │ ├── events.test.ts │ ├── use-data-clones.test.ts │ ├── use-data-local-custom-filters.test.ts │ ├── use-data-local-custom-operators.test.ts │ ├── use-data-local.test.ts │ ├── use-data-storage.test.ts │ ├── use-data-store-getters-whitelist.test.ts │ ├── use-data-store-storage.test.ts │ ├── use-data-store.test.ts │ └── use-service-store.test.ts ├── test-utils.ts ├── use-auth │ └── use-auth.test.ts ├── use-find-get │ ├── use-find-client.test.ts │ ├── use-find-hybrid.test.ts │ ├── use-find-server.test.ts │ ├── use-find.test.ts │ ├── use-get.test.ts │ └── utils-pagination.test.ts └── vue-service │ ├── base-methods-untyped.test.ts │ ├── base-methods.test.ts │ ├── custom-methods.test.ts │ ├── custom-store.test.ts │ ├── pinia-service.test.ts │ └── service-models.dates.test.ts ├── tsconfig.json └── vite.config.ts /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@antfu" 3 | } -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 16 | - uses: actions/checkout@v2 17 | - name: Install modules 18 | run: npm i 19 | - name: Run tests 20 | run: npm test 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | 7 | coverage 8 | __unconfig_vite.config.ts 9 | /docs/.vitepress/cache 10 | /.VSCodeCounter 11 | -------------------------------------------------------------------------------- /.markdownlint.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "MD013": false, // allow line-length longer than 80 3 | "MD033": false, // allow inline html 4 | "MD041": false // allow file to start with something other than H1 5 | } 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | legacy-peer-deps=true 3 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.20.0 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.rulers": [ 3 | 120 4 | ], 5 | "explorer.autoRevealExclude": { 6 | // "**/node_modules": false 7 | }, 8 | "vitest.debugExclude": [ 9 | "/**", 10 | "**/node_modules/**" 11 | ], 12 | "prettier.enable": false, 13 | "editor.formatOnSave": false, 14 | "[markdown]": { 15 | "editor.formatOnSave": true 16 | }, 17 | "editor.codeActionsOnSave": { 18 | "source.fixAll.eslint": "explicit", 19 | "source.organizeImports": "never" 20 | }, 21 | // The following is optional. 22 | // It's better to put under project setting `.vscode/settings.json` 23 | // to avoid conflicts with working with different eslint configs 24 | // that does not support all formats. 25 | "eslint.validate": [ 26 | "javascript", 27 | "javascriptreact", 28 | "typescript", 29 | "typescriptreact", 30 | "vue", 31 | "html", 32 | "markdown", 33 | "json", 34 | "jsonc", 35 | "yaml" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Connect your Feathers API to the elegant data store for Vue 2 | 3 | 4 | Feathers Pinia Website Hero 5 | 6 | 7 | [Read the Documentation](https://feathers-pinia.pages.dev) 8 | -------------------------------------------------------------------------------- /__unconfig_vite.config.ts: -------------------------------------------------------------------------------- 1 | 2 | let __unconfig_data; 3 | let __unconfig_stub = function (data = {}) { __unconfig_data = data }; 4 | __unconfig_stub.default = (data = {}) => { __unconfig_data = data }; 5 | /// 6 | import { defineConfig } from 'vite' 7 | import path from 'path' 8 | import vue from '@vitejs/plugin-vue' 9 | import dts from 'vite-plugin-dts' 10 | 11 | // https://vitejs.dev/config/ 12 | const __unconfig_default = defineConfig({ 13 | plugins: [vue(), dts()], 14 | server: { 15 | hmr: { 16 | port: parseInt(process.env.KUBERNETES_SERVICE_PORT, 10) || 3000, 17 | }, 18 | }, 19 | build: { 20 | lib: { 21 | entry: path.resolve(__dirname, 'src/index.ts'), 22 | name: 'feathersPinia', 23 | fileName: 'feathers-pinia', 24 | }, 25 | sourcemap: true, 26 | rollupOptions: { 27 | // make sure to externalize deps that shouldn't be bundled 28 | // into your library 29 | external: [ 30 | 'vue-demi', 31 | 'vue', 32 | 'pinia', 33 | 'lodash', 34 | 'sift', 35 | '@feathersjs/commons', 36 | '@feathersjs/errors', 37 | '@feathersjs/adapter-commons', 38 | ], 39 | output: { 40 | // Provide global variables to use in the UMD build 41 | // for externalized deps 42 | globals: { 43 | 'vue-demi': 'VueDemi', 44 | vue: 'Vue', 45 | pinia: 'pinia', 46 | lodash: 'lodash', 47 | sift: 'sift', 48 | }, 49 | }, 50 | }, 51 | }, 52 | test: { 53 | globals: true, 54 | }, 55 | }) 56 | 57 | if (typeof __unconfig_default === "function") __unconfig_default(...[{"command":"serve","mode":"development"}]);export default __unconfig_data; -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "ignorePaths": [], 4 | "dictionaryDefinitions": [], 5 | "language": "en,fr", 6 | "dictionaries": [], 7 | "words": [ 8 | "automagically", 9 | "bson", 10 | "feathersjs", 11 | "featureful", 12 | "gzipped", 13 | "Nuxt", 14 | "Pinia", 15 | "stylesheet", 16 | "swrv", 17 | "Todos", 18 | "Vuex", 19 | "socketio", 20 | "typeof", 21 | "Vetur" 22 | ], 23 | "ignoreWords": [ 24 | "findinstore", 25 | "addtostore", 26 | "keyup", 27 | "removefromstore", 28 | "useget", 29 | "countinstore", 30 | "getfromstore", 31 | "amogh", 32 | "Amogh", 33 | "Palnitkar", 34 | "Deku" 35 | ], 36 | "import": ["@cspell/dict-fr-fr/cspell-ext.json"] 37 | } 38 | -------------------------------------------------------------------------------- /docs/components/Badge.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 24 | 25 | 59 | -------------------------------------------------------------------------------- /docs/components/BlockQuote.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 30 | -------------------------------------------------------------------------------- /docs/guide/auto-imports.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 8 | 9 | # Auto-Imports 10 | 11 | [[toc]] 12 | 13 | Auto-Imports are amazing!. 🎉 They keep code clean and decoupled. As an added bonus, you no longer have to manually 14 | import modules at the top of every file. Feathers-Pinia comes with auto-import modules targeted at improving developer 15 | experience. 16 | 17 | This page shows how to set up auto-imports for Single Page Apps, followed by an overview of the available 18 | auto-imports. The [Nuxt module](/guide/nuxt-module) documentation shows how to install Nuxt SSR-friendly versions of 19 | these same utilities. 20 | 21 | ## Preset for `unplugin-auto-import` 22 | 23 | Feathers-Pinia v3 includes a preset for `unplugin-auto-import`, a plugin which enables auto-import for Vite, Rollup, 24 | Webpack, Quasar, and more. [See setup instructions for your environment](https://github.com/antfu/unplugin-auto-import). 25 | 26 | Once you've installed `unplugin-auto-import`, you can use the `feathersPiniaAutoImport` preset. Here is a truncated 27 | example of a `vite.config.ts` file: 28 | 29 | ```ts{4,22} 30 | import { defineConfig } from 'vite' 31 | import Vue from '@vitejs/plugin-vue' 32 | import AutoImport from 'unplugin-auto-import/vite' 33 | import { feathersPiniaAutoImport } from 'feathers-pinia' 34 | 35 | // https://vitejs.dev/config/ 36 | export default defineConfig({ 37 | plugins: [ 38 | Vue({ 39 | reactivityTransform: true, 40 | }), 41 | 42 | // https://github.com/antfu/unplugin-auto-import 43 | AutoImport({ 44 | imports: [ 45 | 'vue', 46 | 'vue-router', 47 | 'vue-i18n', 48 | 'vue/macros', 49 | '@vueuse/head', 50 | '@vueuse/core', 51 | feathersPiniaAutoImport, 52 | ], 53 | dts: 'src/auto-imports.d.ts', 54 | dirs: ['src/composables'], 55 | vueTemplate: true, 56 | }), 57 | ], 58 | }) 59 | ``` 60 | 61 | To enable custom auto-import folders, use the `dirs` option, shown above. 62 | 63 |
64 | You have to start (and sometimes restart) the dev server for new auto-imports to become available. 65 |
66 | 67 |
68 | 69 | For Nuxt apps, use the [Nuxt Module](./nuxt-module.md). 70 | 71 |
72 | 73 | 74 | -------------------------------------------------------------------------------- /docs/guide/base-model.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | 8 | 9 | # BaseModel 10 | 11 | [[toc]] 12 | 13 | BaseModel has been replaced by the [FeathersPiniaService api](/services/). 14 | -------------------------------------------------------------------------------- /docs/guide/handle-clones.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | # `handleClones` is now `params.clones` 6 | 7 | In 3.0 `handleClones` was removed and [replaced by `params.clones`](/migrate/handle-clones). 8 | -------------------------------------------------------------------------------- /docs/guide/hooks.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | 9 | 10 | # Hooks 11 | 12 | [[toc]] 13 | 14 | Feathers-Pinia 2.0 achieves cleaner decoupling between the Models and stores by utilizing Feathers Client hooks. The 15 | hooks are only required when using Feathers service connectivity. 16 | 17 | ## Registering Hooks 18 | 19 | Since all hooks are required, a utility named `feathersPiniaHooks` is provided for registering the hooks in the correct 20 | order. 21 | 22 | ```ts 23 | import { feathersPiniaHooks } from 'feathers-pinia' 24 | 25 | service.hooks({ around: { all: [...feathersPiniaHooks(Model)] } }) 26 | ``` 27 | 28 | The utility requires that you pass a Model function then spread the returned array into the `around all` hooks, as 29 | demonstrated in more detail, below. Because they are Model-specific, **hooks must be registered as service-level 30 | hooks**, not app-level hooks. 31 | 32 | 33 | 34 | ```ts 35 | import type { Tasks, TasksData, TasksQuery } from 'my-feathers-api' 36 | import { type ModelInstance, feathersPiniaHooks, useFeathersModel, useInstanceDefaults } from 'feathers-pinia' 37 | import { api } from '../feathers' 38 | 39 | const service = api.service('tasks') 40 | 41 | function modelFn(data: ModelInstance) { 42 | const withDefaults = useInstanceDefaults({ description: 'default', isComplete: false }, data) 43 | return withDefaults 44 | } 45 | const Task = useFeathersModel( 46 | { name: 'Task', idField: '_id', service }, 47 | modelFn, 48 | ) 49 | 50 | // register hooks in the `around all` array 51 | service.hooks({ around: { all: [...feathersPiniaHooks(Task)] } }) 52 | ``` 53 | 54 | ## Overview of Hooks 55 | 56 | These are the hooks that come with Feathers Pinia, in order of registration. These are `around` hooks, so they execute 57 | in reverse order during responses. 58 | 59 | - `setPending(store)` controls pending state for all request methods. 60 | - `eventLocks(store)` controls event locks to prevent potential duplicate response/events with `patch` and `remove`. 61 | - `syncStore(store)` keeps the store in sync with requested data. Allows skipping the store sync with the `skipStore` 62 | param. 63 | - `makeModelInstances(Model)` turns data from API responses into modeled data. 64 | - `handleFindSsr(store)` handles data for request state transferred from an SSR server, if enabled. 65 | - `normalizeFind()` takes care of normalizing pagination params for some feathers adapters. 66 | - `skipGetIfExists(store)` prevents get requests when the `skipGetIfExists` option is enabled. 67 | - `patchDiffing(store)` saves bandwidth by diffing clones with original records and only sends the top-level keys that 68 | have changed. 69 | -------------------------------------------------------------------------------- /docs/guide/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | # Introduction 6 | 7 | [[toc]] 8 | 9 | Welcome to the apex of Vue Data Modeling and FeathersJS connectivity for the artisan developer. Feathers-Pinia is 10 | **the** first-class data modeling solution built with for Vue Composition API. It is well known for its features which 11 | maximize perceived application speed for end users while providing a pleasant developer experience. 12 | 13 | ## Overview of Features 14 | 15 | - Implicit, Functional Data Modeling maintains your data structure, requires no setup, and is 16 | fully customizable. 17 | - **[Clone and Commit](/guide/common-patterns#mutation-multiplicity-pattern)** dramatically reduces the need for custom 18 | Pinia actions. 19 | - **[Per-Record Defaults](/guide/common-patterns#useinstancedefaults)** offer a functional way of adding default 20 | values and methods to every record. 21 | - **Realtime by Default**: It's ready for WebSocket-enhanced, multi-user interactivity. 22 | - **Independent Reactivity**: no need to assign records to component or store `data` to enable reactive binding. 23 | - **[Local Queries](/data-stores/querying-data)**: Make requests against locally-cached data as though it was a FeathersJS 24 | database, **now with support for SQL `$like` operators.** 25 | - **[Live Queries](/guide/common-patterns.html#reactive-lists-with-live-queries)** with client-side pagination allow 26 | arrays of data to automatically update as new records are added, modified, or removed. 27 | - **[Server-Side Pagination](/services/use-find#server-paging-auto-fetch)**: alternative to live-list pagination, 28 | optionally give all control to the server and perform manual fetching. In this mode, lists only update when new queries 29 | are made. 30 | - **[Super-Efficient SSR](/guide/whats-new#super-efficient-ssr)**: optimize server-loaded data on the client without 31 | refetching. The latest Nuxt APIs are fully supported. 32 | - **[Fall-Through Cache](/services/use-find)** like SWR but with built-in, low-memory query intelligence. It knows which 33 | records can be shared between different queries, which allows relevant records to show immediately while additional data 34 | is fetched. 35 | - Flexible code patterns allow developers to work as they wish. 36 | - **Active Record Pattern**: allows use of utility methods built on each instance. This pattern allows creation of 37 | loosely-coupled components built around the instance interface. 38 | - **Data Mapper Pattern**: allows you to use a store-centric workflow where you let store logic perform operations 39 | on your data. 40 | - **[Flexible Auth Support](/guide/use-auth)** with the new `useAuth` composition utility. 41 | API. 42 | - Full support for [FeathersJS v5 Dove](https://feathersjs.com) and earlier versions of Feathers. 43 | 44 | ## Coming from Feathers-Vuex 45 | 46 | Feathers-Pinia is the next generation of [Feathers-Vuex](https://vuex.feathersjs.com). The difference is that it's built on [Pinia](https://pinia.esm.dev/): a Vue store with an intuitive API. 47 | 48 | Using Pinia in your apps will have a few positive effects: 49 | 50 | - The clean API requires lower mental overhead to use. 51 | - No more weird Vuex syntax. 52 | - No more mutations; just actions. 53 | - Use Composable Stores instead of injected rootState, rootGetters, etc. 54 | - Lower mental overhead means developers spend more time in a creative space. This usually results in an increase of productivity. 55 | - You'll have smaller bundle sizes. Not only is Pinia tiny, it's also modular. You don't have to register all of the plugins in a central store. Pinia's architecture enables tree shaking, so only the services needed for the current view need to load. 56 | 57 | See the Migration Guide for developers [coming from Feathers-Vuex](/migrate/from-feathers-vuex). 58 | -------------------------------------------------------------------------------- /docs/guide/module-overview.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | 9 | 10 | # Module Overview 11 | 12 | [[toc]] 13 | 14 | ## Setup & Store Creation 15 | 16 | These are the primary utilities for creating stores. 17 | 18 | ```ts 19 | // Setup & Store Creation 20 | export { createPiniaClient } from './create-pinia-client' 21 | export { OFetch } from './feathers-ofetch' 22 | export { useInstanceDefaults } from './utils' 23 | ``` 24 | 25 | - [createPiniaClient](/guide/create-pinia-client) wraps the Feathers Client in a Feathers-Pinia client. 26 | - [OFetch](/guide/ofetch) is a special fetch adapter for SSR applications. 27 | -------------------------------------------------------------------------------- /docs/guide/nuxt-module.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | 9 | 10 | # Nuxt Module 11 | 12 | [[toc]] 13 | 14 | Feathers-Pinia v3 comes with a Nuxt Module. It provides two main features: 15 | 16 | ## Installation 17 | 18 | Install the Nuxt module [from npm](https://npmjs.com/package/nuxt-feathers-pinia): 19 | 20 | ```bash 21 | npm i nuxt-feathers-pinia 22 | ``` 23 | 24 | Once installed, add its name to the `nuxt.config.ts` file. You can optionally use the `dirs` key to enable auto-imports 25 | in other directories (in addition to `/composables`.) 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /docs/guide/ofetch.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | 9 | 10 | # OFetch SSR Adapter for Feathers-Rest 11 | 12 | [[toc]] 13 | 14 | The Nuxt team created a truly universal `fetch` API in the form of [ofetch](https://github.com/unjs/ofetch). It's a 15 | great replacement for [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) in the browser and Node's 16 | [undici](https://www.npmjs.com/package/undici) on the server. It has a slightly different response API, eliminating the 17 | need to `await` the response then also `await` the format (`.text` or `.json`). 18 | 19 | Since the API is slightly different than native browser fetch API, we've made a custom adapter for Feathers-Rest. 20 | 21 | ## Setup 22 | 23 | Follow these setups to get the `OFetch` adapter working with your Nuxt app and the Feathers Client. 24 | 25 | ## Install `ofetch` 26 | 27 | The `ofetch` adapter fulfills the promise of the `fetch` API, being a universal client that works on client, server, and in serverless environments. Install it with the following command. Note that you can put it in `devDependencies` since Nuxt makes a clean, standalone version of your project during build. 28 | 29 | ```bash 30 | npm i ofetch -D 31 | ``` 32 | 33 | ## Add to Feathers Client 34 | 35 | Here's an example of setting up the OFetch adapter to work with Feathers-Client in a Nuxt 3 plugin: 36 | 37 | -------------------------------------------------------------------------------- /docs/guide/storage-sync.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | 9 | 10 | # Sync with Storage 11 | 12 | Improve perceived app speed by storing some data client-side. 13 | 14 | [[toc]] 15 | 16 | A super-lightweight utility for syncing with `localStorage` (or any `Storage` interface) is built into Feathers-Pinia. 17 | The internal `syncWithStorage` utility watches for changes in specified keys in the store and writes the changes to 18 | localStorage. Other features include: 19 | 20 | - Can be enabled for the entire `api` instance or individual services. 21 | - Only caches `itemsById` and `pagination` attributes, by default. 22 | - Allows passing custom keys to sync. This is globally configurable or customizable per service. 23 | 24 | The typical use case for this would be to speed up the perceived speed of Single Page Applications. The data hydrates 25 | so quickly that a SPA will feel like a server-rendered application. After any write to the store, the provided keys will 26 | be serialized into `localStorage` after a 500ms period of inactivity. 27 | 28 | ## Examples 29 | 30 | ### Sync All Service Data 31 | 32 | Here's how to set it up for the entire Feathers-Pinia client instance: 33 | 34 | ```ts 35 | export const api = createPiniaClient(feathersClient, { 36 | idField: '_id', 37 | pinia, 38 | syncWithStorage: true, 39 | storage: window.localStorage, 40 | }) 41 | ``` 42 | 43 | Note that you must provide the global-only `storage` option AND the `syncWithStorage` option to enable the feature. 44 | 45 | ### Sync Individual Service Data 46 | 47 | Here's how to enable storage sync for a single service: 48 | 49 | ```ts 50 | export const api = createPiniaClient(feathersClient, { 51 | idField: '_id', 52 | pinia, 53 | syncWithStorage: false, 54 | storage: window.localStorage, 55 | services: { 56 | 'my-service': { 57 | syncWithStorage: true 58 | } 59 | } 60 | }) 61 | ``` 62 | 63 | ### Customize Storage Keys 64 | 65 | You can customize which store keys are synchronized to storage by passing an array to `syncWithStorage`. You must 66 | provide all keys, since this will override the default value, which is `['itemsById', 'pagination']`. 67 | 68 | ```ts 69 | export const api = createPiniaClient(feathersClient, { 70 | idField: '_id', 71 | pinia, 72 | syncWithStorage: ['itemsById', 'pagination', 'tempsById'], 73 | storage: window.localStorage, 74 | }) 75 | ``` 76 | 77 | ## `syncWithStorage` Utility 78 | 79 | The `syncWithStorage` utility is also available to use with any Pinia store, including Feathers-Pinia data stores. Here 80 | is an example: 81 | 82 | ```ts 83 | import { syncWithStorage, useDataStore } from 'feathers-pinia' 84 | import { createPinia, defineStore } from 'pinia' 85 | 86 | const pinia = createPinia() 87 | 88 | const useStore = defineStore('custom-tasks', () => { 89 | const utils = useDataStore({ 90 | idField: 'id', 91 | customSiftOperators: {}, 92 | setupInstance: (data: any, { api, service, servicePath }) => data 93 | }) 94 | return { ...utils } 95 | }) 96 | const store = useStore(pinia) // --> See API, below 97 | 98 | syncWithStorage(store, ['itemsById', 'pagination'], window.localStorage) 99 | ``` 100 | 101 | ### API 102 | 103 | The `syncWithStorage` utility accepts three arguments: 104 | 105 | - `store` The initialized pinia `store` **required** 106 | - `keys[]`An array of strings representing the keys whose values should be cached. **required** 107 | - `storage{}` an object conforming to the `Storage` interface (same as `localStorage`, `sessionStorage`, etc. **optional: defaults to `localStorage`** 108 | 109 | ## Clear Storage 110 | 111 | If you configured a storage adapter, you can clear all service data from storage by calling `api.clearStorage()`. 112 | 113 | ```ts 114 | api.clearStorage() 115 | ``` 116 | 117 | There is also a standalone utility: 118 | 119 | ```ts 120 | import { clearStorage } from 'feathers-pinia' 121 | 122 | clearStorage(window.localStorage) 123 | ``` 124 | -------------------------------------------------------------------------------- /docs/guide/store-associated.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | 9 | 10 | # The `storeAssociated` Utility 11 | 12 | Learn how to store associated data in its proper stores. 13 | 14 |
15 | 16 | The `storeAssociated` API is deprecated. As of Feathers-Pinia v4.2 it has been replaced with a suite of smaller, 17 | more-flexible, single-purpose utilities. See [Data Modeling](/guide/data-modeling) for the new way to store associated 18 | data. 19 | 20 |
21 | 22 | [[toc]] 23 | 24 | Every Feathers-Pinia Client includes a `storeAssociated` method, which receives a `data` object and a `config` object which 25 | tells the utility the service in which to store the associated data. 26 | 27 | ## storeAssociated Deprecated 28 | 29 | ```ts 30 | storeAssociated(data, config) 31 | ``` 32 | 33 | - **data {Object}** any record 34 | - **config {Object}** an object with keys that represent a key from `data` and where the values are service paths. 35 | 36 | Note that `storeAssociated` should generally be used first in `setupInstance`. 37 | 38 | ## Example 39 | 40 | See `storeAssociated` as configured in the `posts` service of this example: 41 | 42 | ```ts 43 | export const api = createPiniaClient(feathersClient, { 44 | pinia, 45 | idField: 'id', 46 | services: { 47 | authors: { 48 | setupInstance(author, { app }) { 49 | const withDefaults = useInstanceDefaults({ setInstanceRan: false }, author) 50 | return withDefaults 51 | }, 52 | }, 53 | posts: { 54 | idField: 'id', 55 | setupInstance(post, { app }) { 56 | app.storeAssociated(post, { 57 | author: 'authors', 58 | comments: 'comments', 59 | }) 60 | const withDefaults = useInstanceDefaults({ authorIds: [] }, post) 61 | 62 | return withDefaults 63 | }, 64 | }, 65 | comments: { 66 | idField: 'id', 67 | setupInstance(comment, { app }) { 68 | const withDefaults = useInstanceDefaults({ description: '', isComplete: false }, comment) 69 | return withDefaults 70 | }, 71 | }, 72 | }, 73 | }) 74 | ``` 75 | 76 | Now when you create an instance with data matching those keys, the related data will move to the associated stores. 77 | 78 | ```ts 79 | const post = api.service('posts').new({ 80 | title: 'foo', 81 | author: { id: 2, name: 'Steve' }, 82 | comments: [ 83 | { id: 1, text: 'comment 1', authorId: 1, postId: 1 }, 84 | { id: 2, text: 'comment 2', authorId: 1, postId: 1 }, 85 | ], 86 | }) 87 | 88 | post.createInStore() 89 | ``` 90 | 91 | If you inspect `post` in the above example, you'll find the following: 92 | 93 | - `post.author` is an author instance, already created in the `authors` service store. 94 | - `post.comments` is an array of comment instances, already created in the `comments` service store. 95 | - The `post.author` and individual records in `post.comments` are all reactive. So if they get updated in their 96 | stores, the values on `post` will reflect the change. 97 | - The `post.comments` list length is not reactive. 98 | - We still have to manually call `post.createInStore()` afterwards to add it to the `posts` service store. 99 | - Finally, all of the values have been rewritten as non-enumerable, so if you call `post.save()`, the related data will 100 | not be sent in the request to the API server. 101 | -------------------------------------------------------------------------------- /docs/guide/use-backup.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 8 | 9 | # useBackup 10 | 11 | [[toc]] 12 | 13 | The new `useBackup` utility is the opposite of working with clones. Instead of binding your form to a clone, you use the original record. It keeps a copy, letting you call `backup` or `restore` to revert changes. The `save` method auto-diffs from the backup, keeping data size to a minimum. 14 | 15 | ## API 16 | 17 | The `useBackup` utility must receive a ComputedRef. It uses the computed value to automatically 18 | 19 | ```ts 20 | const backup = useBackup(data, { idField: '_id' }) 21 | ``` 22 | 23 | Where backup is an object containing the following properties: 24 | 25 | - `data` - the reactive data 26 | - `backup` - a copy of the data 27 | - `restore` - a method to restore the data to the backup. 28 | - `save` - a method to save the data, sending only the diff 29 | 30 | ## Example 31 | 32 | Here's an example: 33 | 34 | ```vue 35 | 52 | 53 | 66 | ``` -------------------------------------------------------------------------------- /docs/guide/use-clones.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | # `useClones` is now `params.clones` 6 | 7 | In 3.0 `useClones` was removed and [replaced by `params.clones`](/migrate/handle-clones). 8 | -------------------------------------------------------------------------------- /docs/guide/use-find.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | 8 | 9 | # useFind 10 | 11 | [[toc]] 12 | 13 | See [service.useFind](/services/use-find). 14 | -------------------------------------------------------------------------------- /docs/guide/use-get.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | # useGet 6 | 7 | [[toc]] 8 | 9 | See [service.useGet](/services/use-get). 10 | -------------------------------------------------------------------------------- /docs/guide/use-instance-defaults.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | 9 | 10 | # Define Default Values 11 | 12 | Learn how to setup default values on new instances with `useInstanceDefaults`. 13 | 14 | ## useInstanceDefaults 15 | 16 | ```ts 17 | useInstanceDefaults(defaults, data) 18 | ``` 19 | 20 | The `useInstanceDefaults` utility allows you to specify default values to assign to new instances. It 21 | only assigns a value if it the attribute not already specified on the incoming object. 22 | 23 | ```ts 24 | function setupInstance(data: any) { 25 | const withDefaults = useInstanceDefaults({ name: '', email: '', password: '' }, data) 26 | return withDefaults 27 | } 28 | ``` 29 | 30 | Now when you create a user, the object will have default values applied: 31 | 32 |
33 | 34 | Existing keys in `data` will not be replaced by a default value, even if that value is `undefined`. 35 | 36 |
37 | 38 | ```ts 39 | // if no properties are passed, the defaults will all apply 40 | const user = api.service('users').new({}) 41 | console.log(user) // --> { name: 'Marshall', email: '', password: '' } 42 | 43 | // If partial keys are passed, non-passed keys will have defaults applied. 44 | const user = api.service('users').new({ name: 'Marshall' }) 45 | console.log(user) // --> { name: 'Marshall', email: '', password: '' } 46 | 47 | // any "own property" that's present on the object will not be replaced by a default value, even `undefined` values. 48 | const user = api.service('users').new({ name: undefined }) 49 | console.log(user) // --> { name: undefined, email: '', password: '' } 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/guide/use-pagination.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | # usePagination 6 | 7 | [[toc]] 8 | 9 | The `usePagination` utility has been removed. See [service.useFind](/services/use-find) for its replacement. 10 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | hero: 4 | name: Feathers-Pinia 5 | text: Build Lightweight, Real-Time Vue Apps 6 | tagline: Outstanding Data Modeling for FeathersJS and Next-Generation Vue 7 | image: 8 | src: ./feathers-pinia-bird.svg 9 | alt: Feathers-Pinia Logo 10 | actions: 11 | - theme: brand 12 | text: What's New 13 | link: /guide/whats-new 14 | - theme: alt 15 | text: Start a Project 16 | link: /setup/ 17 | - theme: alt 18 | text: API Guides 19 | link: /guide/ 20 | 21 | features: 22 | - icon: 🕊️ 23 | title: FeathersJS v5 Dove Support 24 | details: Feathers-Pinia v3 now directly wraps the Feathers Client. Effortlessly use types directly from your backend API. 25 | 26 | - icon: 🍍 27 | title: Powered by Pinia 28 | details: It's a joy to use with a clean API and memorable syntax. It's also crazy fast. Really, the speed difference is ludicrous. ➳ 29 | 30 | - icon: 🧁 31 | title: Best Practices Baked In 32 | details: Vue 3 + Composition API 😎 Common Redux patterns built in. Intelligent Fall-Through Cache. Query the store like a local database. 33 | 34 | - icon: ⚡️ 35 | title: Realtime by Default 36 | details: Realtime isn't an afterthought. Live Queries mean your UI updates as new data arrives from the Feathers server. No effort required. 37 | 38 | - icon: 🐮 39 | title: SWR with more Cowbell 40 | details: Feathers-Pinia can intelligently re-use data across different queries, making apps feel faster. Go Realtime and make SWR obsolete! 41 | 42 | - icon: 🥷 43 | title: Data Modeling Beyond Class 44 | details: v3.0 brings simplified setup and implicit Data Modeling. We've ditched classes for functions and baked it into the Feathers Client. 45 | 46 | --- 47 | 48 | 52 | -------------------------------------------------------------------------------- /docs/migrate/handle-clones.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | 9 | 10 | # Migrate `handleClones` to `useGet` 11 | 12 | [[toc]] 13 | 14 | Feathers-Pinia 3.0 no longer includes a `handleClones` (v0) or `useClones` (v2) API. You can now use `params.clones` 15 | together with `useGet` to replace the same functionality. 16 | 17 | ## API Change 18 | 19 | One of the main features of `useClones` was the automated diffing that happened in the `save_handlers`. Patch diffing is 20 | now automatic when you call `instance.patch()` (and `instance.save()` when it calls patch). The following tabs show the 21 | old ways and the new way: 22 | 23 | ::: code-group 24 | 25 | ```vue [handleClones (0.x)] 26 | 36 | 37 | 50 | ``` 51 | 52 | ```vue [useClones (2.x)] 53 | 62 | 63 | 76 | ``` 77 | 78 | ```vue [service.getFromStore (3.x)] 79 | 93 | 94 | 107 | ``` 108 | 109 | ::: 110 | 111 | ## Behavior Change 112 | 113 | Previously, `useClones` would deep-watch the cloned props, by default. This was great if you wanted your forms to update 114 | in realtime, but also had the unfortunate effect of sometimes overwriting values currently being edited in highly-used 115 | apps. To address this potential UX bug, clone values only update shallowly, when `id` value of the instance prop changes. 116 | To replicate previous functionality, you can watch the original instance in the store, and manually update the clone. 117 | -------------------------------------------------------------------------------- /docs/migrate/models.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | 9 | 10 | # Migrate Models to Services 11 | 12 | [[toc]] 13 | 14 | Model Classes and Functions have served us well, but are no more. Every Feathers Service now includes implicit model 15 | functions which can be extended. 16 | 17 | ## Why Implicit Models? 18 | 19 | Rather than having to setup every single model, we can take advantage of Vue 3's `reactive` API, which allows one to 20 | dynamically add and remove attributes from reactive objects. Feathers-Pinia 3 takes full advantage of this fact and 21 | automatically assumes that every object needs to be a model. So now there is no need to manually create them. 22 | 23 | ## Switch to Implicit Modeling 24 | 25 | The below tabs allow you to compare previous modeling examples to the latest API. In Feathers-Pinia modeling happens in 26 | the `services` config of `createPiniaClient`. 27 | 28 | ::: code-group 29 | 30 | ```ts [Old Model Class] 31 | // models/user.ts 32 | import { BaseModel } from '../store/store.pinia' 33 | 34 | export class User extends BaseModel { 35 | /* You might have defined default values, here, */ 36 | _id?: string 37 | name = '' 38 | email = '' 39 | password = '' 40 | 41 | // Depending on the Feathers-Pinia version, you may not have written a constructor 42 | constructor(data: Partial = {}, options: Record = {}) { 43 | super(data, options) 44 | this.init(data) 45 | } 46 | 47 | // optional for setting up data objects and/or associations 48 | static setupInstance(message: Partial) { 49 | const { store, models } = this 50 | return { 51 | /* default properties used to go here */ 52 | } 53 | } 54 | } 55 | ``` 56 | 57 | ```ts [Old Model Fn] 58 | import type { Users, UsersData, UsersQuery } from 'my-feathers-api' 59 | import { type ModelInstance, useFeathersModel, useInstanceDefaults } from 'feathers-pinia' 60 | import { api } from '../feathers' 61 | 62 | const service = api.service('users') 63 | 64 | function modelFn(data: ModelInstance) { 65 | const withDefaults = useInstanceDefaults({ name: '', email: '', password: '' }, data) 66 | return withDefaults 67 | } 68 | const User = useFeathersModel( 69 | { name: 'User', idField: '_id', service }, 70 | modelFn, 71 | ) 72 | ``` 73 | 74 | ```ts [New API] 75 | import { createPiniaClient, useInstanceDefaults } from 'feathers-pinia' 76 | import type { ServiceInstance } from 'feathers-pinia' 77 | import type { Users } from 'my-feathers-api' 78 | 79 | const api = createPiniaClient(feathersClient, { 80 | pinia, 81 | idField: 'id', 82 | services: { 83 | users: { 84 | setupInstance(data: ServiceInstance) { 85 | const withDefaults = useInstanceDefaults({ name: '', email: '', password: '' }, data) 86 | return withDefaults 87 | }, 88 | }, 89 | }, 90 | }) 91 | ``` 92 | 93 | ::: 94 | 95 | ## Important Changes 96 | 97 | ### No Model Constructors 98 | 99 | The Model constructor is now the model function. If you have any constructor logic, move it into the `setupInstance` 100 | method of the service's configuration. 101 | 102 | ### `useInstanceDefaults` 103 | 104 | The `instanceDefaults` static Model function is replaced by the [useInstanceDefaults utility](/guide/use-instance-defaults). 105 | 106 | ### `setupInstance` Changes 107 | 108 | You must return the object from the `setupInstance` function. If not, you'll run into errors. 109 | -------------------------------------------------------------------------------- /docs/partials/assess-your-auth-risk.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | The auth examples on this page will suffice for apps with simple security requirements. If you are building an app with 4 | privacy requirements, you need something more secure. 5 | 6 | There are multiple ways to secure your app. If you need help, please [contact a FeathersHQ member](https://github.com/feathershq/) 7 | for consulting services. 8 | 9 |
10 | -------------------------------------------------------------------------------- /docs/partials/auto-imports-overview.md: -------------------------------------------------------------------------------- 1 | ## Auto-Imported Composables 2 | 3 | You can use these utilities with auto-imports configured. 4 | 5 | - [createPiniaClient](/guide/create-pinia-client) creates a Feathers-Pinia client from a Feathers Client. 6 | - [useInstanceDefaults](/guide/use-instance-defaults) for implicit modeling, sets up default values on instances. 7 | - [useAuth](/guide/use-auth) for creating auth stores. 8 | - [useDataStore](/data-stores/) for managing non-service data stores with the same local API. 9 | - [defineValues](/guide/utilities#definevalues) for adding configurable, non-enumerable properties to items. 10 | - [defineGetters](/guide/utilities#definegetters) for adding configurable, non-enumerable getters to items. 11 | - [defineSetters](/guide/utilities#definesetters) for adding configurable, non-enumerable setters to items. 12 | -------------------------------------------------------------------------------- /docs/partials/notification-access-token.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | In version 2 the `useAuth` plugin does not store the `accessToken` in the store, since the Feathers Client 4 | always holds a copy, which can be retrieved asynchronously. See the [useAuth docs](/guide/use-auth#obtaining-the-auth-payload) 5 | to see how to manually store the `accessToken`. Keep in mind that storing your `accessToken` in more places likely 6 | makes it less secure. 7 | 8 |
9 | -------------------------------------------------------------------------------- /docs/partials/notification-feathers-client.md: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | Replace `my-feathers-api` in the below example with the package installed 5 | [from your Feathers v5 Dove API](https://feathersjs.com/guides/cli/client.html). You can manually create a client. See 6 | the [install page](/setup/install#install-typed-client) for more details. 7 | 8 |
9 | -------------------------------------------------------------------------------- /docs/partials/nuxt-config.md: -------------------------------------------------------------------------------- 1 | ```ts 2 | // nuxt.config.ts 3 | // https://v3.nuxtjs.org/api/configuration/nuxt.config 4 | export default defineNuxtConfig({ 5 | modules: [ 6 | '@pinia/nuxt', 7 | 'nuxt-feathers-pinia', 8 | ], 9 | imports: { 10 | // Not required, but useful: list folder names here to enable additional "composables" folders. 11 | dirs: [], 12 | }, 13 | // Enable Nuxt Takeover Mode 14 | typescript: { 15 | shim: false, 16 | }, 17 | // optional, Vue Reactivity Transform 18 | experimental: { 19 | reactivityTransform: true, 20 | }, 21 | }) 22 | ``` 23 | 24 | You can read more about the above configuration at these links: 25 | 26 | - [@pinia/nuxt module](https://pinia.vuejs.org/ssr/nuxt.html) 27 | - [nuxt-feathers-pinia module](/guide/nuxt-module) 28 | - [Nuxt `imports` config](https://nuxt.com/docs/api/configuration/nuxt-config#imports) 29 | - [Nuxt Takeover Mode](https://nuxt.com/docs/getting-started/installation#prerequisites) 30 | - [Vue Reactivity Transform](https://vuejs.org/guide/extras/reactivity-transform.html) 31 | 32 | If you use `npm` as your package manager and you see the error `ERESOLVE unable to resolve dependency tree`, add this to 33 | your package.json: 34 | 35 | ```json 36 | { 37 | "overrides": { 38 | "vue": "latest" 39 | } 40 | } 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/partials/patch-diffing.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | Don't waste bandwidth! Just send the props that change! 4 | 5 |
6 | 7 | Patch diffing, which originated in Feathers-Vuex, is now back in Feathers-Pinia with a smarter, faster algorithm that 8 | will work for any scenario you can dream up. 9 | 10 | Diffing only occurs on `patch` requests (and when calling `instance.save()` calls a `patch`). 11 | 12 | ```ts 13 | // clone a record 14 | const clone = user.clone() 15 | // make changes 16 | clone.name = 'Feathers is Amazing!' 17 | // save 18 | await clone.save() // --> Only the changed props go to the server! 19 | ``` 20 | 21 |
22 | 23 | - By default, all keys are deep-compared between the original record and the clone. 24 | - Once all changes are found, only the top-level keys are sent to the server. 25 | 26 | Diffing will work on all databases without data loss. 27 | 28 |
29 | 30 | ### Customize the Diff 31 | 32 | You can use the `diff` option to customize which values are compared. Only props that have changed will be sent to the server. 33 | 34 | ```ts 35 | // string: diff only this prop 36 | await clone.save({ diff: 'teamId' }) 37 | 38 | // array: diff only these props 39 | await clone.save({ diff: ['teamId', 'username'] }) 40 | 41 | // object: merge and diff these props 42 | await clone.save({ diff: { teamId: 1, username: 'foo' } }) 43 | 44 | // or turn off diffing and send everything 45 | await clone.save({ diff: false }) 46 | ``` 47 | 48 | ### Always Save Certain Props 49 | 50 | If there are certain props that need to always go with the request, use the `with` option: 51 | 52 | ```ts 53 | // string: always include this prop 54 | await clone.save({ with: 'teamId' }) 55 | 56 | // array: always include these props 57 | await clone.save({ with: ['teamId', 'username'] }) 58 | 59 | // object: merge and include these props 60 | await clone.save({ with: { teamId: 1, username: 'foo' } }) 61 | ``` 62 | 63 | ### Specify Patch Data 64 | 65 | When calling `.save()` or `.patch()`, you can provide an object as `params.data`, and Feathers-Pinia will use it as the 66 | patch data. This bypasses the `diff` and `with` params. 67 | 68 | ```js 69 | const { api } = useFeathers() 70 | 71 | const task = api.service('tasks').new({ description: 'Do Something', isComplete: false }) 72 | await task.patch({ data: { isComplete: true } }) 73 | ``` 74 | 75 | ### Eager Commits 76 | 77 | Eager updates are enabled, by default, when calling patch/save on a clone. This means that `commit` is called before the 78 | API request goes out. If an API errors occurs, the change will be rolled back. 79 | 80 | Sometimes eager commits aren't desirable, so you can turn them off when needed by passing `{ eager: false }`, like this: 81 | 82 | ```ts 83 | await clone.save({ eager: false }) 84 | ``` 85 | 86 | With `eager: false`, the commit will happen after the API server responds to the patch request. 87 | -------------------------------------------------------------------------------- /docs/partials/service-interface.md: -------------------------------------------------------------------------------- 1 | ```ts 2 | const { api } = useFeathers() 3 | const service = api.service('users') 4 | 5 | // create data instances 6 | service.new(data) 7 | 8 | // api methods 9 | service.find(params) 10 | service.findOne(params) // unique to feathers-pinia 11 | service.count(params) 12 | service.get(id, params) 13 | service.create(id, params) 14 | service.patch(id, params) 15 | service.remove(id, params) 16 | 17 | // store methods 18 | service.findInStore(params) 19 | service.findOneInStore(params) 20 | service.countInStore(params) 21 | service.getFromStore(id, params) 22 | service.createInStore(data, params) // data is a record or array or records 23 | service.patchInStore(idData, params) // idData is one or more ids or records 24 | service.removeFromStore(idData, params) // idData is one or more ids or records 25 | 26 | // hybrid methods 27 | service.useFind(params, options) 28 | service.useGet(id, options) 29 | service.useGetOnce(id, options) 30 | 31 | // event methods 32 | service.on(eventName, eventHandler) 33 | service.emit(eventName, data) 34 | service.removeListener(eventName, eventHandler) 35 | ``` 36 | -------------------------------------------------------------------------------- /docs/services/hybrid-queries.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | 9 | 10 | # Hybrid Query Utilities 11 | 12 | Learn about the easiest way to watch and query data with `service.useFind` and `service.useGet`. 13 | 14 | [[toc]] 15 | 16 | Hybrid queries combine API requests and store requests into a simpler API, so when the query or id changes, the queries 17 | are automatically made and the correct data is pulled from the store for you. 18 | 19 | There are a few scenarios where the hybrid query utilities really shine: 20 | 21 | ## useFind 22 | 23 | The [useFind utility](./use-find) comes in handy when 24 | 25 | - Paginating data with `$limit` and `$skip`. Feathers-style pagination support is built in and made simple. 26 | - Performing server-side pagination with `useFind` and the `paginateOn` option set to `'server'`. Turning on 27 | `paginateOn: 'server'` keeps track of individual pages of server data and re-fetches data whenever the list needs to change. 28 | - Performing client-side pagination, where you pull a big set of data into the store then paginate through it at 29 | lightning speed. 30 | 31 | ## useGet 32 | 33 | The [useGet utility](./use-get) comes in handy when 34 | 35 | - you want to pull individual records from the database, like to populate a form. It's especially useful when the 36 | `skipGetIfExists` configuration option is enabled, which should be enabled on realtime applications, but probably not on 37 | Rest applications. 38 | -------------------------------------------------------------------------------- /docs/services/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | 9 | 10 | # Feathers-Pinia Services 11 | 12 | [[toc]] 13 | 14 | ## Service Interface Overview 15 | 16 | The Feathers-Pinia Service Interface adds methods to the [Feathers Service Interface](https://feathersjs.com/api/services.html), 17 | allowing the service to work as a functional replacement for a Model constructor. In short, in Feathers-Pinia v3 the 18 | service is the Model. 19 | 20 | Here's an overview of the full Feathers-Pinia Service Interface: 21 | 22 | 23 | 24 | ## The "new" Method 25 | 26 | ```ts 27 | service.new(data) 28 | ``` 29 | 30 | The "new" method allows creation of model instances. It takes the place of calling `new Model(data)` in previous 31 | Feathers-Pinia releases. 32 | 33 | ### Customizing Instances 34 | 35 | Customizing the default state of each instance is done through `setupInstance` in each service's configuration. 36 | 37 | ## Service Methods 38 | 39 | Service methods are convenience wrappers around the Feathers Client service provided in the options. 40 | 41 | ### `find(params)` 42 | 43 | ```ts 44 | await todoStore.find({ query: {} }) 45 | ``` 46 | 47 | Uses the Feathers Client to retrieve records from the API server. On an SSR server, find data will be marked as `ssr: true`, which allows extra queries to be skipped on the client. 48 | 49 | ### `findOne(params)` 50 | 51 | ```ts 52 | await service.findOne({ query: {} }) 53 | ``` 54 | 55 | Uses the Feathers Client to retrieve the first matching record from the API server. On an SSR server, find data will be 56 | marked as `ssr: true`, which allows extra queries to be skipped on the client. 57 | 58 | ### `count(params)` 59 | 60 | ```ts 61 | await service.count({ query: { isComplete: false } }) 62 | ``` 63 | 64 | Like `find`, but returns the number of records that match the query. It does not return the actual records. 65 | 66 | ### `get(id, params)` 67 | 68 | ```ts 69 | await todoStore.get(1) 70 | ``` 71 | 72 | Uses the Feathers Client to retrieve a single record from the API server. 73 | 74 | Uses the Feathers Client to send an `update` request to the API server. 75 | 76 | ### `patch(id, data, params)` 77 | 78 | ```ts 79 | await todoStore.patch(1, { isComplete: true }) 80 | ``` 81 | 82 | Uses the Feathers Client to send an `patch` request to the API server. 83 | 84 | ### `remove(id, params)` 85 | 86 | ```ts 87 | await api.service.remove(id) 88 | ``` 89 | 90 | Uses the Feathers Client to send a `remove` request to the API server. 91 | 92 | ### Service Utils 93 | 94 | These utilities use a combination of multiple store methods to eliminate boilerplate and improve developer experience. 95 | 96 | - [useFind()](/services/use-find) 97 | - [useGet()](/services/use-get) 98 | - `useGetOnce()` has the same API as [useGet](/services/use-get), but only queries once per record. 99 | 100 | ## Service Events 101 | 102 | Services are `EventEmitter` instances which emit service events when received. Services can be used as a data-layer 103 | Event Bus. You can even use custom event names: 104 | 105 | ```js 106 | service.on('custom-event', (data) => { 107 | console.log(data) // { test: true } 108 | }) 109 | 110 | service.emit('custom-event', { test: true }) 111 | ``` 112 | 113 | ### service.on 114 | 115 | Register event handlers to listen to events. 116 | 117 | ### service.once 118 | 119 | Register an event handler that only occurs once. 120 | 121 | ### service.removeListener 122 | 123 | Remove an event handler. 124 | -------------------------------------------------------------------------------- /docs/services/stores.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | 9 | 10 | # Service Stores 11 | 12 | Learn about the state, getters, and actions in service stores. 13 | 14 | [[toc]] 15 | 16 | Every service has a `store` property, which includes the following API. 17 | 18 | ## Returned API 19 | 20 | The object returned from `useDataStore` is built on top of the BaseModel store. Refer to the 21 | [Standalone data store documentation](/data-stores/) for API details. The following sections will cover store 22 | APIs not in the BaseModel store. 23 | 24 | The following sections cover additional APIs returned when calling `useDataStore`. APIs are grouped by functionality. 25 | 26 | ### Additional State 27 | 28 | - **`service`** 29 | - **`paramsForServer`** 30 | - **`skipGetIfExists`** 31 | - **`isSsr`** 32 | 33 | ### Model Config 34 | 35 | - **`Model`** gives access to the Model Function provided in the options. 36 | - **`setModel(Model)`** Allows setting/replacing the Model. This means you can call `useFind` without a Model and set 37 | the model afterwards. 38 | 39 | ### Pagination State 40 | 41 | - **`pagination`** keeps track of the latest pagination data for each paginated request to the server. You generally 42 | won't manually modify this. 43 | - **`updatePaginationForQuery()`** 44 | - **`unflagSsr()`** 45 | 46 | ### Pending State 47 | 48 | - **`isPending`** 49 | - **`createPendingById`** keeps track of individual records, by id, that have pending `create` requests. 50 | - **`updatePendingById`** keeps track of individual records, by id, that have pending `update` requests. 51 | - **`patchPendingById`** keeps track of individual records, by id, that have pending `patch` requests. 52 | - **`removePendingById`** keeps track of individual records, by id, that have pending `remove` requests. 53 | - **`isFindPending`** is a boolean computed which will be true if any `find` request is pending. 54 | - **`isCountPending`** is a boolean computed which will be true if any `count` request is pending. 55 | - **`isGetPending`** is a boolean computed which will be true if any `get` request is pending. 56 | - **`isCreatePending`** is a boolean computed which will be true if any `create` request is pending. 57 | - **`isUpdatePending`** is a boolean computed which will be true if any `update` request is pending. 58 | - **`isPatchPending`** is a boolean computed which will be true if any `patch` request is pending. 59 | - **`isRemovePending`** is a boolean computed which will be true if any `remove` request is pending. 60 | - **`setPending()`** allows setting a method as pending. 61 | - **`setPendingById()`** allows setting a record as pending by method name and record id. 62 | - **`unsetPendingById()`** allows unsetting a record's pending status. 63 | - **`clearAllPending()`** resets pending state back to its original, empty state. 64 | 65 | ### Event Locks 66 | 67 | Event locks are automatically managed and require no manual upkeep. 68 | 69 | - **`eventLocks`** helps prevent receiving normal, duplicate responses from the API server during CRUD actions. Instead of processing both the CRUD response AND the realtime event data, it only handles one of them. 70 | - **`toggleEventLock()`** used to toggle an event lock. 71 | - **`clearEventLock()`** used to turn off an event lock 72 | -------------------------------------------------------------------------------- /docs/services/use-get.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | 9 | 10 | # The `useGet` Method 11 | 12 | [[toc]] 13 | 14 | The `useGet` method takes the work out of watching an "id" variable and retrieving individual records from the API 15 | server. 16 | 17 | ## Overview of Features 18 | 19 | - **Auto-Updating** - change the `id` and it does the rest. 20 | - **Fall-Through Cache** - Always pulls from the store while new data is fetched. 21 | - **Easy Request State** - Pending request state is built in. 22 | - **SSR Friendly** - Data can load on the server and hydrate on the client. 23 | 24 | ## Usage 25 | 26 | You can call `useGet` directly from the store. the advantage being that you don't have to provide the `store` in the params, as shown here: 27 | 28 | ```ts 29 | interface Props { 30 | id: string | number 31 | } 32 | const props = defineProps() 33 | 34 | const { api } = useFeathers() 35 | const id = computed(() => props.id) 36 | 37 | const user$ = api.service('users').useGet(id) 38 | ``` 39 | 40 | For a client-only solution, you can just use the `getFromStore` method: 41 | 42 | ```ts 43 | interface Props { 44 | id: string | number 45 | } 46 | const props = defineProps() 47 | 48 | const { api } = useFeathers() 49 | const id = computed(() => props.id) 50 | 51 | const user = api.service('users').getFromStore(id) 52 | ``` 53 | 54 | ## API 55 | 56 | ### useGet(id, params) 57 | 58 | - **`id` {MaybeRef string | number}** the id of the record to retrieve. Can be a computed/ref to enable automatic updates to the returned `data`. 59 | - **`params` {Object}** a combined Feathers `Params` object and set of options for configuring behavior of `useGet`. 60 | - **`query` {Object}** a Feathers query object. 61 | - **`clones` {Boolean}** returns result as a clone. See [Querying Data](/data-stores/querying-data#local-params-api) 62 | - **`temps` {Boolean}** enables retrieving temp records. See [Querying Data](/data-stores/querying-data#local-params-api) 63 | - **`watch` {boolean}** can be used to disable the watcher on `id` 64 | - **`immediate` {boolean}** can be used to disable the initial request to the API server. 65 | 66 | ### Returned Object 67 | 68 | - **`id` {Ref number | string}** is a ref version of the `id` that was provided as the first argument to `useGet`. Modifying `id.value` will cause the `data` to change. 69 | - **`params` {Params}** is a ref version of the params. Params are not currently watched for `useGet`. 70 | - **`data` {Computed Object}** the record returned from the store. 71 | - **`ids` {Ref Array}** is a list of ids that have been retrieved from the API server, in chronological order. May contain duplicates. 72 | - **`get` {Function}** similar to `store.get`. If called without any arguments it will fetch/re-fetch the current `id`. Accepts no arguments. 73 | - **`request` {Promise}** stores the current promise for the `get` request. 74 | - **`requestCount` {Ref number}** a counter of how many requests to the API server have been made. 75 | - **`getFromStore` {Function}** the same as `store.getFromStore`. 76 | - **`isPending` {Computed boolean}** returns true if there is a pending request. While true, the `data` will continue to hold the most recently-fetched record. 77 | - **`hasBeenRequested` {Computed boolean}** returns true if any record has been requested through this instance of `useGet`. It never resets. 78 | - **`hasLoaded` {Computed boolean}** is similar to `isPending` but with different wording. 79 | - **`error` {Computed error}** will display any error that occurs. The error is cleared if another request is made or if `clearError` is called. 80 | - **`clearError` {Function}** can be used to manually clear the `error`. 81 | 82 | ### Only Query Once Per Record 83 | 84 | The simplest way to only query once per record is to set the `skipGetIfExists` option to `true` during configuration. 85 | 86 | You can also use the `useGetOnce` method to achieve the same behavior for individual requests. 87 | -------------------------------------------------------------------------------- /docs/setup/example-apps.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 8 | 9 | # Example Applications 10 | 11 | Two example showcase applications have been created to demonstrate Feathers-Pinia functionality: 12 | 13 | - The [Vite](#vite) example a Single Page App that doesn't do any server-side rendering (SSR). 14 | - The [Nuxt](#nuxt) example is an app built with Nuxt3 and includes support for SSR. 15 | 16 | ## Vite 17 | 18 | [Vite Example Repo](https://github.com/marshallswain/feathers-pinia-vite) 19 | 20 | 21 | 22 | ![Feathers-Pinia-Vite screenshot](https://user-images.githubusercontent.com/128857/202971929-78dd7ca7-111e-409a-8817-c028ebf4d3c5.jpg) 23 | 24 | 25 | 26 | ## Nuxt 27 | 28 | [Nuxt Example Repo](https://github.com/marshallswain/feathers-pinia-nuxt3) 29 | -------------------------------------------------------------------------------- /docs/setup/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 8 | 9 | # Start a Project 10 | 11 | [[toc]] 12 | 13 | ## Install Modules 14 | 15 | You'll need to [Install Modules](/setup/install) no matter which framework you choose. 16 | 17 | ## Framework Install Guides 18 | 19 | We have full documentation for the following frameworks. 20 | 21 | ### Vite + Vue 22 | 23 | Follow the [Vite + Vue](/setup/vite) setup guide for Single Page Apps (SPA) 24 | 25 | ### Nuxt 3 26 | 27 | Our [Nuxt 3](/setup/nuxt3) guide works for SPA, SSG, SSR, or hybrid-rendered apps. 28 | 29 | ### Quasar 30 | 31 | Currently incomplete, the [Quasar](/setup/quasar) guide is the next priority. 32 | 33 | ## Example Applications 34 | 35 | We have [full example applications](/setup/example-apps) for each of the completed framework integrations. 36 | 37 | ## Other Examples 38 | 39 | Examples on the [Other Setup Examples](/setup/other) page include 40 | 41 | - Working with @feathersjs/memory 42 | - Setting up custom SSR 43 | -------------------------------------------------------------------------------- /docs/setup/other.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | 9 | 10 | # Other Setup Examples 11 | 12 | [[toc]] 13 | 14 | ## With `@feathersjs/memory` 15 | 16 | You can try Feathers-Pinia without a backend by using `@feathersjs/memory`. First, you'll need to install the package: 17 | 18 | ```bash 19 | npm i @feathersjs/memory 20 | ``` 21 | 22 | Now you only need to instantiate the memory server on the service. It takes only two lines of code! See here: 23 | 24 | ```ts 25 | // make a memory store for the service 26 | api.use(servicePath, memory({ paginate: { default: 10, max: 100 }, whitelist: ['$options'] })) 27 | api.service(servicePath).hooks({}) 28 | ``` 29 | 30 | With the `memory` adapter in place, you'll be able to make requests as though you were connected to a remote server. And technically, for this service it is no longer required to have a client transport (rest or socket.io) configured. 31 | 32 | One more thing: you can start the memory adapter with fixture data in place, if wanted. Provide the `store` option with the the data keyed by id, as shown below. You can also provide this option during instantiation. 33 | 34 | ```ts 35 | api.use(servicePath, memory({ 36 | paginate: { default: 10, max: 100 }, 37 | whitelist: ['$options'], 38 | store: { 39 | 1: { id: 1, name: 'Marshall' }, 40 | 2: { id: 2, name: 'David' }, 41 | 10: { id: 10, name: 'Wolverine' }, 42 | 10: { id: 10, name: 'Gambit' }, 43 | 11: { id: 11, name: 'Rogue' }, 44 | 12: { id: 12, name: 'Jubilee' }, 45 | } 46 | })) 47 | ``` 48 | 49 | The same data can be written at any time during runtime by setting `api.service('users').options.store` to the new object, keyed by id. 50 | -------------------------------------------------------------------------------- /docs/setup/quasar.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | 9 | 10 | # Quasar 🚧 11 | 12 | Learn how to setup Feathers-Pinia 3 with a Quasar CLI app. 13 | 14 | [[toc]] 15 | 16 | Check out this [community-contributed example](https://github.com/rabalyn/feathers-pinia-quasar). 17 | 18 | 30 | -------------------------------------------------------------------------------- /docs/styles.postcss: -------------------------------------------------------------------------------- 1 | /** 2 | * This injects Tailwind's base styles and any base styles registered by 3 | * plugins. 4 | */ 5 | @tailwind base; 6 | 7 | /** 8 | * This injects Tailwind's component classes and any component classes 9 | * registered by plugins. 10 | */ 11 | @tailwind components; 12 | 13 | /** 14 | * This injects Tailwind's utility classes and any utility classes registered 15 | * by plugins. 16 | */ 17 | @tailwind utilities; 18 | 19 | /** 20 | * Use this directive to control where Tailwind injects the responsive 21 | * variations of each utility. 22 | * 23 | * If omitted, Tailwind will append these classes to the very end of 24 | * your stylesheet by default. 25 | */ 26 | @tailwind screens; 27 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | // postcss.config.js 2 | export default { 3 | plugins: { 4 | 'postcss-nested': {}, 5 | 'tailwindcss': {}, 6 | 'autoprefixer': {}, 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marshallswain/feathers-pinia/2bf7ad8071580591a328bb14aecee875268e8231/public/favicon.ico -------------------------------------------------------------------------------- /public/training/pinia-stores.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | 9 | 10 | # Pinia Stores 11 | 12 | [[toc]] 13 | 14 | ## Setup Stores, Only 15 | 16 | Similar to how VueJS components can be written using the "Options API" or the "Composition API", Pinia, itself, also 17 | supports "Options API" and "Composition API" stores, except Pinia calls "Composition API" stores "setup stores". Up 18 | until Feathers-Pinia 2.0, only the "Options API" stores were supported. As of Feathers-Pinia 2.0, only `setup` stores 19 | are supported. Setup stores share the same benefits of the Vue Composition API, being more flexible and easier to 20 | customize. 21 | 22 | ## Create a "Setup" Store 23 | 24 | To create a `setup` store, import `defineStore` from `pinia` then call it with a string for the store name followed by a 25 | function that accepts no arguments and returns an object, as shown below. Make sure the store's name is unique to 26 | prevent conflicts. 27 | 28 | ```ts 29 | import { defineStore } from 'pinia' 30 | 31 | export const useMyStore = defineStore('my-store', () => { 32 | // implement store logic 33 | return {} 34 | }) 35 | ``` 36 | 37 | Learn more about [Pinia Setup Stores](https://pinia.vuejs.org/core-concepts/#setup-stores). 38 | 39 | ## HMR Support 40 | 41 | If you're building your app with Vite, you can utilize Pinia's `acceptHMRUpdate` to make sure your stores are properly 42 | reloaded after a hot module swap. To implement, import `acceptHMRUpdate` from `pinia` and conditionally call it below 43 | your store declaration block: 44 | 45 | ```ts 46 | import { defineStore, acceptHMRUpdate } from 'pinia' 47 | 48 | export const useMyStore = defineStore('my-store', () => { 49 | // implement store logic 50 | return {} 51 | }) 52 | 53 | // Adds HMR support 54 | if (import.meta.hot) { 55 | import.meta.hot.accept(acceptHMRUpdate(useMyStore, import.meta.hot)) 56 | } 57 | ``` 58 | 59 | ## Store Types 60 | 61 | Feathers-Pinia comes with two utilities for creating different types of stores: 62 | 63 | - [useService](/data-stores/) is used to create a store that connects to a FeathersJS service. 64 | - [useAuth](/guide/use-auth) is used to create an auth store. 65 | -------------------------------------------------------------------------------- /src/composables/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-backup' 2 | -------------------------------------------------------------------------------- /src/composables/use-backup.ts: -------------------------------------------------------------------------------- 1 | import { copyStrict } from 'fast-copy' 2 | import { isRef, ref, unref, watch } from 'vue' 3 | import type { ComputedRef, Ref } from 'vue' 4 | import { diff } from '../utils/utils' 5 | import type { AnyData, DiffDefinition } from '../types' 6 | 7 | type AnyRef = Ref | ComputedRef 8 | 9 | interface UseBackupOptions { 10 | onlyProps?: keyof D 11 | idField?: keyof D 12 | } 13 | 14 | /** 15 | * Provides a backup of the current instance, and methods to save and restore it. 16 | * The `save` method will diff the current instance with the backup. Any values in the new 17 | * instance that are different in the old instance will be passed to the save method as data. 18 | */ 19 | export function useBackup(data: AnyRef, options?: UseBackupOptions) { 20 | const backup = ref(null) 21 | 22 | // automatically update if the record changes 23 | watch(data, async (val: any) => { 24 | if (!data) 25 | return 26 | 27 | const idField = options?.idField 28 | const id = idField ? val?.[idField] : val?.id 29 | const backupId = idField ? backup.value?.[idField] : backup.value?.id 30 | if (id !== backupId) 31 | backup.value = copyStrict(val) 32 | }, { immediate: true }) 33 | 34 | /** 35 | * Diff the current instance with the backup. Any values in the new instance that are 36 | * different in the old instance will be passed to the save method as data. 37 | */ 38 | async function save() { 39 | const toDiff = unref(data as any) 40 | const diffData = diff(backup.value, toDiff, options?.onlyProps as DiffDefinition) 41 | // if any keys were different... 42 | if (Object.keys(diffData).length) { 43 | // save the diff 44 | try { 45 | const withUpdates = await data.value.save({ data: diffData }) 46 | 47 | // update the backup to match the new instance 48 | backup.value = copyStrict(withUpdates) 49 | } 50 | catch (error) { 51 | console.error('could not save', error) 52 | throw error 53 | } 54 | } 55 | // if nothing changed, return the original record 56 | else { return toDiff } 57 | } 58 | 59 | function restore(currentInstance: any) { 60 | if (backup.value) { 61 | const target = isRef(currentInstance) ? currentInstance.value : currentInstance 62 | Object.assign(target, backup.value) 63 | } 64 | return currentInstance 65 | } 66 | 67 | return { data, backup, save, restore } 68 | } 69 | -------------------------------------------------------------------------------- /src/custom-operators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sql-like.js' 2 | -------------------------------------------------------------------------------- /src/custom-operators/sql-like.ts: -------------------------------------------------------------------------------- 1 | import { createEqualsOperation } from 'sift' 2 | 3 | // Simulate SQL's case-sensitive LIKE. 4 | // A combination of answers from https://stackoverflow.com/questions/1314045/emulating-sql-like-in-javascript 5 | export function like(value: string, search: string, regexOptions = 'g') { 6 | const specials = ['/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\'] 7 | // Remove specials 8 | search = search.replace(new RegExp(`(\\${specials.join('|\\')})`, regexOptions), '\\$1') 9 | // Replace % and _ with equivalent regex 10 | search = search.replace(/%/g, '.*').replace(/_/g, '.') 11 | // Check matches 12 | return RegExp(`^${search}$`, regexOptions).test(value) 13 | } 14 | 15 | // Simulate PostgreSQL's case-insensitive ILIKE 16 | export function iLike(str: string, search: string) { 17 | return like(str, search, 'ig') 18 | } 19 | 20 | export function $like(params: any, ownerQuery: any, options: any) { 21 | return createEqualsOperation((value: any) => like(value, params), ownerQuery, options) 22 | } 23 | 24 | export function $notLike(params: any, ownerQuery: any, options: any) { 25 | return createEqualsOperation((value: any) => !like(value, params), ownerQuery, options) 26 | } 27 | 28 | export function $ilike(params: any, ownerQuery: any, options: any) { 29 | return createEqualsOperation((value: any) => iLike(value, params), ownerQuery, options) 30 | } 31 | 32 | function $notILike(params: any, ownerQuery: any, options: any) { 33 | return createEqualsOperation((value: any) => !iLike(value, params), ownerQuery, options) 34 | } 35 | 36 | export const sqlOperations = { 37 | $like, 38 | $notLike, 39 | $notlike: $notLike, 40 | $ilike, 41 | $iLike: $ilike, 42 | $notILike, 43 | } 44 | -------------------------------------------------------------------------------- /src/feathers-ofetch.ts: -------------------------------------------------------------------------------- 1 | import { FetchClient } from '@feathersjs/rest-client' 2 | import type { Params } from '@feathersjs/feathers' 3 | 4 | // A feathers-rest transport adapter for https://github.com/unjs/ofetch 5 | export class OFetch extends FetchClient { 6 | async request(options: any, params: Params) { 7 | const fetchOptions = Object.assign({}, options, (params as any).connection) 8 | 9 | fetchOptions.headers = Object.assign({ Accept: 'application/json' }, this.options.headers, fetchOptions.headers) 10 | 11 | if (options.body) 12 | fetchOptions.body = options.body 13 | 14 | try { 15 | const response = await this.connection.raw(options.url, fetchOptions) 16 | const { _data, status } = response 17 | 18 | if (status === 204) 19 | return null 20 | return _data 21 | } 22 | catch (error: any) { 23 | throw error.data 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/hooks/0-prepare-query.ts: -------------------------------------------------------------------------------- 1 | import type { HookContext, NextFunction } from '@feathersjs/feathers' 2 | import { deepUnref } from '../utils/index.js' 3 | 4 | /** 5 | * deeply unrefs `params.query` 6 | */ 7 | export function unrefQuery() { 8 | return async (context: HookContext, next: NextFunction) => { 9 | if (context.params.value) 10 | context.params = deepUnref(context.params) 11 | 12 | if (context.params.query) 13 | context.params.query = deepUnref(context.params.query) 14 | 15 | if (context.method === 'find') { 16 | const query = context.params.query || {} 17 | if (query.$limit == null) 18 | query.$limit = context.service.store.defaultLimit 19 | 20 | if (query.$skip == null) 21 | query.$skip = 0 22 | 23 | context.params.query = query 24 | } 25 | 26 | next && await next() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/hooks/1-set-pending.ts: -------------------------------------------------------------------------------- 1 | import type { HookContext, NextFunction } from '@feathersjs/feathers' 2 | 3 | /** 4 | * Controls pending state 5 | */ 6 | export function setPending() { 7 | return async (context: HookContext, next: NextFunction) => { 8 | const store = context.service.store 9 | let unsetPending 10 | 11 | if (!store.isSsr) { 12 | const method = context.method === 'find' ? (context.params.query?.$limit === 0 ? 'count' : 'find') : context.method 13 | 14 | store.setPending(method, true) 15 | if (context.id != null && method !== 'get') 16 | store.setPendingById(context.id, method, true) 17 | 18 | const isTemp = context.data?.__isTemp 19 | const tempId = context.data?.__tempId 20 | if (isTemp && method === 'create') 21 | store.setPendingById(context.data.__tempId, method, true) 22 | 23 | unsetPending = () => { 24 | store.setPending(method, false) 25 | const id = context.id != null ? context.id : tempId 26 | if (id != null && method !== 'get') 27 | store.setPendingById(id, method, false) 28 | } 29 | } 30 | 31 | try { 32 | await next() 33 | } 34 | catch (error) { 35 | if (unsetPending) 36 | unsetPending() 37 | 38 | throw error 39 | } 40 | 41 | if (unsetPending) 42 | unsetPending() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/hooks/2-event-locks.ts: -------------------------------------------------------------------------------- 1 | import type { HookContext, NextFunction } from '@feathersjs/feathers' 2 | 3 | export function eventLocks() { 4 | return async (context: HookContext, next: NextFunction) => { 5 | const { id, method } = context 6 | const store = context.service.store 7 | const isLockableMethod = ['update', 'patch', 'remove'].includes(method) 8 | const eventNames: any = { 9 | update: 'updated', 10 | patch: 'patched', 11 | remove: 'removed', 12 | } 13 | const eventName = eventNames[method] 14 | 15 | if (isLockableMethod && id && !store.isSsr) 16 | store.toggleEventLock(id, eventName) 17 | 18 | await next() 19 | 20 | if (isLockableMethod && id && !store.isSsr) 21 | store.clearEventLock(id, eventName) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/hooks/3-sync-store.ts: -------------------------------------------------------------------------------- 1 | import type { HookContext, NextFunction } from '@feathersjs/feathers' 2 | import { restoreTempIds } from '../utils/index.js' 3 | 4 | export function syncStore() { 5 | return async (context: HookContext, next: NextFunction) => { 6 | const { method, params } = context 7 | const store = context.service.store 8 | 9 | if (method === 'patch' && params.data) 10 | context.data = params.data 11 | 12 | if (next) 13 | await next() 14 | 15 | if (!context.params.skipStore) { 16 | if (method === 'remove') { 17 | store.removeFromStore(context.result) 18 | } 19 | else if (method === 'create') { 20 | const restoredTempIds = restoreTempIds(context.data, context.result) 21 | context.result = store.createInStore(restoredTempIds) 22 | } 23 | else if (method === 'find' && Array.isArray(context.result.data)) { 24 | context.result.data = store.createInStore(context.result.data) 25 | } 26 | else { 27 | context.result = store.createInStore(context.result) 28 | } 29 | 30 | // Update pagination based on the qid 31 | if (method === 'find' && context.result.total) { 32 | const { qid = 'default', query, preserveSsr = false } = context.params 33 | store.updatePaginationForQuery({ qid, response: context.result, query, preserveSsr }) 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/hooks/4-model-instances.ts: -------------------------------------------------------------------------------- 1 | import type { HookContext, NextFunction } from '@feathersjs/feathers' 2 | import type { AnyData } from '../types.js' 3 | 4 | export function makeModelInstances() { 5 | return async (context: HookContext, next: NextFunction) => { 6 | if (next) 7 | await next() 8 | 9 | if (context.service.new) { 10 | if (Array.isArray(context.result?.data)) 11 | context.result.data = context.result.data.map((i: AnyData) => context.service.new(i)) 12 | 13 | else if (Array.isArray(context.result)) 14 | context.result = context.result.map((i: AnyData) => context.service.new(i)) 15 | 16 | else 17 | context.result = context.service.new(context.result) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/hooks/5-handle-find-ssr.ts: -------------------------------------------------------------------------------- 1 | import type { HookContext, Id, NextFunction } from '@feathersjs/feathers' 2 | 3 | /** 4 | * Assures that the client reuses SSR-provided data instead of re-making the same query. 5 | * 6 | * Checks the `store.pagination` object to see if a query's results came from SSR-provided data. 7 | * If the data was from SSR, the SSR'd data is used and then set to `fromSSR = false` to allow 8 | * normal queries to happen again. 9 | */ 10 | export function handleFindSsr() { 11 | return async (context: HookContext, next: NextFunction) => { 12 | const store = context.service.store 13 | 14 | if (context.method === 'find') { 15 | const { params } = context 16 | const info = store.getQueryInfo(params) 17 | const qidData = store.pagination[info.qid] 18 | const queryData = qidData?.[info.queryId] 19 | const pageData = queryData?.[info.pageId as string] 20 | 21 | if (pageData?.ssr) { 22 | context.result = { 23 | data: pageData.ids.map((id: Id) => store.getFromStore(id).value), 24 | limit: pageData.pageParams.$limit, 25 | skip: pageData.pageParams.$skip, 26 | total: queryData.total, 27 | fromSsr: true, 28 | } 29 | if (!params.preserveSsr) 30 | store.unflagSsr(params) 31 | } 32 | } 33 | 34 | if (next) 35 | await next() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/hooks/6-normalize-find.ts: -------------------------------------------------------------------------------- 1 | import type { HookContext, NextFunction } from '@feathersjs/feathers' 2 | import { hasOwn } from '../utils/index.js' 3 | 4 | /** 5 | * Normalizes two things 6 | * - pagination across all adapters, including @feathersjs/memory 7 | * - the find response so that it always holds data at `response.data` 8 | * @returns { data: AnyData[] } 9 | */ 10 | export function normalizeFind() { 11 | return async (context: HookContext, next?: NextFunction) => { 12 | // Client-side services, like feathers-memory, require paginate.default to be truthy. 13 | if (context.method === 'find') { 14 | const { params } = context 15 | const { query = {} } = params 16 | const isPaginated = params.paginate === true || hasOwn(query, '$limit') || hasOwn(query, '$skip') 17 | if (isPaginated) 18 | params.paginate = { default: true } 19 | } 20 | 21 | next && await next() 22 | 23 | // this makes sure it only affects finds that are not paginated and are not custom. 24 | // so the custom find responses fall through. 25 | if (context.method === 'find' && !context.result?.data && Array.isArray(context.result)) { 26 | context.result = { 27 | data: context.result, 28 | limit: context.params.$limit || context.result.length, 29 | skip: context.params.$skip || 0, 30 | total: context.result.length, 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/hooks/7-skip-get-if-exists.ts: -------------------------------------------------------------------------------- 1 | import type { HookContext, NextFunction } from '@feathersjs/feathers' 2 | 3 | export function skipGetIfExists() { 4 | return async (context: HookContext, next: NextFunction) => { 5 | const { params, id } = context 6 | const store = context.service.store 7 | 8 | if (context.method === 'get' && id != null) { 9 | const skipIfExists = params.skipGetIfExists || store.skipGetIfExists 10 | delete params.skipGetIfExists 11 | 12 | // If the records is already in store, return it 13 | const existingItem = store.getFromStore(context.id, params) 14 | if (existingItem && skipIfExists) 15 | context.result = existingItem 16 | } 17 | await next() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/hooks/8-patch-diffs.ts: -------------------------------------------------------------------------------- 1 | import type { HookContext, NextFunction } from '@feathersjs/feathers' 2 | import fastCopy from 'fast-copy' 3 | import { diff, pickDiff } from '../utils/index.js' 4 | 5 | export function patchDiffing() { 6 | return async (context: HookContext, next: NextFunction) => { 7 | const { method, data, params, id } = context 8 | const store = context.service.store 9 | 10 | let rollbackData: any 11 | let clone: any 12 | const shouldDiff = method === 'patch' && !params.data && (data.__isClone || params.diff) 13 | 14 | if (shouldDiff) { 15 | clone = data 16 | const original = store.getFromStore(id).value 17 | const diffedData = diff(original, clone, params.diff) 18 | rollbackData = fastCopy(original) 19 | 20 | // Do eager updating. 21 | if (params.eager !== false) 22 | data.commit(diffedData) 23 | 24 | // Always include matching values from `params.with`. 25 | if (params.with) { 26 | const dataFromWith = pickDiff(clone, params.with) 27 | // If params.with was an object, merge the values into dataFromWith 28 | if (typeof params.with !== 'string' && !Array.isArray(params.with)) 29 | Object.assign(dataFromWith, params.with) 30 | 31 | Object.assign(diffedData, dataFromWith) 32 | } 33 | 34 | context.data = diffedData 35 | 36 | // If diff is empty, return the clone without making a request. 37 | if (Object.keys(context.data).length === 0) 38 | context.result = clone 39 | } 40 | else { 41 | context.data = fastCopy(data) 42 | } 43 | 44 | try { 45 | await next() 46 | } 47 | catch (error) { 48 | if (shouldDiff) { 49 | // If saving fails, reverse the eager update 50 | clone && clone.commit(rollbackData) 51 | } 52 | throw error 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/hooks/9-ssr-qid-cache.ts: -------------------------------------------------------------------------------- 1 | import type { HookContext, NextFunction } from '@feathersjs/feathers' 2 | 3 | /** 4 | * Prevents duplicate requests by cacheing results from qid-enabled queries. 5 | * Clears the cache on the client after 500ms. 6 | */ 7 | export function handleQidCache() { 8 | return async (context: HookContext, next: NextFunction) => { 9 | const { params } = context 10 | const store = context.service.store 11 | 12 | // Reuse any cached results for the same qid 13 | // this prevents duplicate requests on the server and client. 14 | // The client will cache the result for 500ms. It is assumed that all startup 15 | // requests will be completed within 500ms. 16 | if (params.qid) { 17 | const cached = store.getQid(params.qid) 18 | 19 | // specifically check for undefined because null is a valid value 20 | if (cached !== undefined) { 21 | // on the client, schedule the value to be removed from the cache after 500ms 22 | if (!store.isSsr) { 23 | setTimeout(() => { 24 | store.clearQid(params.qid) 25 | }, 500) 26 | } 27 | 28 | // set the result to prevent the request 29 | context.result = cached 30 | return await next() 31 | } 32 | } 33 | 34 | await next() 35 | 36 | // on the ssr server, cache the result if params.qid is set 37 | if (params.qid && store.isSsr) 38 | store.setQid(params.qid, context.result) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | import { unrefQuery } from './0-prepare-query.js' 2 | import { setPending } from './1-set-pending.js' 3 | import { eventLocks } from './2-event-locks.js' 4 | import { syncStore } from './3-sync-store.js' 5 | import { makeModelInstances } from './4-model-instances.js' 6 | import { handleFindSsr } from './5-handle-find-ssr.js' 7 | import { normalizeFind } from './6-normalize-find.js' 8 | import { skipGetIfExists } from './7-skip-get-if-exists.js' 9 | import { patchDiffing } from './8-patch-diffs.js' 10 | import { handleQidCache } from './9-ssr-qid-cache.js' 11 | 12 | export { syncStore, setPending, eventLocks, normalizeFind, skipGetIfExists, makeModelInstances } 13 | 14 | export function feathersPiniaHooks() { 15 | return [ 16 | unrefQuery(), 17 | setPending(), 18 | eventLocks(), 19 | syncStore(), 20 | makeModelInstances(), 21 | handleFindSsr(), 22 | normalizeFind(), 23 | skipGetIfExists(), 24 | patchDiffing(), 25 | handleQidCache(), 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types.js' 2 | 3 | export { createPiniaClient } from './create-pinia-client.js' 4 | export type * from './create-pinia-client.js' 5 | export { PiniaService } from './create-pinia-service' 6 | export { OFetch } from './feathers-ofetch.js' 7 | 8 | export { feathersPiniaAutoImport } from './unplugin-auto-import-preset.js' 9 | 10 | export * from './hooks/index.js' 11 | export * from './localstorage/index.js' 12 | export * from './modeling/index.js' 13 | export * from './use-auth/index.js' 14 | export * from './use-find-get/index.js' 15 | export * from './stores/index.js' 16 | export * from './composables/index.js' 17 | export * from './utils/utils.js' 18 | export * from './utils/service-utils.js' 19 | export * from './custom-operators/index.js' 20 | export { useInstanceDefaults, defineGetters, defineSetters, defineValues } from './utils/index.js' 21 | -------------------------------------------------------------------------------- /src/isomorphic-mongo-objectid.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'isomorphic-mongo-objectid' 2 | -------------------------------------------------------------------------------- /src/localstorage/clear-storage.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Clears all services from localStorage. You might use this when a user 3 | * logs out to make sure their data doesn't persist for the next user. 4 | * 5 | * @param storage an object using the Storage interface 6 | */ 7 | export function clearStorage(storage: Storage = window.localStorage) { 8 | const prefix = 'service:' // replace this with your prefix 9 | for (let i = 0; i < storage.length; i++) { 10 | const key = storage.key(i) 11 | if (key?.startsWith(prefix)) 12 | storage.removeItem(key) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/localstorage/index.ts: -------------------------------------------------------------------------------- 1 | export * from './storage-sync.js' 2 | export * from './clear-storage.js' 3 | -------------------------------------------------------------------------------- /src/localstorage/storage-sync.ts: -------------------------------------------------------------------------------- 1 | import { computed, watch } from 'vue-demi' 2 | import { _ } from '@feathersjs/commons' 3 | import debounce from 'just-debounce' 4 | 5 | // Writes data to localStorage 6 | export function writeToStorage(id: string, data: any, storage: any) { 7 | const compressed = JSON.stringify(data) 8 | storage.setItem(id, compressed) 9 | } 10 | 11 | // Moves data from localStorage into the store 12 | export function hydrateStore(store: any, storage: any) { 13 | const data = storage.getItem(store.$id) 14 | if (data) { 15 | const hydrationData = JSON.parse(data as string) || {} 16 | Object.assign(store, hydrationData) 17 | } 18 | } 19 | 20 | /** 21 | * 22 | * @param store pinia store 23 | * @param keys an array of keys to watch and write to localStorage. 24 | */ 25 | export function syncWithStorage(store: any, stateKeys: Array, storage: Storage = window.localStorage) { 26 | hydrateStore(store, storage) 27 | 28 | const debouncedWrite = debounce(writeToStorage, 500) 29 | const toWatch = computed(() => _.pick(store, ...stateKeys)) 30 | 31 | watch(toWatch, val => debouncedWrite(store.$id, val, storage), { deep: true }) 32 | } 33 | -------------------------------------------------------------------------------- /src/modeling/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types.js' 2 | 3 | export { useServiceInstance } from './use-feathers-instance.js' 4 | export { useModelInstance } from './use-model-instance.js' 5 | export { storeAssociated } from './store-associated.js' 6 | -------------------------------------------------------------------------------- /src/modeling/store-associated.ts: -------------------------------------------------------------------------------- 1 | import { defineValues } from '../utils/index.js' 2 | 3 | export function storeAssociated(this: any, data: any, config: Record) { 4 | const updatedValues: any = {} 5 | Object.keys(config).forEach((key) => { 6 | const related = data[key] 7 | const servicePath = config[key] 8 | const service = this.service(servicePath) 9 | if (!service) 10 | console.error(`there is no service at path ${servicePath}. Check your storeAssociated config`, data, config) 11 | if (related && service) { 12 | const created = service.createInStore(related) 13 | updatedValues[key] = created 14 | } 15 | }) 16 | 17 | defineValues(data, updatedValues) 18 | } 19 | -------------------------------------------------------------------------------- /src/modeling/types.ts: -------------------------------------------------------------------------------- 1 | import type { AnyData, PatchParams } from '../types.js' 2 | import type { CloneOptions } from '../stores/index.js' 3 | 4 | export interface BaseModelData { 5 | /** 6 | * Indicates if this instance is a clone.. It will be 7 | * 8 | * - `true` after calling `instance.clone()` 9 | * - `false` after calling `instance.commit()`. 10 | * 11 | * Should not be set manually when creating new instances. 12 | */ 13 | __isClone?: boolean 14 | /** 15 | * If no idField is specified, `__tempId` can be manually specified, otherwise a random one will be added for you. 16 | * The automatically-added `__tempId` values are valid ObjectId strings. 17 | */ 18 | __tempId?: string 19 | } 20 | 21 | export interface StoreInstanceProps { 22 | /** 23 | * The name of the Model function 24 | */ 25 | readonly __modelName: string 26 | 27 | // see `BaseModelData.__isClone` 28 | readonly __isClone: boolean 29 | /** 30 | * The attribute on the data holding the unique id property. It will match whatever was provided in the Model options. 31 | * This value should match the API service. General `idField` values are as follows: 32 | * 33 | * - `id` for SQL databases 34 | * - `_id` for MongoDB 35 | */ 36 | readonly __idField: string 37 | 38 | // see `BaseModelData.__tempId` 39 | readonly __tempId: string 40 | /** 41 | * A boolean indicating if the instance is a temp. Will be `true` if the instance does not have an idField. This is 42 | * the only reliable way to determine if a record is a temp or not, since after calling `temp.save`, the temp will 43 | * have both a `__tempId` and a real idField value. 44 | */ 45 | readonly __isTemp: boolean 46 | /** 47 | * Returns the item's clone from the store, if one exists. 48 | */ 49 | hasClone(this: N): N | null 50 | /** 51 | * Creates a copy of an item or temp record. The copy will have `__isClone` set to `true` and will be added to the 52 | * Model's clone storage. If not already stored, the original item will be added to the appropriate store. 53 | * @param data 54 | * @param options 55 | */ 56 | clone(this: N, data?: Partial, options?: CloneOptions): N 57 | /** 58 | * Copies a clone's data onto the original item or temp record. 59 | * @param data 60 | * @param options 61 | */ 62 | commit(this: N, data?: Partial, options?: CloneOptions): N 63 | /** 64 | * Resets a clone's data to match the original item or temp record. If additional properties were added to the clone, 65 | * they will be removed to exactly match the original. 66 | * @param data 67 | * @param options 68 | */ 69 | reset(this: N, data?: Partial, options?: CloneOptions): N 70 | /** 71 | * Adds the current instance to the appropriate store. If the instance is a clone, it will be added to `clones`. If it 72 | * has an `idField`, it will be added to items, otherwise it will be added to temps. 73 | */ 74 | createInStore(this: N): N 75 | /** 76 | * Removes the current instance from items, temps, and clones. 77 | */ 78 | removeFromStore(this: N): N 79 | } 80 | 81 | export type ModelInstanceData = Partial 82 | export type ModelInstance = ModelInstanceData & StoreInstanceProps 83 | 84 | export interface ServiceInstanceProps = PatchParams> { 85 | readonly isSavePending: boolean 86 | readonly isCreatePending: boolean 87 | readonly isPatchPending: boolean 88 | readonly isRemovePending: boolean 89 | save: (this: N, params?: P) => Promise 90 | create: (this: ModelInstance, params?: P) => Promise 91 | patch: (this: ModelInstance, params?: P) => Promise 92 | remove: (this: ModelInstance, params?: P) => Promise 93 | } 94 | export type ServiceInstance = ModelInstanceData & 95 | StoreInstanceProps & 96 | ServiceInstanceProps 97 | -------------------------------------------------------------------------------- /src/modeling/use-feathers-instance.ts: -------------------------------------------------------------------------------- 1 | import { BadRequest } from '@feathersjs/errors' 2 | import type { FeathersService, Params } from '@feathersjs/feathers' 3 | import type { AnyData } from '../types.js' 4 | import { defineGetters, defineValues } from '../utils/define-properties' 5 | import type { PiniaService } from '../create-pinia-service.js' 6 | import type { ServiceInstanceProps } from './types.js' 7 | 8 | export type Service = FeathersService | PiniaService 9 | 10 | export interface useServiceInstanceOptions { 11 | service: S 12 | store: any 13 | } 14 | 15 | export function useServiceInstance(data: M, 16 | options: useServiceInstanceOptions) { 17 | if (data.__isServiceInstance) 18 | return data 19 | 20 | const { service, store } = options 21 | const merge = (data: M, toMerge: AnyData) => Object.assign(data, toMerge) 22 | 23 | defineGetters(data, { 24 | isPending() { 25 | return this.isCreatePending || this.isPatchPending || this.isRemovePending 26 | }, 27 | isSavePending() { 28 | return this.isCreatePending || this.isPatchPending 29 | }, 30 | isCreatePending() { 31 | return !!(store.createPendingById[this[store.idField]] || store.createPendingById[this.__tempId]) 32 | }, 33 | isPatchPending() { 34 | return !!store.patchPendingById[this[store.idField]] 35 | }, 36 | isRemovePending() { 37 | return !!store.removePendingById[this[store.idField]] 38 | }, 39 | } as any) 40 | 41 | defineValues(data, { 42 | __isServiceInstance: true, 43 | save(this: M, params?: P) { 44 | const id = this[store.idField] 45 | return id != null ? this.patch(params) : this.create(params) 46 | }, 47 | create(this: M, params?: P): Promise { 48 | return service.create(this, params).then(result => merge(this, result)) 49 | }, 50 | patch(this: M, params?: P): Promise { 51 | const id = this[store.idField] 52 | if (id === undefined) 53 | throw new BadRequest('the item has no id') 54 | return (service as FeathersService).patch(id as any, this as any, params as any).then(result => merge(this, result)) 55 | }, 56 | remove(this: M, params?: P): Promise { 57 | if (this.__isTemp) { 58 | store.removeFromStore(this.__tempId) 59 | return Promise.resolve(this) 60 | } 61 | else { 62 | const id = this[store.idField] 63 | return (service as FeathersService).remove(id, params).then(result => merge(this, result)) 64 | } 65 | }, 66 | }) 67 | 68 | return data as M & ServiceInstanceProps 69 | } 70 | -------------------------------------------------------------------------------- /src/modeling/use-model-instance.ts: -------------------------------------------------------------------------------- 1 | import ObjectID from 'isomorphic-mongo-objectid' 2 | import type { Ref } from 'vue-demi' 3 | import type { CloneOptions } from '../stores/index.js' 4 | import type { AnyData, ById, Params } from '../types.js' 5 | import { defineValues } from '../utils/define-properties' 6 | import type { BaseModelData, ModelInstanceData, StoreInstanceProps } from './types.js' 7 | 8 | interface UseModelInstanceOptions { 9 | idField: string 10 | clonesById: Ref> 11 | clone: (item: M, data?: Record, options?: CloneOptions) => M 12 | commit: (item: M, data?: Partial) => M 13 | reset: (item: M, data?: Record) => M 14 | createInStore: (data: M | M[]) => M | M[] 15 | removeFromStore: (data: M | M[] | null, params?: Params) => M | M[] | null 16 | } 17 | 18 | export function useModelInstance(data: ModelInstanceData, 19 | options: UseModelInstanceOptions) { 20 | if (data.__isStoreInstance) 21 | return data 22 | 23 | const { idField, clonesById, clone, commit, reset, createInStore, removeFromStore } = options 24 | const __isClone = data.__isClone || false 25 | 26 | // instance.__isTemp 27 | Object.defineProperty(data, '__isTemp', { 28 | configurable: true, 29 | enumerable: false, 30 | get() { 31 | return this[this.__idField] == null 32 | }, 33 | }) 34 | 35 | // BaseModel properties 36 | const asBaseModel = defineValues(data, { 37 | __isStoreInstance: true, 38 | __isClone, 39 | __idField: idField, 40 | __tempId: data[idField] == null && data.__tempId == null ? new ObjectID().toString() : data.__tempId || undefined, 41 | hasClone(this: M) { 42 | const id = this[this.__idField] || this.__tempId 43 | const item = clonesById.value[id] 44 | return item || null 45 | }, 46 | clone(this: M, data: Partial = {}, options: CloneOptions = {}) { 47 | const item = clone(this, data, options) 48 | return item 49 | }, 50 | commit(this: M, data: Partial = {}) { 51 | const item = commit(this, data) 52 | return item 53 | }, 54 | reset(this: M, data: Partial = {}) { 55 | const item = reset(this, data) 56 | return item 57 | }, 58 | createInStore(this: M) { 59 | const item = createInStore(this) 60 | return item 61 | }, 62 | removeFromStore(this: M) { 63 | const item = removeFromStore(this) 64 | return item 65 | }, 66 | }) as M & BaseModelData & StoreInstanceProps 67 | 68 | return asBaseModel 69 | } 70 | -------------------------------------------------------------------------------- /src/stores/all-storage-types.ts: -------------------------------------------------------------------------------- 1 | import fastCopy from 'fast-copy' 2 | import type { AnyData, MakeCopyOptions } from '../types.js' 3 | import { defineValues } from '../utils/index.js' 4 | import { useServiceTemps } from './temps.js' 5 | import { useServiceClones } from './clones.js' 6 | import { useServiceStorage } from './storage.js' 7 | 8 | interface UseAllStorageOptions { 9 | getIdField: (val: AnyData) => any 10 | setupInstance: any 11 | } 12 | 13 | export function useAllStorageTypes(options: UseAllStorageOptions) { 14 | const { getIdField, setupInstance } = options 15 | 16 | /** 17 | * Makes a copy of the Model instance with __isClone properly set 18 | * Private 19 | */ 20 | const makeCopy = (item: M, data: AnyData = {}, { isClone }: MakeCopyOptions) => { 21 | const copied = fastCopy(item) 22 | Object.assign(copied, data) 23 | // instance.__isTemp 24 | Object.defineProperty(copied, '__isTemp', { 25 | configurable: true, 26 | enumerable: false, 27 | get() { 28 | return this[this.__idField] == null 29 | }, 30 | }) 31 | const withExtras = defineValues(copied, { 32 | __isClone: isClone, 33 | __tempId: item.__tempId, 34 | }) 35 | return withExtras 36 | } 37 | 38 | // item storage 39 | const itemStorage = useServiceStorage({ 40 | getId: getIdField, 41 | beforeWrite: setupInstance, 42 | onRead: setupInstance, 43 | }) 44 | 45 | // temp item storage 46 | const { tempStorage, moveTempToItems } = useServiceTemps({ 47 | getId: item => item.__tempId, 48 | itemStorage, 49 | beforeWrite: setupInstance, 50 | onRead: setupInstance, 51 | }) 52 | 53 | // clones 54 | const { cloneStorage, clone, commit, reset, markAsClone } = useServiceClones({ 55 | itemStorage, 56 | tempStorage, 57 | makeCopy, 58 | beforeWrite: (item) => { 59 | markAsClone(item) 60 | return setupInstance(item) 61 | }, 62 | onRead: setupInstance, 63 | }) 64 | 65 | /** 66 | * Stores the provided item in the correct storage (itemStorage, tempStorage, or cloneStorage). 67 | * If an item has both an id and a tempId, it gets moved from tempStorage to itemStorage. 68 | * Private 69 | */ 70 | const addItemToStorage = (item: M) => { 71 | const id = getIdField(item) 72 | item = setupInstance(item) 73 | 74 | if (item.__isClone) 75 | return cloneStorage.merge(item) 76 | else if (id != null && item.__tempId != null) 77 | return moveTempToItems(item) 78 | else if (id != null) 79 | return itemStorage.merge(item) 80 | else if (tempStorage && item.__tempId != null) 81 | return tempStorage?.merge(item) 82 | 83 | return itemStorage.merge(item) 84 | } 85 | 86 | return { 87 | itemStorage, 88 | tempStorage, 89 | cloneStorage, 90 | clone, 91 | commit, 92 | reset, 93 | addItemToStorage, 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/stores/event-locks.ts: -------------------------------------------------------------------------------- 1 | import type { Id } from '@feathersjs/feathers' 2 | import { del, reactive, set } from 'vue-demi' 3 | import type { MaybeArray } from '../types.js' 4 | import { getArray } from '../utils/index.js' 5 | import type { EventLocks, EventName } from './types.js' 6 | 7 | export function useServiceEventLocks() { 8 | const eventLocks = reactive({ 9 | created: {}, 10 | patched: {}, 11 | updated: {}, 12 | removed: {}, 13 | }) 14 | 15 | function toggleEventLock(data: MaybeArray, event: EventName) { 16 | const { items: ids } = getArray(data) 17 | ids.forEach((id) => { 18 | const currentLock = eventLocks[event][id] 19 | if (currentLock) { 20 | clearEventLock(data, event) 21 | } 22 | else { 23 | set(eventLocks[event], id, true) 24 | // auto-clear event lock after 250 ms 25 | setTimeout(() => { 26 | clearEventLock(data, event) 27 | }, 250) 28 | } 29 | }) 30 | } 31 | function clearEventLock(data: MaybeArray, event: EventName) { 32 | const { items: ids } = getArray(data) 33 | ids.forEach((id) => { 34 | del(eventLocks[event], id) 35 | }) 36 | } 37 | return { eventLocks, toggleEventLock, clearEventLock } 38 | } 39 | -------------------------------------------------------------------------------- /src/stores/event-queue-promise.ts: -------------------------------------------------------------------------------- 1 | import { watch } from 'vue-demi' 2 | 3 | type EventName = 'created' | 'updated' | 'patched' | 'removed' 4 | 5 | interface QueuePromiseState { 6 | promise: Promise 7 | isResolved: boolean 8 | getter: 'isCreatePending' | 'isUpdatePending' | 'isPatchPending' | 'isRemovePending' 9 | } 10 | 11 | const events = ['created', 'updated', 'patched', 'removed'] 12 | const state: { [key: string]: QueuePromiseState } = {} 13 | 14 | export function makeGetterName(event: EventName) { 15 | return `is${event.slice(0, 1).toUpperCase()}${event.slice(1, event.length - 1)}Pending` 16 | } 17 | 18 | export function makeState(event: EventName) { 19 | return { 20 | promise: null, 21 | isResolved: false, 22 | getter: makeGetterName(event), 23 | } 24 | } 25 | export function resetState() { 26 | events.forEach((e) => { 27 | delete state[e] 28 | }) 29 | } 30 | /** 31 | * Creates or reuses a promise for each event type, like "created". The promise 32 | * resolves when the matching `isPending` attribute, like "isCreatePending" becomes 33 | * false. 34 | * @param store 35 | * @param event 36 | * @returns 37 | */ 38 | export function useQueuePromise(store: any, event: EventName) { 39 | state[event] = state[event] || makeState(event) 40 | 41 | if (!state[event].promise || state[event].isResolved) { 42 | state[event].promise = new Promise((resolve) => { 43 | const stopWatching = watch( 44 | () => store[state[event].getter], 45 | async (isPending) => { 46 | if (!isPending) { 47 | setTimeout(() => { 48 | stopWatching() 49 | state[event].isResolved = true 50 | resolve(state[event].isResolved) 51 | }, 0) 52 | } 53 | }, 54 | { immediate: true }, 55 | ) 56 | }) 57 | } 58 | return state[event].promise 59 | } 60 | -------------------------------------------------------------------------------- /src/stores/events.ts: -------------------------------------------------------------------------------- 1 | import type { FeathersService } from '@feathersjs/feathers' 2 | import { del, ref, set } from 'vue-demi' 3 | import _debounce from 'just-debounce' 4 | import type { AnyData } from '../types.js' 5 | import { convertData, getId, hasOwn } from '../utils/index.js' 6 | import type { PiniaService } from '../create-pinia-service.js' 7 | import type { HandleEvents, HandledEvents } from './types.js' 8 | 9 | interface UseServiceStoreEventsOptions { 10 | service: PiniaService> 11 | debounceEventsTime?: number 12 | debounceEventsGuarantee?: boolean 13 | handleEvents?: HandleEvents 14 | } 15 | 16 | export function useServiceEvents(options: UseServiceStoreEventsOptions) { 17 | if (!options.service || options.handleEvents === false) 18 | return 19 | 20 | const service = options.service 21 | 22 | const addOrUpdateById = ref({}) 23 | const removeItemsById = ref({}) 24 | 25 | const flushAddOrUpdateQueue = _debounce( 26 | async () => { 27 | const values = Object.values(addOrUpdateById.value) 28 | if (values.length === 0) 29 | return 30 | service.store.createInStore(values) 31 | addOrUpdateById.value = {} 32 | }, 33 | options.debounceEventsTime || 20, 34 | undefined, 35 | options.debounceEventsGuarantee, 36 | ) 37 | 38 | function enqueueAddOrUpdate(item: any) { 39 | const id = getId(item, service.store.idField) 40 | if (!id) 41 | return 42 | 43 | set(addOrUpdateById, id, item) 44 | 45 | if (hasOwn(removeItemsById.value, id)) 46 | del(removeItemsById, id) 47 | 48 | flushAddOrUpdateQueue() 49 | } 50 | 51 | const flushRemoveItemQueue = _debounce( 52 | () => { 53 | const values = Object.values(removeItemsById.value) 54 | if (values.length === 0) 55 | return 56 | service.store.removeFromStore(values) 57 | removeItemsById.value = {} 58 | }, 59 | options.debounceEventsTime || 20, 60 | undefined, 61 | options.debounceEventsGuarantee, 62 | ) 63 | 64 | function enqueueRemoval(item: any) { 65 | const id = getId(item, service.store.idField) 66 | if (!id) 67 | return 68 | 69 | set(removeItemsById, id, item) 70 | 71 | if (hasOwn(addOrUpdateById.value, id)) 72 | del(addOrUpdateById.value, id) 73 | 74 | flushRemoveItemQueue() 75 | } 76 | 77 | function handleEvent(eventName: HandledEvents, item: any) { 78 | const handler = (options.handleEvents as any)?.[eventName] 79 | if (handler === false) 80 | return 81 | 82 | /** 83 | * For `created` events, we don't know the id since it gets assigned on the server. Also, since `created` events 84 | * arrive before the `create` response, we only act on other events. For all other events, toggle the event lock. 85 | */ 86 | const id = getId(item, service.store.idField) 87 | if (eventName !== 'created' && service.store.eventLocks[eventName][id]) { 88 | service.store.toggleEventLock(id, eventName) 89 | return 90 | } 91 | 92 | if (handler) { 93 | const handled = handler(item, { service }) 94 | if (!handled) 95 | return 96 | } 97 | 98 | if (!options.debounceEventsTime) 99 | eventName === 'removed' ? service.store.removeFromStore(item) : service.store.createInStore(item) 100 | else eventName === 'removed' ? enqueueRemoval(item) : enqueueAddOrUpdate(item) 101 | } 102 | 103 | // Listen to socket events when available. 104 | service.on('created', (item: any) => { 105 | const data = convertData(service, item) 106 | handleEvent('created', data) 107 | }) 108 | service.on('updated', (item: any) => { 109 | const data = convertData(service, item) 110 | handleEvent('updated', data) 111 | }) 112 | service.on('patched', (item: any) => { 113 | const data = convertData(service, item) 114 | handleEvent('patched', data) 115 | }) 116 | service.on('removed', (item: any) => { 117 | const data = convertData(service, item) 118 | handleEvent('removed', data) 119 | }) 120 | } 121 | -------------------------------------------------------------------------------- /src/stores/filter-query.ts: -------------------------------------------------------------------------------- 1 | import { _ } from '@feathersjs/commons' 2 | import type { Query } from '@feathersjs/feathers' 3 | import type { FilterQueryOptions, FilterSettings, PaginationParams } from '@feathersjs/adapter-commons' 4 | 5 | const parse = (value: any) => (typeof value !== 'undefined' ? Number.parseInt(value, 10) : value) 6 | 7 | function getFilters(query: Query, settings: any) { 8 | const filterNames = Object.keys(settings.filters) 9 | 10 | return filterNames.reduce((current, key) => { 11 | const queryValue = query[key] 12 | const filter = settings.filters[key] 13 | 14 | if (filter) { 15 | const value = typeof filter === 'function' ? filter(queryValue, settings) : queryValue 16 | 17 | if (value !== undefined) 18 | current[key] = value 19 | } 20 | 21 | return current 22 | }, {} as { [key: string]: any }) 23 | } 24 | 25 | function getQuery(query: Query) { 26 | const keys = Object.keys(query).concat(Object.getOwnPropertySymbols(query) as any as string[]) 27 | 28 | return keys.reduce((rebuiltQuery, key) => { 29 | if (!(typeof key === 'string' && key.startsWith('$'))) 30 | rebuiltQuery[key] = query[key] 31 | 32 | return rebuiltQuery 33 | }, {} as Query) 34 | } 35 | 36 | /** 37 | * Returns the converted `$limit` value based on the `paginate` configuration. 38 | * @param _limit The limit value 39 | * @param paginate The pagination options 40 | * @returns The converted $limit value 41 | */ 42 | export function getLimit(_limit: any, paginate?: PaginationParams) { 43 | const limit = parse(_limit) 44 | 45 | if (paginate && (paginate.default || paginate.max)) { 46 | const base = paginate.default || 0 47 | const lower = typeof limit === 'number' && !Number.isNaN(limit) && limit >= 0 ? limit : base 48 | const upper = typeof paginate.max === 'number' ? paginate.max : Number.MAX_VALUE 49 | 50 | return Math.min(lower, upper) 51 | } 52 | 53 | return limit 54 | } 55 | 56 | export const OPERATORS = ['$in', '$nin', '$lt', '$lte', '$gt', '$gte', '$ne', '$or'] 57 | 58 | export const FILTERS: FilterSettings = { 59 | $skip: (value: any) => parse(value), 60 | $sort: (sort: any): { [key: string]: number } => { 61 | if (typeof sort !== 'object' || Array.isArray(sort)) 62 | return sort 63 | 64 | return Object.keys(sort).reduce((result, key) => { 65 | result[key] = typeof sort[key] === 'object' ? sort[key] : parse(sort[key]) 66 | 67 | return result 68 | }, {} as { [key: string]: number }) 69 | }, 70 | $limit: (_limit: any, { paginate }: FilterQueryOptions) => getLimit(_limit, paginate), 71 | $select: (select: any) => { 72 | if (Array.isArray(select)) 73 | return select.map(current => `${current}`) 74 | 75 | return select 76 | }, 77 | $or: (or: any) => or, 78 | $and: (and: any) => and, 79 | } 80 | 81 | /** 82 | * Converts Feathers special query parameters and pagination settings 83 | * and returns them separately as `filters` and the rest of the query 84 | * as `query`. `options` also gets passed the pagination settings and 85 | * a list of additional `operators` to allow when querying properties. 86 | * 87 | * @param query The initial query 88 | * @param options Options for filtering the query 89 | * @returns An object with `query` which contains the query without `filters` 90 | * and `filters` which contains the converted values for each filter. 91 | */ 92 | export function filterQuery(_query: Query, options: FilterQueryOptions = {}) { 93 | const query = _query || {} 94 | const settings = { 95 | ...options, 96 | filters: { 97 | ...FILTERS, 98 | ...options.filters, 99 | }, 100 | operators: OPERATORS.concat(options.operators || []), 101 | } 102 | 103 | return { 104 | filters: getFilters(query, settings), 105 | query: getQuery(query), 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/stores/index.ts: -------------------------------------------------------------------------------- 1 | export { useDataStore, type UseDataStoreOptions } from './use-data-store' 2 | export { useServiceStore, type UseServiceStoreOptions } from './use-service-store' 3 | 4 | export { useQueuePromise } from './event-queue-promise' 5 | export { useServiceLocal } from './local-queries' 6 | export { useServiceTemps } from './temps' 7 | export { useServiceEvents } from './events' 8 | export { useServiceClones } from './clones' 9 | export { useServicePending } from './pending' 10 | export { useServiceStorage } from './storage' 11 | export { useAllStorageTypes } from './all-storage-types' 12 | export { useServiceEventLocks } from './event-locks' 13 | export { useServicePagination } from './pagination' 14 | 15 | export * from './types' 16 | -------------------------------------------------------------------------------- /src/stores/pagination.ts: -------------------------------------------------------------------------------- 1 | import type { ComputedRef, Ref } from 'vue-demi' 2 | import { ref, set } from 'vue-demi' 3 | import stringify from 'fast-json-stable-stringify' 4 | import { _ } from '@feathersjs/commons/lib' 5 | import { deepUnref, getId, hasOwn } from '../utils/index.js' 6 | import type { Params, Query, QueryInfo } from '../types.js' 7 | import type { PaginationState, UpdatePaginationForQueryOptions } from './types.js' 8 | 9 | export interface UseServicePagination { 10 | idField: string 11 | isSsr: ComputedRef 12 | defaultLimit?: number 13 | } 14 | 15 | export function useServicePagination(options: UseServicePagination) { 16 | const { idField, isSsr } = options 17 | const defaultLimit = options.defaultLimit || 10 18 | 19 | const pagination = ref({}) as Ref 20 | 21 | function clearPagination() { 22 | const { defaultLimit, defaultSkip } = pagination.value 23 | pagination.value = { defaultLimit, defaultSkip } as any 24 | } 25 | 26 | /** 27 | * Stores pagination data on state.pagination based on the query identifier 28 | * (qid) The qid must be manually assigned to `params.qid` 29 | */ 30 | function updatePaginationForQuery({ 31 | qid, 32 | response, 33 | query = {}, 34 | preserveSsr = false, 35 | }: UpdatePaginationForQueryOptions) { 36 | const { data, total } = response 37 | const ids = data.map((i: any) => getId(i, idField)) 38 | const queriedAt = new Date().getTime() 39 | const { queryId, queryParams, pageId, pageParams } = getQueryInfo({ qid, query }) 40 | 41 | if (!pagination.value[qid]) 42 | set(pagination.value, qid, {}) 43 | 44 | if (!hasOwn(query, '$limit') && hasOwn(response, 'limit')) 45 | set(pagination.value, 'defaultLimit', response.limit) 46 | 47 | if (!hasOwn(query, '$skip') && hasOwn(response, 'skip')) 48 | set(pagination.value, 'defaultSkip', response.skip) 49 | 50 | const mostRecent = { 51 | query, 52 | queryId, 53 | queryParams, 54 | pageId, 55 | pageParams, 56 | queriedAt, 57 | total, 58 | } 59 | 60 | const existingPageData = pagination.value[qid]?.[queryId]?.[pageId as string] 61 | 62 | const qidData = pagination.value[qid] || {} 63 | Object.assign(qidData, { mostRecent }) 64 | 65 | set(qidData, queryId, qidData[queryId] || {}) 66 | const queryData = { 67 | total, 68 | queryParams, 69 | } 70 | 71 | set(qidData, queryId, Object.assign({}, qidData[queryId], queryData)) 72 | 73 | const ssr = preserveSsr ? existingPageData?.ssr : isSsr.value 74 | 75 | const pageData = { 76 | [pageId as string]: { pageParams, ids, queriedAt, ssr: !!ssr }, 77 | } 78 | 79 | Object.assign(qidData[queryId], pageData) 80 | 81 | const newState = Object.assign({}, pagination.value[qid], qidData) 82 | 83 | set(pagination.value, qid, newState) 84 | } 85 | 86 | function unflagSsr(params: Params) { 87 | const queryInfo = getQueryInfo(params) 88 | const { qid, queryId, pageId } = queryInfo 89 | 90 | const pageData = pagination.value[qid]?.[queryId]?.[pageId as string] 91 | pageData.ssr = false 92 | } 93 | 94 | function getQueryInfo(_params: Params): QueryInfo { 95 | const params = deepUnref(_params) 96 | const { query = {} } = params 97 | const qid = params.qid || 'default' 98 | const $limit = query?.$limit || defaultLimit 99 | const $skip = query?.$skip || 0 100 | 101 | const pageParams = $limit !== undefined ? { $limit, $skip } : undefined 102 | const pageId = pageParams ? stringify(pageParams) : undefined 103 | 104 | const queryParams = _.omit(query, '$limit', '$skip') 105 | const queryId = stringify(queryParams) 106 | 107 | return { 108 | qid, 109 | query, 110 | queryId, 111 | queryParams, 112 | pageParams, 113 | pageId, 114 | isExpired: false, 115 | } 116 | } 117 | 118 | return { 119 | pagination, 120 | updatePaginationForQuery, 121 | unflagSsr, 122 | getQueryInfo, 123 | clearPagination, 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/stores/pending.ts: -------------------------------------------------------------------------------- 1 | import type { NullableId } from '@feathersjs/feathers' 2 | import type { Ref } from 'vue-demi' 3 | import { computed, del, ref, set } from 'vue-demi' 4 | import type { RequestTypeById } from './types.js' 5 | 6 | function defaultPending() { 7 | return { 8 | find: 0, 9 | count: 0, 10 | get: 0, 11 | create: 0, 12 | update: 0, 13 | patch: 0, 14 | remove: 0, 15 | } 16 | } 17 | 18 | export function useServicePending() { 19 | const isPending = ref(defaultPending()) 20 | 21 | const createPendingById = ref({}) as Ref> 22 | const updatePendingById = ref({}) as Ref> 23 | const patchPendingById = ref({}) as Ref> 24 | const removePendingById = ref({}) as Ref> 25 | 26 | const isFindPending = computed(() => { 27 | return isPending.value.find > 0 28 | }) 29 | 30 | const isCountPending = computed(() => { 31 | return isPending.value.count > 0 32 | }) 33 | 34 | const isGetPending = computed(() => { 35 | return isPending.value.get > 0 36 | }) 37 | 38 | const isCreatePending = computed(() => { 39 | return isPending.value.create > 0 || Object.keys(createPendingById.value).length > 0 40 | }) 41 | 42 | const isUpdatePending = computed(() => { 43 | return isPending.value.update > 0 || Object.keys(updatePendingById.value).length > 0 44 | }) 45 | 46 | const isPatchPending = computed(() => { 47 | return isPending.value.patch > 0 || Object.keys(patchPendingById.value).length > 0 48 | }) 49 | 50 | const isRemovePending = computed(() => { 51 | return isPending.value.remove > 0 || Object.keys(removePendingById.value).length > 0 52 | }) 53 | 54 | function setPending(method: 'find' | 'count' | 'get' | 'create' | 'update' | 'patch' | 'remove', value: boolean) { 55 | if (value) 56 | isPending.value[method]++ 57 | else isPending.value[method]-- 58 | } 59 | 60 | function setPendingById(id: NullableId, method: RequestTypeById, val: boolean) { 61 | if (id == null) 62 | return 63 | 64 | let place 65 | 66 | if (method === 'create') 67 | place = createPendingById.value 68 | else if (method === 'update') 69 | place = updatePendingById.value 70 | else if (method === 'patch') 71 | place = patchPendingById.value 72 | else if (method === 'remove') 73 | place = removePendingById.value 74 | 75 | if (val) 76 | set(place, id, true) 77 | else del(place, id) 78 | } 79 | 80 | function unsetPendingById(...ids: NullableId[]) { 81 | ids.forEach((id) => { 82 | if (id == null) 83 | return 84 | del(createPendingById.value, id) 85 | del(updatePendingById.value, id) 86 | del(patchPendingById.value, id) 87 | del(removePendingById.value, id) 88 | }) 89 | } 90 | 91 | function clearAllPending() { 92 | isPending.value = defaultPending() 93 | 94 | createPendingById.value = {} 95 | updatePendingById.value = {} 96 | patchPendingById.value = {} 97 | removePendingById.value = {} 98 | } 99 | 100 | return { 101 | isPending, 102 | createPendingById, 103 | updatePendingById, 104 | patchPendingById, 105 | removePendingById, 106 | isFindPending, 107 | isCountPending, 108 | isGetPending, 109 | isCreatePending, 110 | isUpdatePending, 111 | isPatchPending, 112 | isRemovePending, 113 | setPending, 114 | setPendingById, 115 | unsetPendingById, 116 | clearAllPending, 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/stores/ssr-query-cache.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from 'vue' 2 | import type { AnyData } from '../types' 3 | 4 | export function useSsrQueryCache() { 5 | const resultsByQid = reactive>({}) 6 | 7 | function getQid(qid: string) { 8 | return resultsByQid[qid] 9 | } 10 | function setQid(qid: string, data: any) { 11 | resultsByQid[qid] = data 12 | } 13 | function clearQid(qid: string) { 14 | delete resultsByQid[qid] 15 | } 16 | function clearAllQids() { 17 | Object.keys(resultsByQid).forEach((qid) => { 18 | clearQid(qid) 19 | }) 20 | } 21 | 22 | return { resultsByQid, getQid, setQid, clearQid, clearAllQids } 23 | } 24 | -------------------------------------------------------------------------------- /src/stores/storage.ts: -------------------------------------------------------------------------------- 1 | import type { Id } from '@feathersjs/feathers' 2 | import { type Ref, computed, ref, del as vueDel, set as vueSet } from 'vue-demi' 3 | import type { AnyData, ById } from '../types.js' 4 | import type { AssignFn, beforeWriteFn, onReadFn } from './types.js' 5 | 6 | interface UseServiceStorageOptions { 7 | getId: (item: M) => string 8 | onRead?: onReadFn 9 | beforeWrite?: beforeWriteFn 10 | assign?: AssignFn 11 | } 12 | 13 | export type StorageMapUtils = ReturnType> 14 | 15 | /** 16 | * General storage adapter 17 | */ 18 | export function useServiceStorage({ 19 | getId, 20 | onRead = item => item, 21 | beforeWrite = item => item, 22 | assign = (dest, src) => Object.assign(dest, src), 23 | }: UseServiceStorageOptions) { 24 | const byId: Ref> = ref({}) 25 | 26 | const list = computed(() => { 27 | return Object.values(byId.value) 28 | }) 29 | 30 | const ids = computed(() => { 31 | return Object.keys(byId.value) 32 | }) 33 | 34 | /** 35 | * Checks if an item with the provided `id` is stored. 36 | * @param id 37 | * @returns 38 | */ 39 | const hasItem = (id: Id) => { 40 | return !!byId.value[id] 41 | } 42 | 43 | /** 44 | * Checks if the provided `item` is stored. 45 | * @param item 46 | * @returns boolean 47 | */ 48 | const has = (item: M) => { 49 | const id = getId(item) 50 | return hasItem(id as Id) 51 | } 52 | 53 | /** 54 | * Retrives the stored record that matches the provided `id`. 55 | * @param id 56 | * @returns 57 | */ 58 | const getItem = (id: Id) => { 59 | const inStore = byId.value[id] 60 | const _item = inStore ? onRead(inStore) : null 61 | return _item as M 62 | } 63 | 64 | const setItem = (id: Id, item: M) => { 65 | if (id == null) 66 | throw new Error('item has no id') 67 | vueSet(byId.value, id, beforeWrite(item)) 68 | return getItem(id) 69 | } 70 | 71 | /** 72 | * Writes the provided item to the store 73 | * @param item item to store 74 | * @returns 75 | */ 76 | const set = (item: M) => { 77 | const id = getId(item) as Id 78 | return setItem(id, item) 79 | } 80 | 81 | /** 82 | * If the item is stored, merges the `item` into the stored version. 83 | * If not yet stored, the item is stored. 84 | * @param item the item to merge or write to the store. 85 | * @returns the stored item 86 | */ 87 | const merge = (item: M) => { 88 | const id = getId(item) as Id 89 | const existing = getItem(id) 90 | if (existing) 91 | assign(existing, item) 92 | else setItem(id, item) 93 | 94 | return getItem(id) 95 | } 96 | 97 | /** 98 | * Retrieves the stored record that matches the provided `item`. 99 | * @param item 100 | * @returns 101 | */ 102 | const get = (item: M) => { 103 | const id = getId(item) as Id 104 | return getItem(id) 105 | } 106 | 107 | /** 108 | * Remove item with matching `id`, if found. 109 | * @param id 110 | * @returns boolean indicating if an item was removed 111 | */ 112 | const removeItem = (id: Id) => { 113 | const hadItem = hasItem(id) 114 | if (hadItem) 115 | vueDel(byId.value, id) 116 | 117 | return hadItem 118 | } 119 | 120 | /** 121 | * remove `item` if found 122 | * @param item 123 | * @returns boolean indicating if item was removed 124 | */ 125 | const remove = (item: M) => { 126 | const id = getId(item) as Id 127 | return removeItem(id) 128 | } 129 | 130 | const getKeys = () => { 131 | return ids.value 132 | } 133 | 134 | /** 135 | * empties the store 136 | */ 137 | const clear = () => { 138 | Object.keys(byId.value).forEach((id) => { 139 | vueDel(byId.value, id) 140 | }) 141 | } 142 | 143 | return { byId, list, ids, getId, clear, has, hasItem, get, getItem, set, setItem, remove, removeItem, getKeys, merge } 144 | } 145 | -------------------------------------------------------------------------------- /src/stores/temps.ts: -------------------------------------------------------------------------------- 1 | import type { AnyData } from '../types.js' 2 | import type { beforeWriteFn, onReadFn } from './types.js' 3 | import type { StorageMapUtils } from './storage.js' 4 | import { useServiceStorage } from './storage.js' 5 | 6 | interface UseServiceTempsOptions { 7 | getId: (item: M) => string 8 | itemStorage: StorageMapUtils 9 | onRead?: onReadFn 10 | beforeWrite?: beforeWriteFn 11 | } 12 | 13 | export function useServiceTemps(options: UseServiceTempsOptions) { 14 | const { getId, itemStorage, onRead, beforeWrite } = options 15 | 16 | const tempStorage = useServiceStorage({ 17 | getId, 18 | onRead, 19 | beforeWrite, 20 | }) 21 | 22 | function moveTempToItems(data: M) { 23 | if (tempStorage.has(data)) 24 | tempStorage.remove(data) 25 | 26 | return itemStorage.set(data) 27 | } 28 | 29 | return { tempStorage, moveTempToItems } 30 | } 31 | -------------------------------------------------------------------------------- /src/stores/use-data-store.ts: -------------------------------------------------------------------------------- 1 | import type { Query } from '@feathersjs/feathers' 2 | 3 | import { computed, unref } from 'vue-demi' 4 | import type { MaybeRef } from '@vueuse/core' 5 | import type { AnyData } from '../types.js' 6 | import { useModelInstance } from '../modeling/use-model-instance' 7 | import { useServiceLocal } from './local-queries.js' 8 | 9 | import { useAllStorageTypes } from './all-storage-types.js' 10 | 11 | export interface UseDataStoreOptions { 12 | idField: string 13 | ssr?: MaybeRef 14 | customSiftOperators?: Record 15 | setupInstance?: any 16 | } 17 | 18 | function makeDefaultOptions() { 19 | return { 20 | skipGetIfExists: false, 21 | } 22 | } 23 | 24 | export function useDataStore(_options: UseDataStoreOptions) { 25 | const options = Object.assign({}, makeDefaultOptions(), _options) 26 | const { idField, customSiftOperators } = options 27 | 28 | // storage 29 | const { itemStorage, tempStorage, cloneStorage, clone, commit, reset, addItemToStorage } = useAllStorageTypes({ 30 | getIdField: (val: AnyData) => val[idField], 31 | setupInstance, 32 | }) 33 | 34 | // local data filtering 35 | const { findInStore, findOneInStore, countInStore, getFromStore, createInStore, patchInStore, removeFromStore } 36 | = useServiceLocal({ 37 | idField, 38 | itemStorage, 39 | tempStorage, 40 | cloneStorage, 41 | addItemToStorage, 42 | customSiftOperators, 43 | }) 44 | 45 | function setupInstance(this: any, data: N) { 46 | const asBaseModel = useModelInstance(data, { 47 | idField, 48 | clonesById: cloneStorage.byId, 49 | clone, 50 | commit, 51 | reset, 52 | createInStore, 53 | removeFromStore, 54 | }) 55 | 56 | if (data.__isSetup) { 57 | return asBaseModel 58 | } 59 | else { 60 | const afterSetup = options.setupInstance ? options.setupInstance(asBaseModel) : asBaseModel 61 | Object.defineProperty(afterSetup, '__isSetup', { value: true }) 62 | return afterSetup 63 | } 64 | } 65 | 66 | const isSsr = computed(() => { 67 | const ssr = unref(options.ssr) 68 | return !!ssr 69 | }) 70 | 71 | function clearAll() { 72 | itemStorage.clear() 73 | tempStorage.clear() 74 | cloneStorage.clear() 75 | } 76 | 77 | const store = { 78 | new: setupInstance, 79 | idField, 80 | isSsr, 81 | 82 | // items 83 | itemsById: itemStorage.byId, 84 | items: itemStorage.list, 85 | itemIds: itemStorage.ids, 86 | 87 | // temps 88 | tempsById: tempStorage.byId, 89 | temps: tempStorage.list, 90 | tempIds: tempStorage.ids, 91 | 92 | // clones 93 | clonesById: cloneStorage.byId, 94 | clones: cloneStorage.list, 95 | cloneIds: cloneStorage.ids, 96 | clone, 97 | commit, 98 | reset, 99 | 100 | // local queries 101 | findInStore, 102 | findOneInStore, 103 | countInStore, 104 | createInStore, 105 | getFromStore, 106 | patchInStore, 107 | removeFromStore, 108 | clearAll, 109 | } 110 | 111 | return store 112 | } 113 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Params as FeathersParams, Id } from '@feathersjs/feathers' 2 | import type { ComputedRef } from 'vue-demi' 3 | import type { MaybeRef } from '@vueuse/core' 4 | import type { ServiceInstance } from './modeling/index.js' 5 | import type { PaginationStateQuery } from './stores/index.js' 6 | 7 | export type MaybeArray = T | T[] 8 | export type AnyData = Record 9 | export type AnyDataOrArray = MaybeArray 10 | 11 | export type MaybeRefOrComputed = MaybeRef | ComputedRef 12 | 13 | export interface Filters { 14 | $sort?: { [prop: string]: number } 15 | $limit?: MaybeRef 16 | $skip?: MaybeRef 17 | $select?: string[] 18 | } 19 | export interface Query extends Filters, AnyData { } 20 | 21 | export interface CustomFilter { 22 | key: string 23 | operator: (items: M[], queryValue: any, query: Record) => M[] 24 | } 25 | 26 | export interface Paginated { 27 | total: number 28 | limit: number 29 | skip: number 30 | data: T[] 31 | fromSsr?: true 32 | } 33 | 34 | export interface QueryInfo { 35 | qid: string 36 | query: Query 37 | queryId: string 38 | queryParams: Query 39 | pageParams: { $limit: MaybeRef, $skip: MaybeRef | undefined } | undefined 40 | pageId: string | undefined 41 | isExpired: boolean 42 | } 43 | 44 | export interface QueryInfoExtended extends QueryInfo { 45 | ids: Id[] 46 | items: AnyData | ServiceInstance[] 47 | total: number 48 | queriedAt: number 49 | queryState: PaginationStateQuery 50 | } 51 | export type ExtendedQueryInfo = QueryInfoExtended | null 52 | 53 | export type DiffDefinition = undefined | string | string[] | Record | false 54 | 55 | export interface PaginationOptions { 56 | default?: number | true 57 | max?: number 58 | } 59 | 60 | export interface Params extends FeathersParams { 61 | query?: Q 62 | paginate?: boolean | PaginationOptions 63 | provider?: string 64 | route?: Record 65 | headers?: Record 66 | temps?: boolean 67 | clones?: boolean 68 | qid?: string 69 | ssr?: boolean 70 | skipGetIfExists?: boolean 71 | data?: any 72 | preserveSsr?: boolean 73 | } 74 | export interface PatchParams extends Params { 75 | /** 76 | * For `create` and `patch`, only. Provide `params.data` to specify exactly which data should be passed to the API 77 | * server. This will disable the built-in diffing that normally happens before `patch` requests. 78 | */ 79 | data?: Partial 80 | /** 81 | * For `patch` with clones, only. When you call patch (or save) on a clone, the data will be diffed before sending 82 | * it to the API server. If no data has changed, the request will be resolve without making a request. The `diff` 83 | * param lets you control which data gets diffed: 84 | * 85 | * - `diff: string` will only include the prop matching the provided string. 86 | * - `diff: string[]` will only include the props matched in the provided array of strings 87 | * - `diff: object` will compare the provided `diff` object with the original. 88 | */ 89 | diff?: DiffDefinition 90 | /** 91 | * For `patch` with clones, only. When you call patch (or save) on a clone, after the data is diffed, any data matching the `with` 92 | * param will also be included in the request. 93 | * 94 | * - `with: string` will include the prop matchin the provided string. 95 | * - `with: string[]` will include the props that match any string in the provided array. 96 | * - `with: object` will include the exact object along with the request. 97 | */ 98 | with?: DiffDefinition 99 | /** 100 | * For `patch` with clones, only. Set `params.eager` to false to prevent eager updates during patch requests. This behavior is enabled on patch 101 | * requests, by default. 102 | */ 103 | eager?: boolean 104 | } 105 | 106 | // for cloning 107 | export interface MakeCopyOptions { 108 | isClone: boolean 109 | } 110 | 111 | export type ById = Record 112 | -------------------------------------------------------------------------------- /src/ufuzzy.ts: -------------------------------------------------------------------------------- 1 | export * from './custom-operators/fuzzy-search-with-ufuzzy.js' -------------------------------------------------------------------------------- /src/unplugin-auto-import-preset.ts: -------------------------------------------------------------------------------- 1 | export const feathersPiniaAutoImport = { 2 | 'feathers-pinia': [ 3 | 'useServiceInstance', 4 | 'useInstanceDefaults', 5 | 'useDataStore', 6 | 'useAuth', 7 | 'createPiniaClient', 8 | 'defineGetters', 9 | 'defineSetters', 10 | 'defineValues', 11 | 'useBackup', 12 | ], 13 | } 14 | -------------------------------------------------------------------------------- /src/use-auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-auth.js' 2 | -------------------------------------------------------------------------------- /src/use-find-get/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types' 2 | 3 | export { useFind } from './use-find' 4 | export { useGet } from './use-get' 5 | -------------------------------------------------------------------------------- /src/use-find-get/types.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue-demi' 2 | import type { AnyData, Params, Query } from '../types.js' 3 | import type { MostRecentQuery, PaginationStateQuery } from '../stores/types' 4 | 5 | export interface UseFindPage { 6 | limit: Ref 7 | skip: Ref 8 | } 9 | 10 | export interface UseFindGetDeps { 11 | service: any 12 | } 13 | 14 | export interface UseFindParams extends Params { 15 | query: Query 16 | qid?: string 17 | } 18 | 19 | export interface UseFindOptions { 20 | paginateOn?: 'client' | 'server' | 'hybrid' 21 | pagination?: UseFindPage 22 | debounce?: number 23 | immediate?: boolean 24 | watch?: boolean 25 | } 26 | 27 | export interface UseGetParams extends Params { 28 | query?: Query 29 | immediate?: boolean 30 | watch?: boolean 31 | } 32 | 33 | export interface CurrentQuery extends MostRecentQuery { 34 | qid: string 35 | ids: number[] 36 | items: M[] 37 | total: number 38 | queriedAt: number 39 | queryState: PaginationStateQuery 40 | } 41 | -------------------------------------------------------------------------------- /src/use-find-get/utils-pagination.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue-demi' 2 | import { computed } from 'vue-demi' 3 | import { timeout } from '../utils/index.js' 4 | 5 | interface Options { 6 | limit: Ref 7 | skip: Ref 8 | total: Ref 9 | request?: any 10 | } 11 | 12 | export function usePageData(options: Options) { 13 | const { limit, skip, total, request } = options 14 | /** 15 | * The number of pages available based on the results returned in the latestQuery prop. 16 | */ 17 | const pageCount = computed(() => { 18 | if (total.value) 19 | return Math.ceil(total.value / limit.value) 20 | else return 1 21 | }) 22 | 23 | // Uses Math.floor so we can't land on a non-integer page like 1.4 24 | const currentPage = computed({ 25 | set(pageNumber: number) { 26 | if (pageNumber < 1) 27 | pageNumber = 1 28 | else if (pageNumber > pageCount.value) 29 | pageNumber = pageCount.value 30 | const newSkip = limit.value * Math.floor(pageNumber - 1) 31 | skip.value = newSkip 32 | }, 33 | get() { 34 | const skipVal = skip.value || 0 35 | return pageCount.value === 0 ? 0 : Math.floor(skipVal / limit.value + 1) 36 | }, 37 | }) 38 | 39 | const canPrev = computed(() => { 40 | return currentPage.value - 1 > 0 41 | }) 42 | const canNext = computed(() => { 43 | return currentPage.value < pageCount.value 44 | }) 45 | 46 | const wait = async () => { 47 | if (request?.value) 48 | await request.value 49 | } 50 | const toStart = async () => { 51 | currentPage.value = 1 52 | await timeout(0) 53 | return wait() 54 | } 55 | const toEnd = async () => { 56 | currentPage.value = pageCount.value 57 | await timeout(0) 58 | return wait() 59 | } 60 | const toPage = async (page: number) => { 61 | currentPage.value = page 62 | await timeout(0) 63 | return wait() 64 | } 65 | const next = async () => { 66 | currentPage.value++ 67 | await timeout(0) 68 | return wait() 69 | } 70 | const prev = async () => { 71 | currentPage.value-- 72 | await timeout(0) 73 | return wait() 74 | } 75 | 76 | return { pageCount, currentPage, canPrev, canNext, toStart, toEnd, toPage, next, prev } 77 | } 78 | -------------------------------------------------------------------------------- /src/use-find-get/utils.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRef } from '@vueuse/core' 2 | import type { Ref } from 'vue-demi' 3 | import { _ } from '@feathersjs/commons' 4 | import { unref } from 'vue-demi' 5 | import type { Params, Query } from '../types.js' 6 | import type { UseFindParams } from './types.js' 7 | 8 | export function makeParamsWithoutPage(params: MaybeRef) { 9 | params = unref(params) 10 | const query = _.omit(params.query, '$limit', '$skip') 11 | const newParams = _.omit(params, 'query', 'store') 12 | return { ...newParams, query } 13 | } 14 | 15 | // Updates the _params with everything from _newParams except `$limit` and `$skip` 16 | export function updateParamsExcludePage(_params: Ref, _newParams: MaybeRef) { 17 | _params.value.query = { 18 | ...unref(_newParams).query, 19 | ..._.pick(unref(_params).query, '$limit', '$skip'), 20 | } 21 | } 22 | 23 | export function getIdsFromQueryInfo(pagination: any, queryInfo: any): any[] { 24 | const { queryId, pageId } = queryInfo 25 | const queryLevel = pagination[queryId] 26 | const pageLevel = queryLevel && queryLevel[pageId] 27 | const ids = pageLevel && pageLevel.ids 28 | 29 | return ids || [] 30 | } 31 | 32 | /** 33 | * A wrapper for findInStore that can return server-paginated data 34 | */ 35 | export function itemsFromPagination(store: any, service: any, params: Params) { 36 | const qid = params.qid || 'default' 37 | const pagination = store.pagination[qid] || {} 38 | const queryInfo = store.getQueryInfo(params) 39 | const ids = getIdsFromQueryInfo(pagination, queryInfo) 40 | const items = ids 41 | .map((id) => { 42 | const fromStore = service.getFromStore(id).value 43 | return fromStore 44 | }) 45 | .filter(i => i) // filter out undefined values 46 | return items 47 | } 48 | 49 | export function getAllIdsFromQueryInfo(pagination: any, queryInfo: any): any[] { 50 | const { queryId } = queryInfo 51 | const queryLevel = pagination[queryId] || {} 52 | 53 | const ids = [...new Set(Object.keys(queryLevel).filter(key => !['total', 'queryParams'].includes(key)).reduce((acc, qkey) => { 54 | acc = acc.concat(queryLevel?.[qkey]?.ids || []) 55 | return acc 56 | }, []))] 57 | 58 | return ids || [] 59 | } 60 | 61 | /** 62 | * A wrapper for findInStore that can return all server-paginated data 63 | */ 64 | export function allItemsFromPagination(store: any, service: any, params: Params) { 65 | const qid = params.qid || 'default' 66 | const pagination = store.pagination[qid] || {} 67 | const queryInfo = store.getQueryInfo(params) 68 | const ids = getAllIdsFromQueryInfo(pagination, queryInfo) 69 | const items = ids 70 | .map((id) => { 71 | const fromStore = service.getFromStore(id).value 72 | return fromStore 73 | }) 74 | .filter(i => i) // filter out undefined values 75 | return items 76 | } 77 | -------------------------------------------------------------------------------- /src/utils/convert-data.ts: -------------------------------------------------------------------------------- 1 | import type { FeathersService } from '@feathersjs/feathers' 2 | import type { PiniaService } from '../create-pinia-service.js' 3 | import type { AnyData, AnyDataOrArray } from '../types.js' 4 | import type { ServiceInstance } from '../modeling/index.js' 5 | 6 | export function convertData(service: PiniaService, result: AnyDataOrArray) { 7 | if (!result) { 8 | return result 9 | } 10 | else if (Array.isArray(result)) { 11 | return result.map(i => service.new(i)) as ServiceInstance[] 12 | } 13 | else if (result && Array.isArray(result.data)) { 14 | result.data = result.data.map(i => service.new(i)) as ServiceInstance[] 15 | return result 16 | } 17 | else { 18 | return service.new(result) as ServiceInstance 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/deep-unref.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRef } from '@vueuse/core' 2 | import { isRef, unref } from 'vue-demi' 3 | 4 | const isObject = (val: Record) => val !== null && typeof val === 'object' 5 | const isArray = Array.isArray 6 | 7 | // Unref a value, recursing into it if it's an object. 8 | function smartUnref(val: Record) { 9 | // Non-ref object? Go deeper! 10 | if (val !== null && !isRef(val) && typeof val === 'object') 11 | return deepUnref(val) 12 | 13 | return unref(val) 14 | } 15 | 16 | // Unref an array, recursively. 17 | const unrefArray = (arr: any) => arr.map(smartUnref) 18 | 19 | // Unref an object, recursively. 20 | function unrefObject(obj: Record) { 21 | const unreffed: Record = {} 22 | 23 | Object.keys(obj).forEach((key) => { 24 | unreffed[key] = smartUnref(obj[key]) 25 | }) 26 | 27 | return unreffed 28 | } 29 | 30 | /** 31 | * Deeply unref a value, recursing into objects and arrays. 32 | * 33 | * Adapted from https://github.com/DanHulton/vue-deepunref 34 | */ 35 | export function deepUnref(val: MaybeRef>) { 36 | const checkedVal: any = isRef(val) ? unref(val) : val 37 | 38 | if (!isObject(checkedVal)) 39 | return checkedVal 40 | 41 | if (isArray(checkedVal)) 42 | return unrefArray(checkedVal) 43 | 44 | return unrefObject(checkedVal) 45 | } 46 | -------------------------------------------------------------------------------- /src/utils/define-properties.ts: -------------------------------------------------------------------------------- 1 | import type { AnyData } from '../types.js' 2 | 3 | /** 4 | * Defines all provided properties as non-enumerable, configurable, values 5 | */ 6 | export function defineValues(data: M, properties: D) { 7 | Object.keys(properties).forEach((key) => { 8 | Object.defineProperty(data, key, { 9 | enumerable: false, 10 | configurable: true, 11 | value: properties[key], 12 | }) 13 | }) 14 | return data 15 | } 16 | 17 | /** 18 | * Defines all provided properties as non-enumerable, configurable, getters 19 | */ 20 | export function defineGetters(data: M, properties: D) { 21 | Object.keys(properties).forEach((key) => { 22 | Object.defineProperty(data, key, { 23 | enumerable: false, 24 | configurable: true, 25 | get: properties[key], 26 | }) 27 | }) 28 | return data 29 | } 30 | 31 | /** 32 | * Defines all provided properties as non-enumerable, configurable, setters 33 | */ 34 | export function defineSetters(data: M, properties: D) { 35 | Object.keys(properties).forEach((key) => { 36 | // eslint-disable-next-line accessor-pairs 37 | Object.defineProperty(data, key, { 38 | enumerable: false, 39 | configurable: true, 40 | set: properties[key], 41 | }) 42 | }) 43 | return data 44 | } 45 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils' 2 | export * from './use-counter' 3 | export * from './convert-data' 4 | export * from './define-properties' 5 | export * from './deep-unref' 6 | export * from './use-instance-defaults' 7 | export * from './service-utils' 8 | 9 | // typical Feathers service methods not on PiniaService 10 | export const existingServiceMethods = [ 11 | 'update', 12 | 'hooks', 13 | 'setMaxListeners', 14 | 'getMaxListeners', 15 | 'addListener', 16 | 'prependListener', 17 | 'once', 18 | 'prependOnceListener', 19 | 'removeListener', 20 | 'off', 21 | 'removeAllListeners', 22 | 'listeners', 23 | 'rawListeners', 24 | 'emit', 25 | 'eventNames', 26 | 'listenerCount', 27 | 'on', 28 | ] 29 | -------------------------------------------------------------------------------- /src/utils/service-utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Push data to the store and return the new data. 3 | */ 4 | export function pushToStore(data: Data, service: { createInStore: any }) { 5 | if (!data) 6 | return data 7 | 8 | const createInStore = (item: any) => service.createInStore(item) 9 | 10 | if (Array.isArray(data)) 11 | return data.map(createInStore) 12 | 13 | else 14 | return createInStore(data) 15 | } 16 | 17 | /** 18 | * Define a virtual property on an object. 19 | */ 20 | export function defineVirtualProperty(data: Data, key: string, getter: any) { 21 | const definition: any = { enumerable: false } 22 | if (typeof getter === 'function') { 23 | definition.get = function get(this: Data) { 24 | return getter(this as Data) 25 | } 26 | } 27 | else { definition.value = getter } 28 | 29 | Object.defineProperty(data, key, definition) 30 | } 31 | 32 | export function defineVirtualProperties(data: Data, getters: Record) { 33 | Object.keys(getters).forEach(key => defineVirtualProperty(data, key, getters[key])) 34 | } 35 | -------------------------------------------------------------------------------- /src/utils/use-counter.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue-demi' 2 | 3 | /** 4 | * Use a counter to track the number of pending queries. Prevents collisions with overlapping queries. 5 | */ 6 | export function useCounter() { 7 | const count = ref(0) 8 | const add = () => { 9 | count.value = count.value + 1 10 | } 11 | const sub = () => { 12 | count.value = count.value === 0 ? 0 : count.value - 1 13 | } 14 | return { count, add, sub } 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/use-instance-defaults.ts: -------------------------------------------------------------------------------- 1 | import fastCopy from 'fast-copy' 2 | import { _ } from '@feathersjs/commons' 3 | import type { AnyData } from '../types.js' 4 | 5 | export function useInstanceDefaults(defaults: D, data: M) { 6 | const dataKeys = Object.keys(data) 7 | const defaultsToApply = _.omit(defaults, ...dataKeys) as D 8 | const cloned = Object.assign(data, fastCopy(defaultsToApply)) 9 | 10 | return cloned 11 | } 12 | -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | darkMode: 'class', // or 'media' or 'class' 3 | content: ['docs/**/*.*'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | variants: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } 12 | -------------------------------------------------------------------------------- /tests/composables/use-backup.test.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue' 2 | import { api } from '../fixtures/index.js' 3 | import { resetService } from '../test-utils.js' 4 | import { useBackup } from '../../src/index.js' 5 | 6 | beforeEach(async () => { 7 | resetService(api.service('posts')) 8 | resetService(api.service('authors')) 9 | resetService(api.service('comments')) 10 | }) 11 | afterEach(() => { 12 | resetService(api.service('contacts')) 13 | }) 14 | 15 | describe('useBackup', () => { 16 | it('returns the original object in data.value', async () => { 17 | const props = { 18 | book: api.service('books').createInStore({ 19 | id: 1, 20 | title: 'Book 1', 21 | }), 22 | } 23 | 24 | const bookBackup = useBackup(computed(() => props.book)) 25 | const { data: book } = bookBackup 26 | 27 | expect(book.value).toStrictEqual(props.book) 28 | 29 | expect(typeof bookBackup.save).toBe('function') 30 | expect(typeof bookBackup.restore).toBe('function') 31 | }) 32 | 33 | it('can restore original values', async () => { 34 | const props = { 35 | book: api.service('books').createInStore({ 36 | id: 1, 37 | title: 'Book 1', 38 | }), 39 | } 40 | 41 | const bookBackup = useBackup(computed(() => props.book)) 42 | const { data: book } = bookBackup 43 | 44 | expect(book.value.title).toBe('Book 1') 45 | 46 | book.value.title = 'New Title' 47 | 48 | expect(book.value.title).toBe('New Title') 49 | 50 | bookBackup.restore(book) 51 | 52 | expect(book.value.title).toBe('Book 1') 53 | }) 54 | 55 | it('can save', async () => { 56 | const storedBook = await api.service('books').create({ title: 'Book 1' }) 57 | const props = { book: storedBook } 58 | 59 | const bookBackup = useBackup(computed(() => props.book)) 60 | const { data: book } = bookBackup 61 | 62 | book.value.title = 'Book 1 Revised Edition' 63 | 64 | await bookBackup.save() 65 | 66 | expect(book.value.title).toBe('Book 1 Revised Edition') 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /tests/fixtures/data.ts: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker' 2 | 3 | export function makeContactsData() { 4 | return { 5 | 1: { _id: '1', name: 'Moose', age: 4, birthdate: '2019-03-1T07:00:00.000Z' }, 6 | 2: { _id: '2', name: 'moose', age: 5, birthdate: '2018-03-1T07:00:00.000Z' }, 7 | 3: { _id: '3', name: 'Goose', age: 6, birthdate: '2017-03-1T07:00:00.000Z' }, 8 | 4: { _id: '4', name: 'Loose', age: 5, birthdate: '2018-03-1T07:00:00.000Z' }, 9 | 5: { _id: '5', name: 'Marshall', age: 21, birthdate: '2002-03-1T07:00:00.000Z' }, 10 | 6: { _id: '6', name: 'David', age: 23, birthdate: '2000-03-1T07:00:00.000Z' }, 11 | 7: { _id: '7', name: 'Beau', age: 24, birthdate: '1999-03-1T07:00:00.000Z' }, 12 | 8: { _id: '8', name: 'Batman', age: 25, birthdate: '1998-03-1T07:00:00.000Z' }, 13 | 9: { _id: '9', name: 'Flash', age: 44, birthdate: '1979-03-1T07:00:00.000Z' }, 14 | 10: { _id: '10', name: 'Wolverine', age: 55, birthdate: '1968-03-1T07:00:00.000Z' }, 15 | 11: { _id: '11', name: 'Rogue', age: 66, birthdate: '1957-03-1T07:00:00.000Z' }, 16 | 12: { _id: '12', name: 'Jubilee', age: 77, birthdate: '1946-03-1T07:00:00.000Z' }, 17 | } 18 | } 19 | 20 | export function makeContactsDataRandom(count = 100) { 21 | const placeholders = Array.from(Array(count).keys()) 22 | const contacts = placeholders.map((item: number) => { 23 | const _id = item.toString() 24 | const name = faker.person.fullName() 25 | const birthdate = new Date(faker.date.birthdate()) 26 | const age = calculateAge(birthdate) 27 | 28 | return { _id, name, age, birthdate: birthdate.getTime() } 29 | }) 30 | const contactsById = contacts.reduce((acc, contact) => { 31 | acc[contact._id] = contact 32 | return acc 33 | }, {}) 34 | return contactsById 35 | } 36 | 37 | function calculateAge(birthday: Date) { 38 | const ageDifMs = Date.now() - birthday.getTime() 39 | const ageDate = new Date(ageDifMs) 40 | return Math.abs(ageDate.getUTCFullYear() - 1970) 41 | } 42 | -------------------------------------------------------------------------------- /tests/fixtures/index.ts: -------------------------------------------------------------------------------- 1 | export * from './feathers' 2 | export * from './schemas' 3 | export * from './data' 4 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/authors.ts: -------------------------------------------------------------------------------- 1 | import { type Static, Type, querySyntax } from '@feathersjs/typebox' 2 | 3 | // Main data model schema 4 | export const authorsSchema = Type.Object( 5 | { 6 | id: Type.Number(), 7 | name: Type.String({ description: 'The author\'s name' }), 8 | /** 9 | * We define relationship data here only if we want to be able to create new instances with related data. This 10 | * scenario is the same as pre-populating data on the API server and sending it to the client. 11 | */ 12 | posts: Type.Optional(Type.Any()), 13 | comments: Type.Optional(Type.Any()), 14 | }, 15 | { $id: 'Authors', additionalProperties: false }, 16 | ) 17 | export type Authors = Static 18 | 19 | // Schema for creating new entries 20 | export const authorsDataSchema = Type.Pick(authorsSchema, ['name', 'posts', 'comments'], { 21 | $id: 'AuthorsData', 22 | additionalProperties: false, 23 | }) 24 | export type AuthorsData = Static 25 | 26 | // Schema for allowed query properties 27 | export const authorsQueryProperties = Type.Omit(authorsSchema, [], { 28 | additionalProperties: false, 29 | }) 30 | export const authorsQuerySchema = querySyntax(authorsQueryProperties) 31 | export type AuthorsQuery = Static 32 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/comments.ts: -------------------------------------------------------------------------------- 1 | import { type Static, Type, querySyntax } from '@feathersjs/typebox' 2 | 3 | // Main data model schema 4 | export const commentsSchema = Type.Object( 5 | { 6 | id: Type.Number(), 7 | text: Type.String(), 8 | authorId: Type.Number(), 9 | postId: Type.Number(), 10 | // populated 11 | author: Type.Optional(Type.Any()), 12 | }, 13 | { $id: 'Comments', additionalProperties: false }, 14 | ) 15 | export type Comments = Static 16 | 17 | // Schema for creating new entries 18 | export const commentsDataSchema = Type.Pick(commentsSchema, ['text', 'authorId', 'postId'], { 19 | $id: 'CommentsData', 20 | additionalProperties: false, 21 | }) 22 | export type CommentsData = Static 23 | 24 | // Schema for allowed query properties 25 | export const commentsQueryProperties = Type.Omit(commentsSchema, [], { 26 | additionalProperties: false, 27 | }) 28 | export const commentsQuerySchema = querySyntax(commentsQueryProperties) 29 | export type CommentsQuery = Static 30 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/contacts.ts: -------------------------------------------------------------------------------- 1 | import { Type, querySyntax } from '@feathersjs/typebox' 2 | import type { Static } from '@feathersjs/typebox' 3 | 4 | // Full Schema 5 | export const contactsSchema = Type.Object( 6 | { 7 | _id: Type.String(), 8 | name: Type.String(), 9 | age: Type.Optional(Type.Number()), 10 | }, 11 | { $id: 'Contacts', additionalProperties: false }, 12 | ) 13 | export type Contacts = Static 14 | 15 | // Create 16 | export const contactsDataSchema = Type.Pick(contactsSchema, ['name', 'age'], { 17 | $id: 'ContactsData', 18 | }) 19 | export type ContactsData = Static 20 | 21 | // Patch 22 | export const contactsPatchSchema = Type.Partial(contactsDataSchema, { 23 | $id: 'ContactsPatch', 24 | }) 25 | export type ContactsPatch = Static 26 | 27 | // Query 28 | export const contactsQueryProperties = Type.Pick(contactsSchema, ['_id', 'name', 'age']) 29 | export const contactsQuerySchema = Type.Intersect( 30 | [ 31 | querySyntax(contactsQueryProperties), 32 | // Add additional query properties here 33 | Type.Object({}, { additionalProperties: false }), 34 | ], 35 | { additionalProperties: false }, 36 | ) 37 | export type ContactsQuery = Static 38 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/index.ts: -------------------------------------------------------------------------------- 1 | export * from './authors' 2 | export * from './comments' 3 | export * from './contacts' 4 | export * from './posts' 5 | export * from './tasks' 6 | export * from './users' 7 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/posts.ts: -------------------------------------------------------------------------------- 1 | import { type Static, Type, querySyntax } from '@feathersjs/typebox' 2 | import { commentsSchema } from './comments.js' 3 | 4 | // Main data model schema 5 | export const postsSchema = Type.Object( 6 | { 7 | id: Type.Number(), 8 | title: Type.String(), 9 | authorIds: Type.Array(Type.Number()), 10 | // Associated data 11 | authors: Type.Optional(Type.Array(Type.Record(Type.String(), Type.Any()))), 12 | comments: Type.Optional(Type.Array(commentsSchema)), 13 | }, 14 | { $id: 'Posts', additionalProperties: false }, 15 | ) 16 | export type Posts = Static 17 | 18 | // Schema for creating new entries 19 | export const postsDataSchema = Type.Pick(postsSchema, ['title', 'authorIds'], { 20 | $id: 'PostsData', 21 | additionalProperties: false, 22 | }) 23 | export type PostsData = Static 24 | 25 | // Schema for allowed query properties 26 | export const postsQueryProperties = Type.Omit(postsSchema, [], { 27 | additionalProperties: false, 28 | }) 29 | export const postsQuerySchema = querySyntax(postsQueryProperties) 30 | export type PostsQuery = Static 31 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/tasks.ts: -------------------------------------------------------------------------------- 1 | import { Type, querySyntax } from '@feathersjs/typebox' 2 | import type { Static } from '@feathersjs/typebox' 3 | 4 | // Full Schema 5 | export const tasksSchema = Type.Object( 6 | { 7 | _id: Type.String(), 8 | description: Type.String(), 9 | isComplete: Type.Boolean(), 10 | }, 11 | { $id: 'Tasks', additionalProperties: false }, 12 | ) 13 | export type Tasks = Static 14 | 15 | // Create 16 | export const tasksDataSchema = Type.Pick(tasksSchema, ['description', 'isComplete'], { 17 | $id: 'TasksData', 18 | }) 19 | export type TasksData = Static 20 | 21 | // Patch 22 | export const tasksPatchSchema = Type.Partial(tasksDataSchema, { 23 | $id: 'TasksPatch', 24 | }) 25 | export type TasksPatch = Static 26 | 27 | // Query 28 | export const tasksQueryProperties = Type.Pick(tasksSchema, ['_id', 'description', 'isComplete']) 29 | export const tasksQuerySchema = Type.Intersect( 30 | [ 31 | querySyntax(tasksQueryProperties), 32 | // extend properties here 33 | Type.Object({}, { additionalProperties: false }), 34 | ], 35 | { additionalProperties: false }, 36 | ) 37 | export type TasksQuery = Static 38 | -------------------------------------------------------------------------------- /tests/fixtures/schemas/users.ts: -------------------------------------------------------------------------------- 1 | import { Type, querySyntax } from '@feathersjs/typebox' 2 | import type { Static } from '@feathersjs/typebox' 3 | 4 | // Full Schema 5 | export const usersSchema = Type.Object( 6 | { 7 | _id: Type.String(), 8 | email: Type.String(), 9 | password: Type.Optional(Type.String()), 10 | }, 11 | { $id: 'Users', additionalProperties: false }, 12 | ) 13 | export type Users = Static 14 | 15 | // Create 16 | export const usersDataSchema = Type.Pick(usersSchema, ['email', 'password'], { 17 | $id: 'UsersData', 18 | }) 19 | export type UsersData = Static 20 | 21 | // Patch 22 | export const usersPatchSchema = Type.Partial(usersDataSchema, { 23 | $id: 'UsersPatch', 24 | }) 25 | export type UsersPatch = Static 26 | 27 | // Query 28 | export const usersQueryProperties = Type.Pick(usersSchema, ['_id', 'email']) 29 | export const usersQuerySchema = Type.Intersect( 30 | [ 31 | querySyntax(usersQueryProperties), 32 | // Add additional query properties here 33 | Type.Object({}, { additionalProperties: false }), 34 | ], 35 | { additionalProperties: false }, 36 | ) 37 | export type UsersQuery = Static 38 | -------------------------------------------------------------------------------- /tests/instance-api/instance-clones.test.ts: -------------------------------------------------------------------------------- 1 | import { api, makeContactsData } from '../fixtures/index.js' 2 | import { resetService } from '../test-utils.js' 3 | 4 | const service = api.service('contacts') 5 | 6 | beforeEach(async () => { 7 | resetService(service) 8 | service.service.store = makeContactsData() 9 | }) 10 | afterEach(() => resetService(service)) 11 | 12 | describe('useModelInstance clones', () => { 13 | test('clone an item', async () => { 14 | const contact = service.new({ _id: '1', name: 'Evan' }) 15 | const cloned = contact.clone() 16 | expect(cloned._id).toBe('1') 17 | expect(cloned.__isClone).toBe(true) 18 | expect(cloned.name).toBe('Evan') 19 | }) 20 | 21 | test('clone a temp keeps the tempId', async () => { 22 | const contact = service.new({ name: 'Evan' }) 23 | expect(contact.__tempId).toBeDefined() 24 | expect(typeof contact.clone).toBe('function') 25 | const cloned = contact.clone() 26 | expect(cloned.__tempId).toBe(contact.__tempId) 27 | expect(cloned.__isClone).toBe(true) 28 | expect(cloned.name).toBe('Evan') 29 | }) 30 | 31 | test('clone a non-stored temp adds it to temps with __isClone set to false', () => { 32 | const contact = service.new({ name: 'Evan' }) 33 | contact.clone() 34 | const storedTemp = service.store.tempsById[contact.__tempId as string] 35 | expect(storedTemp).toBe(contact) 36 | expect(storedTemp.__isClone).toBe(false) 37 | }) 38 | 39 | test('clone values are independent, do not leak into original item', async () => { 40 | const contact = service.new({ name: 'Evan' }) 41 | 42 | const cloned = contact.clone() 43 | cloned.name = 'George' 44 | expect(contact.name).toBe('Evan') 45 | }) 46 | 47 | test('modified clone properties commit to the original item', async () => { 48 | const contact = service.new({ name: 'Evan' }) 49 | 50 | const cloned = contact.clone() 51 | cloned.name = 'George' 52 | 53 | const committed = cloned.commit() 54 | expect(committed.name).toEqual('George') 55 | }) 56 | 57 | test('committing a temp keeps the tempId', async () => { 58 | const contact = service.new({ name: 'Evan' }) 59 | const cloned = contact.clone() 60 | const committed = cloned.commit() 61 | expect(committed.__isClone).toBe(false) 62 | expect(committed).toEqual(contact) 63 | }) 64 | 65 | test('can re-clone after commit', async () => { 66 | const contact = service.new({ name: 'Evan' }) 67 | const cloned = contact.clone() 68 | const committed = cloned.commit() 69 | const recloned = committed.clone() 70 | expect(cloned).toBe(recloned) 71 | }) 72 | 73 | test('calling reset on an original item clones the item', async () => { 74 | const contact = service.new({ name: 'Evan' }) 75 | const resetted = contact.reset() 76 | 77 | const storedClone = service.store.clonesById[contact.__tempId] 78 | expect(storedClone).toBe(resetted) 79 | }) 80 | 81 | test('calling reset on a clone resets the clone', async () => { 82 | const contact = service.new({ name: 'Evan' }) 83 | const clone = contact.clone() 84 | clone.name = 'George' 85 | 86 | const resetted = clone.reset() 87 | expect(clone).toBe(resetted) 88 | expect(resetted.name).toBe('Evan') 89 | }) 90 | 91 | test('saving a clone', async () => { 92 | const contact = service.new({ name: 'test' }) 93 | const clone = contact.clone() 94 | const result = await clone.save() 95 | expect(result).toBe(clone) 96 | expect(result).not.toBe(contact) 97 | 98 | const original = service.getFromStore(result._id).value 99 | expect(result).not.toBe(original) 100 | }) 101 | }) 102 | -------------------------------------------------------------------------------- /tests/instance-api/instance-defaults.test.ts: -------------------------------------------------------------------------------- 1 | import { api, makeContactsData } from '../fixtures/index.js' 2 | import { resetService } from '../test-utils.js' 3 | 4 | const service = api.service('contacts') 5 | 6 | beforeEach(async () => { 7 | resetService(service) 8 | service.service.store = makeContactsData() 9 | await service.find({ query: { $limit: 100 } }) 10 | }) 11 | afterEach(() => resetService(service)) 12 | 13 | describe('useInstanceDefaults', () => { 14 | test('has defaults', async () => { 15 | const contact = service.new({}) 16 | expect(contact.name).toBe('') 17 | expect(contact.age).toBe(0) 18 | }) 19 | 20 | test('overwrite defaults with data', async () => { 21 | const contact = service.new({ name: 'foo', age: 55 }) 22 | expect(contact.name).toBe('foo') 23 | expect(contact.age).toBe(55) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /tests/instance-api/instance-temps.test.ts: -------------------------------------------------------------------------------- 1 | import { api, makeContactsData } from '../fixtures/index.js' 2 | import { resetService } from '../test-utils.js' 3 | 4 | const service = api.service('contacts') 5 | 6 | beforeEach(async () => { 7 | resetService(service) 8 | service.service.store = makeContactsData() 9 | await service.find({ query: { $limit: 100 } }) 10 | }) 11 | afterEach(() => resetService(service)) 12 | 13 | describe('Temporary Records', () => { 14 | test('store can hold temps', () => { 15 | expect(service.store).toHaveProperty('tempsById') 16 | expect(service.store).toHaveProperty('temps') 17 | expect(service.store).toHaveProperty('tempIds') 18 | }) 19 | 20 | test('records without idField get tempIdField added', () => { 21 | const item = service.new({ name: 'this is a test' }) 22 | expect(typeof item.__tempId).toBe('string') 23 | expect(item.__isTemp).toBeTruthy() 24 | }) 25 | 26 | test('records with idField do not get tempIdField added', () => { 27 | const item = service.new({ _id: '2', name: 'this is a test' }) 28 | expect(item.__tempId).toBeUndefined() 29 | expect(item.__isTemp).toBeFalsy() 30 | }) 31 | 32 | test('temps can be retrieved with getFromStore', () => { 33 | const item = service.new({ name: 'this is a test' }).createInStore() 34 | const tempFromStore = service.getFromStore(item.__tempId).value 35 | expect(tempFromStore?.__tempId).toBe(item.__tempId) 36 | expect(tempFromStore?.__isTemp).toBeTruthy() 37 | }) 38 | 39 | test('temps are added to tempsById', () => { 40 | const item = service.new({ name: 'this is a test' }).createInStore() 41 | expect(service.store.tempsById).toHaveProperty(item.__tempId) 42 | }) 43 | 44 | test('saving a temp does not remove __tempId, standalone temp not updated', async () => { 45 | const temp = service.new({ name: 'this is a test' }) 46 | expect(temp._id).toBeUndefined() 47 | expect(temp.__tempId).toBeDefined() 48 | 49 | const item = await temp.save() 50 | expect(temp._id).toBeDefined() 51 | expect(temp.__tempId).toBeDefined() 52 | expect(item._id).toBeDefined() 53 | expect(item.__tempId).toBeDefined() 54 | }) 55 | 56 | test('saving a temp does not remove __tempId, temp added to store is updated', async () => { 57 | const temp = service.new({ name: 'this is a test' }).createInStore() 58 | const item = await temp.save() 59 | expect(temp._id).toBeDefined() 60 | expect(temp.__tempId).toBeDefined() 61 | expect(item._id).toBeDefined() 62 | expect(item.__tempId).toBeDefined() 63 | }) 64 | 65 | test('saving a temp removes it from tempsById', async () => { 66 | const item = service.new({ name: 'this is a test' }) 67 | await item.save() 68 | expect(item.__tempId).toBeDefined() 69 | expect(service.store.tempsById).not.toHaveProperty(item.__tempId) 70 | }) 71 | 72 | test('find getter does not returns temps when params.temps is falsy', async () => { 73 | service.new({ name: 'this is a test' }).createInStore() 74 | const { data } = service.findInStore({ query: {} }) 75 | expect(data.length).toBe(12) 76 | }) 77 | 78 | test('find getter returns temps when temps param is true', async () => { 79 | service.new({ name: 'this is a test' }).createInStore() 80 | const contact$ = service.findInStore({ query: {}, temps: true } as any) 81 | expect(contact$.data.length).toBe(13) 82 | }) 83 | 84 | test('temps can be removed from the store', async () => { 85 | const item = service.new({ name: 'this is a test' }).createInStore() 86 | item.removeFromStore() 87 | expect(item.__tempId).toBeDefined() 88 | expect(service.store.tempsById).not.toHaveProperty(item.__tempId) 89 | }) 90 | }) 91 | -------------------------------------------------------------------------------- /tests/localstorage/clear-storage.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-invalid-this */ 2 | import { vi } from 'vitest' 3 | import { clearStorage, syncWithStorage } from '../../src' 4 | import { api } from '../fixtures/index.js' 5 | import { resetService, timeout } from '../test-utils.js' 6 | 7 | const service = api.service('contacts') 8 | 9 | const localStorageMock: Storage = { 10 | 'getItem': vi.fn(), 11 | 'setItem': vi.fn(function () { 12 | this.length++ 13 | }), 14 | 'removeItem': vi.fn(function () { 15 | this.length-- 16 | }), 17 | 'clear': vi.fn(), 18 | 'length': 0, 19 | 'key': vi.fn(() => { 20 | return 'service:contacts' 21 | }), 22 | 23 | // Dummy key to make sure removeItem is called 24 | 'service.items': '{"hey": "there"}', 25 | } 26 | syncWithStorage(service.store, ['tempsById'], localStorageMock) 27 | 28 | const reset = () => resetService(service) 29 | 30 | describe('Clear Storage', () => { 31 | beforeEach(() => { 32 | reset() 33 | }) 34 | 35 | test('clear storage', async () => { 36 | service.createInStore({ name: 'test' }) 37 | await timeout(600) 38 | 39 | expect(localStorageMock.setItem).toHaveBeenCalled() 40 | const [key] = (localStorageMock.setItem as any).mock.calls[0] 41 | expect(key).toBe('service:contacts') 42 | 43 | clearStorage(localStorageMock) 44 | expect(localStorageMock.removeItem).toHaveBeenCalled() 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /tests/localstorage/storage-sync.test.ts: -------------------------------------------------------------------------------- 1 | import { api, localStorageMock } from '../fixtures/index.js' 2 | import { resetService, timeout } from '../test-utils.js' 3 | 4 | const service = api.service('contacts') 5 | 6 | const reset = () => resetService(service) 7 | 8 | describe('Storage Sync', () => { 9 | beforeEach(() => { 10 | reset() 11 | }) 12 | 13 | test('writes to storage', async () => { 14 | const msg = service.createInStore({ name: 'test' }) 15 | await timeout(600) 16 | expect(localStorageMock.setItem).toHaveBeenCalled() 17 | const [key, value] = (localStorageMock.setItem as any).mock.calls[0] 18 | expect(key).toBe('service:contacts') 19 | const val = JSON.parse(value) 20 | expect(val.tempsById[msg.__tempId]).toBeTruthy() 21 | }) 22 | 23 | test('reads from storage', async () => { 24 | service.createInStore({ name: 'test2' }) 25 | await timeout(1000) 26 | expect(localStorageMock.getItem).toHaveBeenCalled() 27 | const [key, value] = (localStorageMock.getItem as any).mock.calls[0] 28 | expect(key).toBe('service:contacts') 29 | expect(value).toBeUndefined() 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /tests/modeling/push-to-store.test.ts: -------------------------------------------------------------------------------- 1 | import { api } from '../fixtures/index.js' 2 | import { resetService } from '../test-utils.js' 3 | 4 | beforeEach(async () => { 5 | resetService(api.service('posts')) 6 | resetService(api.service('authors')) 7 | resetService(api.service('comments')) 8 | }) 9 | afterEach(() => { 10 | resetService(api.service('contacts')) 11 | }) 12 | 13 | describe('pushToStore', () => { 14 | it('distributes data to the correct service stores', async () => { 15 | api.service('books').createInStore({ 16 | id: 1, 17 | title: 'Book 1', 18 | pages: [ 19 | { id: 1, title: 'Page 1', bookId: 1 }, 20 | { id: 2, title: 'Page 2', bookId: 1 }, 21 | ], 22 | }) 23 | 24 | const page1 = api.service('pages').getFromStore(1) 25 | const page2 = api.service('pages').getFromStore(2) 26 | 27 | expect(page1.value.title).toBe('Page 1') 28 | expect(page2.value.title).toBe('Page 2') 29 | }) 30 | 31 | it('replaces values with non-enumerable values', async () => { 32 | const book = api.service('books').createInStore({ 33 | id: 2, 34 | title: 'Book 2', 35 | pages: [ 36 | { id: 3, title: 'Page 3', bookId: 2 }, 37 | { id: 4, title: 'Page 4', bookId: 2 }, 38 | ], 39 | }) 40 | 41 | expect(book.pages.length).toBe(2) 42 | 43 | const serialized = JSON.parse(JSON.stringify(book)) 44 | 45 | expect(serialized.pages).toBeUndefined() 46 | }) 47 | 48 | it('related data is reactive', async () => { 49 | const book = api.service('books').createInStore({ 50 | id: 3, 51 | title: 'Book 2', 52 | pages: [ 53 | { id: 5, title: 'Page 5', bookId: 3 }, 54 | { id: 6, title: 'Page 6', bookId: 3 }, 55 | ], 56 | }) 57 | 58 | expect(book.pages.length).toBe(2) 59 | 60 | const page = api.service('pages').createInStore({ 61 | id: 7, 62 | title: 'Page 7', 63 | bookId: 3, 64 | }) 65 | 66 | expect(book.pages.length).toBe(3) 67 | expect(book.pages[2].title).toBe(page.title) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /tests/modeling/store-associated.test.ts: -------------------------------------------------------------------------------- 1 | import { api } from '../fixtures/index.js' 2 | import { resetService } from '../test-utils.js' 3 | 4 | beforeEach(async () => { 5 | resetService(api.service('posts')) 6 | resetService(api.service('authors')) 7 | resetService(api.service('comments')) 8 | }) 9 | afterEach(() => { 10 | resetService(api.service('contacts')) 11 | }) 12 | 13 | describe('storeAssociated', () => { 14 | it('distributes data to the correct service stores', async () => { 15 | api.service('posts').new({ 16 | title: 'foo', 17 | authors: [{ id: 1, name: 'Marshall' }], 18 | author: { id: 2, name: 'Myriah' }, 19 | comments: [ 20 | { id: 1, text: 'comment 1', authorId: 1, postId: 1 }, 21 | { id: 2, text: 'comment 2', authorId: 1, postId: 1 }, 22 | ], 23 | }) 24 | 25 | const author1 = api.service('authors').getFromStore(1) 26 | const author2 = api.service('authors').getFromStore(2) 27 | const comments = api.service('comments').findInStore({ query: {} }) 28 | 29 | expect(author1.value.name).toBe('Marshall') 30 | expect(author2.value.name).toBe('Myriah') 31 | expect(comments.data.length).toBe(2) 32 | }) 33 | 34 | it('replaces values with non-enumerable values', async () => { 35 | const post = api.service('posts').new({ 36 | title: 'foo', 37 | authors: [{ id: 1, name: 'Marshall' }], 38 | author: { id: 2, name: 'Myriah' }, 39 | comments: [ 40 | { id: 1, text: 'comment 1', authorId: 1, postId: 1 }, 41 | { id: 2, text: 'comment 2', authorId: 1, postId: 1 }, 42 | ], 43 | }) 44 | 45 | expect(post.authors.length).toBe(1) 46 | expect(post.author.name).toBe('Myriah') 47 | expect(post.comments.length).toBe(2) 48 | 49 | const serialized = JSON.parse(JSON.stringify(post)) 50 | 51 | expect(serialized.authors).toBeUndefined() 52 | expect(serialized.author).toBeUndefined() 53 | expect(serialized.comments).toBeUndefined() 54 | }) 55 | 56 | it('related data is reactive', async () => { 57 | const post = api.service('posts').new({ 58 | title: 'foo', 59 | authors: [{ id: 1, name: 'Marshall' }], 60 | author: { id: 2, name: 'Myriah' }, 61 | comments: [ 62 | { id: 1, text: 'comment 1', authorId: 1, postId: 1 }, 63 | { id: 2, text: 'comment 2', authorId: 1, postId: 1 }, 64 | ], 65 | }) 66 | 67 | const author1 = api.service('authors').getFromStore(2) 68 | const result = api.service('authors').patchInStore(author1.value.id, { name: 'Austin' }) 69 | 70 | expect(result.name).toBe('Austin') 71 | expect(post.author.name).toBe('Austin') 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /tests/modeling/use-feathers-instance.test.ts: -------------------------------------------------------------------------------- 1 | import { api } from '../fixtures/index.js' 2 | import { resetService, timeout, timeoutHook } from '../test-utils.js' 3 | 4 | const service = api.service('contacts') 5 | 6 | beforeEach(() => { 7 | resetService(service) 8 | }) 9 | 10 | describe('PiniaService', () => { 11 | test('instances have pending state', async () => { 12 | const contact = service.new({}) 13 | expect(contact.isSavePending).toBeDefined() 14 | expect(contact.isCreatePending).toBeDefined() 15 | expect(contact.isPatchPending).toBeDefined() 16 | expect(contact.isRemovePending).toBeDefined() 17 | }) 18 | 19 | test('isSavePending with isCreatePending state properly updates', async () => { 20 | service.hooks({ before: { all: [timeoutHook(20)] } }) 21 | const contact = service.new({}) 22 | 23 | const request = contact.save() 24 | await timeout(0) 25 | expect(contact.isSavePending).toBeTruthy() 26 | expect(contact.isCreatePending).toBeTruthy() 27 | 28 | await request 29 | expect(contact.isSavePending).toBeFalsy() 30 | expect(contact.isCreatePending).toBeFalsy() 31 | }) 32 | 33 | test('isSavePending with isPatchPending state properly updates', async () => { 34 | service.hooks({ before: { all: [timeoutHook(20)] } }) 35 | const contact = await service.new({}).save() 36 | contact.name = 'foo' 37 | 38 | const request = contact.save() 39 | await timeout(0) 40 | expect(contact.isSavePending).toBeTruthy() 41 | expect(contact.isPatchPending).toBeTruthy() 42 | 43 | await request 44 | expect(contact.isSavePending).toBeFalsy() 45 | expect(contact.isPatchPending).toBeFalsy() 46 | }) 47 | 48 | test('isRemovePending properly updates', async () => { 49 | service.hooks({ before: { all: [timeoutHook(20)] } }) 50 | const contact = await service.new({}).save() 51 | 52 | const request = contact.remove() 53 | await timeout(0) 54 | expect(contact.isRemovePending).toBeTruthy() 55 | 56 | await request 57 | expect(contact.isRemovePending).toBeFalsy() 58 | }) 59 | 60 | test('instances have methods', async () => { 61 | const contact = service.new({}) 62 | expect(typeof contact.save).toBe('function') 63 | expect(typeof contact.create).toBe('function') 64 | expect(typeof contact.patch).toBe('function') 65 | expect(typeof contact.remove).toBe('function') 66 | }) 67 | 68 | test('instance.create', async () => { 69 | const contact = service.new({ _id: '1' }) 70 | const result = await contact.create() 71 | expect(result._id).toBe('1') 72 | 73 | expect(service.store.items.length).toBe(1) 74 | }) 75 | 76 | test('instance.patch', async () => { 77 | const contact = service.new({ _id: '1' }) 78 | expect(contact.name).toEqual('') 79 | expect(contact.age).toEqual(0) 80 | 81 | await contact.create() 82 | 83 | contact.name = 'do the dishes' 84 | 85 | const result = await contact.patch() 86 | expect(contact.name).toBe('do the dishes') 87 | expect(result.name).toBe('do the dishes') 88 | }) 89 | 90 | test('instance.remove', async () => { 91 | const contact = service.new({ name: 'test' }) 92 | const saved = await contact.save() 93 | 94 | expect(saved._id).toBe(0) 95 | expect(saved.name).toBe('test') 96 | 97 | const stored = await api.service('contacts').get(0) 98 | 99 | expect(stored.name).toBe('test') 100 | 101 | await contact.remove() 102 | 103 | await expect(api.service('contacts').get(0)).rejects.toThrow('No record found for id \'0\'') 104 | }) 105 | }) 106 | -------------------------------------------------------------------------------- /tests/stores/events.test.ts: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest' 2 | import { api, makeContactsData } from '../fixtures/index.js' 3 | import { resetService, timeout } from '../test-utils.js' 4 | 5 | const service = api.service('contacts') 6 | 7 | beforeEach(async () => { 8 | resetService(service) 9 | service.service.store = makeContactsData() 10 | await service.find({ query: { $limit: 100 } }) 11 | }) 12 | afterEach(() => resetService(service)) 13 | 14 | describe('useDataStore events', () => { 15 | it('handles created events', async () => { 16 | api.service('contacts').emit('created', { _id: 'foo', name: 'Steve', age: 99 }) 17 | await timeout(50) 18 | const item = api.service('contacts').getFromStore('foo') 19 | expect(item.value.name).toBe('Steve') 20 | }) 21 | 22 | it('handles updated events', async () => { 23 | api.service('contacts').emit('updated', { _id: 'foo', name: 'Steve', age: 99 }) 24 | await timeout(50) 25 | const item = api.service('contacts').getFromStore('foo') 26 | expect(item.value.name).toBe('Steve') 27 | }) 28 | 29 | it('handles patched events', async () => { 30 | api.service('contacts').emit('patched', { _id: 'foo', name: 'Steve', age: 99 }) 31 | await timeout(50) 32 | const item = api.service('contacts').getFromStore('foo') 33 | expect(item.value.name).toBe('Steve') 34 | }) 35 | 36 | it('handles removed events', async () => { 37 | const data = { _id: 'foo', name: 'Steve', age: 99 } 38 | api.service('contacts').store.createInStore(data) 39 | const added = api.service('contacts').getFromStore('foo') 40 | expect(added.value.name).toBe('Steve') 41 | 42 | api.service('contacts').emit('removed', data) 43 | await timeout(50) 44 | const item = api.service('contacts').getFromStore('foo') 45 | expect(item.value).toBeNull() 46 | }) 47 | 48 | it('only handles events once', async () => { 49 | const data = { _id: 'foo', name: 'Steve', age: 99 } 50 | const eventHandler = vi.fn() 51 | api.service('contacts').on('created', eventHandler) 52 | api.service('contacts').emit('created', data) 53 | 54 | await timeout(50) 55 | expect(eventHandler).toHaveBeenCalledTimes(1) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /tests/stores/use-data-clones.test.ts: -------------------------------------------------------------------------------- 1 | import { useServiceClones, useServiceStorage } from '../../src' 2 | 3 | const itemStorage = useServiceStorage({ getId: item => item.id }) 4 | const tempStorage = useServiceStorage({ getId: item => item.tempId }) 5 | const { cloneStorage, clone, commit, reset } = useServiceClones({ itemStorage, tempStorage }) 6 | 7 | describe('use-service-clones', () => { 8 | beforeEach(() => { 9 | itemStorage.clear() 10 | tempStorage.clear() 11 | cloneStorage.clear() 12 | }) 13 | 14 | test('can clone', () => { 15 | const item = { id: 1, name: '1' } 16 | itemStorage.setItem(1, item) 17 | const cloned = clone(item) 18 | expect(cloned).toBe(cloneStorage.getItem(1)) 19 | }) 20 | 21 | test('can clone with data', () => { 22 | const item = { id: 1, name: '1' } 23 | itemStorage.setItem(1, item) 24 | const cloned = clone(item, { test: true }) 25 | expect(cloned.test).toBe(true) 26 | }) 27 | 28 | test('can useExisting clone', () => { 29 | const item = { id: 1, name: '1' } 30 | itemStorage.setItem(1, item) 31 | clone(item, { test: true }) 32 | 33 | itemStorage.setItem(1, { id: 1, name: 'test' }) 34 | 35 | const cloned2 = clone(item, undefined, { useExisting: true }) 36 | expect(cloned2.name).toBe('1') 37 | }) 38 | 39 | test('can commit', () => { 40 | const item = { id: 1, name: '1' } 41 | itemStorage.setItem(1, item) 42 | const cloned = clone(item) 43 | cloned.name = 'one' 44 | const committed = commit(cloned) 45 | expect(committed).toBe(itemStorage.getItem(1)) 46 | }) 47 | 48 | test('can reset', () => { 49 | const item = { id: 1, name: '1' } 50 | itemStorage.setItem(1, item) 51 | const cloned = clone(item) 52 | cloned.name = 'one' 53 | reset(cloned) 54 | expect(cloned.name).toBe('1') 55 | }) 56 | 57 | test('clone when missing original: `item` is stored in cloneStorage', () => { 58 | const item = { id: 1, name: '1' } 59 | const cloned = clone(item) 60 | expect(cloned.name).toBe('1') 61 | expect(itemStorage.getItem(1)).toEqual(item) 62 | }) 63 | 64 | test('commit when missing clone: `item` stored in itemStorage', () => { 65 | const item = { id: 1, name: '1' } 66 | const cloned = clone(item) 67 | expect(cloned.name).toBe('1') 68 | }) 69 | 70 | test('reset when missing original: `item` stored in cloneStorage', () => { 71 | const item = { id: 1, name: '1' } 72 | const cloned = reset(item) 73 | expect(cloned.name).toBe('1') 74 | expect(itemStorage.getItem(1)).toEqual(item) 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /tests/stores/use-data-local-custom-filters.test.ts: -------------------------------------------------------------------------------- 1 | import { api, makeContactsData } from '../fixtures/index.js' 2 | import { resetService } from '../test-utils.js' 3 | 4 | const service = api.service('contacts') 5 | 6 | describe('Custom Filters for findInStore', () => { 7 | beforeEach(async () => { 8 | resetService(service) 9 | service.service.store = makeContactsData() 10 | await service.find({ query: { $limit: 100 } }) 11 | }) 12 | afterEach(() => resetService(service)) 13 | 14 | test('can filter objects with $fuzzy', async () => { 15 | const { data } = service.findInStore({ 16 | query: { 17 | $fuzzy: { 18 | search: 'gose', 19 | fields: ['name'], 20 | } 21 | } 22 | }) 23 | expect(data[0].name).toEqual('Goose') 24 | }) 25 | 26 | test('$fuzzy can filter multiple fields', async () => { 27 | const { data } = service.findInStore({ 28 | query: { 29 | $fuzzy: { 30 | search: '25', 31 | fields: ['name', 'age'], 32 | } 33 | } 34 | }) 35 | expect(data[0].name).toEqual('Batman') 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /tests/stores/use-data-local.test.ts: -------------------------------------------------------------------------------- 1 | import { useAllStorageTypes, useServiceLocal } from '../../src' 2 | 3 | const idField = 'id' 4 | const { itemStorage, tempStorage, addItemToStorage } = useAllStorageTypes({ 5 | getIdField: (val: any) => val[idField], 6 | setupInstance: data => data, 7 | }) 8 | const { findInStore, countInStore, getFromStore } = useServiceLocal({ 9 | idField: 'id', 10 | itemStorage, 11 | tempStorage, 12 | whitelist: [], 13 | paramsForServer: [], 14 | addItemToStorage, 15 | }) 16 | 17 | describe('use-service-local', () => { 18 | beforeEach(() => { 19 | const items = [ 20 | { id: 1, name: 'Goose' }, 21 | { id: 2, name: 'Moose' }, 22 | { id: 3, name: 'Loose' }, 23 | { id: 4, name: 'Juice' }, 24 | { id: 5, name: 'King Bob' }, 25 | ] 26 | items.forEach(i => itemStorage.set(i)) 27 | }) 28 | test('findInStore', () => { 29 | const results = findInStore({ query: {} }) 30 | expect(results.data.length).toBe(5) 31 | }) 32 | 33 | test('findInStore with $and', () => { 34 | const results = findInStore({ 35 | query: { 36 | $and: [{ id: 1 }, { name: 'Goose' }], 37 | }, 38 | }) 39 | expect(results.data.length).toBe(1) 40 | }) 41 | 42 | test('findInStore with $or', () => { 43 | const results = findInStore({ 44 | query: { 45 | $or: [{ id: 1 }, { name: 'Moose' }], 46 | }, 47 | }) 48 | expect(results.data.length).toBe(2) 49 | }) 50 | 51 | test('findInStore with exact filter', () => { 52 | const results = findInStore({ query: { name: 'Juice' } }) 53 | expect(results.data.length).toBe(1) 54 | }) 55 | 56 | test('findInStore with regex filter', () => { 57 | const results = findInStore({ query: { name: { $regex: /oose/ } } }) 58 | expect(results.data.length).toBe(5) 59 | }) 60 | 61 | test('findInStore with params.clones does nothing when items are not instances (no service)', () => { 62 | const results = findInStore({ query: {}, clones: true }) 63 | results.data.forEach((item) => { 64 | expect(item.__isClone).not.toBeDefined() 65 | }) 66 | expect(results.data.length).toBe(5) 67 | }) 68 | 69 | test('countInStore', () => { 70 | const result = countInStore({ query: {} }).value 71 | expect(result).toBe(5) 72 | }) 73 | 74 | test('countInStore with exact filter', () => { 75 | const result = countInStore({ query: { name: 'Juice' } }).value 76 | expect(result).toBe(1) 77 | }) 78 | 79 | test('countInStore with regex filter', () => { 80 | const result = countInStore({ query: { name: { $regex: /oose/ } } }).value 81 | expect(result).toBe(5) 82 | }) 83 | 84 | test('getFromStore', () => { 85 | const item = getFromStore(1).value 86 | expect(item?.id).toBe(1) 87 | }) 88 | 89 | test('getFromStore with params.clones does nothing without service', () => { 90 | const item = getFromStore(1, { clones: true }).value 91 | expect(item?.id).toBe(1) 92 | expect(item?.__isClone).not.toBeDefined() 93 | }) 94 | 95 | test('getFromStore invalid id', () => { 96 | const item = getFromStore('one').value 97 | expect(item).toBe(null) 98 | }) 99 | }) 100 | -------------------------------------------------------------------------------- /tests/stores/use-data-storage.test.ts: -------------------------------------------------------------------------------- 1 | import { useServiceStorage } from '../../src' 2 | 3 | const storage = useServiceStorage({ 4 | getId: item => item.id, 5 | }) 6 | 7 | describe('use-service-storage', () => { 8 | beforeEach(() => { 9 | storage.clear() 10 | }) 11 | 12 | test('list and ids', () => { 13 | const items = [ 14 | { id: 1, name: 'One' }, 15 | { id: 2, name: 'Two' }, 16 | { id: 3, name: 'Three' }, 17 | ] 18 | const ids = items.map(i => i.id.toString()) 19 | items.map(item => storage.set(item)) 20 | expect(storage.list.value).toEqual(items) 21 | expect(storage.ids.value).toEqual(ids) 22 | }) 23 | 24 | test('setItem', () => { 25 | const item = { id: 1, name: 'One' } 26 | const stored = storage.setItem(1, item) 27 | expect(stored).toEqual(item) 28 | expect(storage.byId.value[1]).toEqual(item) 29 | }) 30 | 31 | test('set', () => { 32 | const item = { id: 1, name: 'One' } 33 | const stored = storage.set(item) 34 | expect(stored).toEqual(item) 35 | expect(storage.byId.value[1]).toEqual(item) 36 | }) 37 | 38 | test('hasItem', () => { 39 | const item = { id: 1, name: 'One' } 40 | expect(storage.hasItem(1)).toBeFalsy() 41 | storage.set(item) 42 | expect(storage.hasItem(1)).toBeTruthy() 43 | }) 44 | 45 | test('has', () => { 46 | const item = { id: 1, name: 'One' } 47 | expect(storage.has(item)).toBeFalsy() 48 | storage.set(item) 49 | expect(storage.has(item)).toBeTruthy() 50 | }) 51 | 52 | test('merge', () => { 53 | const item = { id: 1, name: 'One' } 54 | storage.merge(item) 55 | expect(storage.has(item)).toBeTruthy() 56 | 57 | storage.merge({ id: 1, test: true }) 58 | expect(storage.getItem(1)).toEqual({ id: 1, name: 'One', test: true }) 59 | }) 60 | 61 | test('getItem', () => { 62 | const item = { id: 1, name: 'One' } 63 | storage.set(item) 64 | expect(storage.getItem(1)).toEqual(item) 65 | }) 66 | 67 | test('get', () => { 68 | const item = { id: 1, name: 'One' } 69 | storage.set(item) 70 | expect(storage.get({ id: 1 })).toEqual(item) 71 | }) 72 | 73 | test('removeItem', () => { 74 | const item = { id: 1, name: 'One' } 75 | storage.set(item) 76 | expect(storage.list.value.length).toBe(1) 77 | storage.removeItem(1) 78 | expect(storage.list.value.length).toBe(0) 79 | }) 80 | 81 | test('remove', () => { 82 | const item = { id: 1, name: 'One' } 83 | storage.set(item) 84 | expect(storage.list.value.length).toBe(1) 85 | storage.remove(item) 86 | expect(storage.list.value.length).toBe(0) 87 | }) 88 | 89 | test('getKeys', () => { 90 | const item = { id: 1, name: 'One' } 91 | storage.set(item) 92 | expect(storage.getKeys()).toEqual(['1']) 93 | }) 94 | 95 | test('clear', () => { 96 | const items = [ 97 | { id: 1, name: 'One' }, 98 | { id: 2, name: 'Two' }, 99 | { id: 3, name: 'Three' }, 100 | ] 101 | items.map(item => storage.set(item)) 102 | expect(storage.list.value.length).toEqual(3) 103 | storage.clear() 104 | expect(storage.list.value.length).toEqual(0) 105 | }) 106 | }) 107 | -------------------------------------------------------------------------------- /tests/stores/use-data-store-getters-whitelist.test.ts: -------------------------------------------------------------------------------- 1 | import { api } from '../fixtures/index.js' 2 | 3 | describe('whitelist', () => { 4 | test('adds whitelist to the state', async () => { 5 | expect(api.service('contacts').store.whitelist).toContain('$test') 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /tests/stores/use-data-store-storage.test.ts: -------------------------------------------------------------------------------- 1 | import { api, makeContactsData } from '../fixtures/index.js' 2 | import { resetService } from '../test-utils.js' 3 | 4 | const service = api.service('contacts') 5 | 6 | beforeEach(async () => { 7 | resetService(service) 8 | service.service.store = makeContactsData() 9 | await service.find({ query: { $limit: 100 } }) 10 | }) 11 | afterEach(() => resetService(service)) 12 | 13 | describe('useModelInstance temps', () => { 14 | beforeEach(() => { 15 | service.store.clearAll() 16 | }) 17 | 18 | test('assigns tempid when no id provided', async () => { 19 | const task = service.new({ name: 'test' }) 20 | expect(task.__tempId).toBeDefined() 21 | }) 22 | 23 | test('has no __tempId id is present', async () => { 24 | const task = service.new({ _id: '1', name: 'foo', age: 44 }) 25 | expect(task.__tempId).toBeUndefined() 26 | }) 27 | 28 | test('not added to Model store by default', () => { 29 | service.new({ description: 'foo', isComplete: true } as any) 30 | expect(service.store.items.length).toBe(0) 31 | expect(service.store.temps.length).toBe(0) 32 | expect(service.store.clones.length).toBe(0) 33 | }) 34 | 35 | test('call createInStore without id to add to tempStore', () => { 36 | const task = service.new({ description: 'foo', isComplete: true } as any).createInStore() 37 | expect(service.store.temps.length).toBe(1) 38 | expect(service.store.temps[0]).toBe(task) 39 | }) 40 | 41 | test('call createInStore with id to add to itemStore', () => { 42 | const task = service.new({ _id: '1', description: 'foo', isComplete: true } as any).createInStore() 43 | expect(service.store.items.length).toBe(1) 44 | expect(service.store.items[0]).toBe(task) 45 | }) 46 | 47 | test('call removeFromStore on temp', () => { 48 | const task = service.new({ description: 'foo', isComplete: true } as any).createInStore() 49 | task.removeFromStore() 50 | expect(service.store.temps.length).toBe(0) 51 | }) 52 | 53 | test('call removeFromStore on item', () => { 54 | const task = service.new({ _id: '1', description: 'foo', isComplete: true } as any).createInStore() 55 | task.removeFromStore() 56 | expect(service.store.items.length).toBe(0) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /tests/stores/use-data-store.test.ts: -------------------------------------------------------------------------------- 1 | import { useDataStore } from '../../src' 2 | 3 | describe('useDataStore', () => { 4 | test('setup', () => { 5 | const service = useDataStore({ 6 | idField: 'id', 7 | }) 8 | 9 | expect(Object.keys(service)).toEqual([ 10 | 'new', 11 | 'idField', 12 | 'isSsr', 13 | 14 | // items 15 | 'itemsById', 16 | 'items', 17 | 'itemIds', 18 | 19 | // temps 20 | 'tempsById', 21 | 'temps', 22 | 'tempIds', 23 | 24 | // clones 25 | 'clonesById', 26 | 'clones', 27 | 'cloneIds', 28 | 'clone', 29 | 'commit', 30 | 'reset', 31 | 32 | // local queries 33 | 'findInStore', 34 | 'findOneInStore', 35 | 'countInStore', 36 | 'createInStore', 37 | 'getFromStore', 38 | 'patchInStore', 39 | 'removeFromStore', 40 | 'clearAll', 41 | ]) 42 | expect(service).toBeTruthy() 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /tests/stores/use-service-store.test.ts: -------------------------------------------------------------------------------- 1 | import { useServiceStore } from '../../src' 2 | 3 | describe('useServiceStore', () => { 4 | test('setup', () => { 5 | const service = useServiceStore({ 6 | idField: 'id', 7 | servicePath: 'posts', 8 | }) 9 | 10 | expect(Object.keys(service)).toEqual([ 11 | 'new', 12 | 'idField', 13 | 'servicePath', 14 | 'isSsr', 15 | 'defaultLimit', 16 | 17 | // items 18 | 'itemsById', 19 | 'items', 20 | 'itemIds', 21 | 22 | // temps 23 | 'tempsById', 24 | 'temps', 25 | 'tempIds', 26 | 27 | // clones 28 | 'clonesById', 29 | 'clones', 30 | 'cloneIds', 31 | 'clone', 32 | 'commit', 33 | 'reset', 34 | 35 | // local queries 36 | 'findInStore', 37 | 'findOneInStore', 38 | 'countInStore', 39 | 'createInStore', 40 | 'getFromStore', 41 | 'patchInStore', 42 | 'removeFromStore', 43 | 'clearAll', 44 | 45 | // ssr query cache 46 | 'resultsByQid', 47 | 'getQid', 48 | 'setQid', 49 | 'clearQid', 50 | 'clearAllQids', 51 | 52 | // server options 53 | 'whitelist', 54 | 'paramsForServer', 55 | 56 | // server pagination 57 | 'pagination', 58 | 'updatePaginationForQuery', 59 | 'unflagSsr', 60 | 'getQueryInfo', 61 | 62 | // pending state 63 | 'isPending', 64 | 'createPendingById', 65 | 'updatePendingById', 66 | 'patchPendingById', 67 | 'removePendingById', 68 | 'isFindPending', 69 | 'isCountPending', 70 | 'isGetPending', 71 | 'isCreatePending', 72 | 'isUpdatePending', 73 | 'isPatchPending', 74 | 'isRemovePending', 75 | 'setPending', 76 | 'setPendingById', 77 | 'unsetPendingById', 78 | 'clearAllPending', 79 | 80 | 'eventLocks', 81 | 'toggleEventLock', 82 | 'clearEventLock', 83 | ]) 84 | expect(service).toBeTruthy() 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /tests/test-utils.ts: -------------------------------------------------------------------------------- 1 | import { timeout } from '../src/utils' 2 | 3 | export function resetService(service: any) { 4 | // reset the wrapped service's memory store 5 | service.service.store = {} 6 | service.service._uId = 0 7 | // clear the pinia store 8 | service.store.clearAll() 9 | } 10 | 11 | export { timeout } 12 | 13 | export function timeoutHook(ms: number) { 14 | return async () => { 15 | await timeout(ms) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/use-auth/use-auth.test.ts: -------------------------------------------------------------------------------- 1 | import { createPinia, defineStore } from 'pinia' 2 | import { useAuth } from '../../src' 3 | import { api } from '../fixtures/index.js' 4 | 5 | describe('useAuth return values', () => { 6 | const utils = useAuth({ api }) 7 | 8 | test('can authenticate', async () => { 9 | const response = await utils.authenticate({ strategy: 'jwt', accessToken: 'hi' }) 10 | expect(response).toHaveProperty('accessToken') 11 | expect(response).toHaveProperty('payload') 12 | }) 13 | }) 14 | 15 | describe('useAuth in Pinia store', () => { 16 | const pinia = createPinia() 17 | const useAuthStore = defineStore('auth', () => { 18 | const utils = useAuth({ api }) 19 | return { ...utils } 20 | }) 21 | const authStore = useAuthStore(pinia) 22 | 23 | test('has all useAuth values', async () => { 24 | expect(authStore.$id).toBe('auth') 25 | expect(typeof authStore.authenticate).toBe('function') 26 | expect(typeof authStore.clearError).toBe('function') 27 | expect(typeof authStore.getPromise).toBe('function') 28 | expect(typeof authStore.isTokenExpired).toBe('function') 29 | expect(typeof authStore.logout).toBe('function') 30 | expect(typeof authStore.reAuthenticate).toBe('function') 31 | expect(authStore.error).toBeDefined() 32 | expect(authStore.isAuthenticated).toBeDefined() 33 | expect(authStore.isInitDone).toBeDefined() 34 | expect(authStore.isLogoutPending).toBeDefined() 35 | expect(authStore.isPending).toBeDefined() 36 | expect(authStore.loginRedirect).toBeDefined() 37 | expect(authStore.user).toBeNull() 38 | }) 39 | 40 | test('authenticate', async () => { 41 | await authStore.authenticate({ strategy: 'jwt', accessToken: 'hi' }) 42 | expect(authStore.isAuthenticated).toBe(true) 43 | expect(authStore.user).toBeNull() 44 | }) 45 | 46 | test('custom types', async () => { 47 | interface AuthenticateData { 48 | strategy: 'jwt' | 'local' | 'ldap' 49 | accessToken?: string 50 | tuid?: string 51 | password?: string 52 | } 53 | 54 | interface User { 55 | id: number; 56 | email: string; 57 | } 58 | 59 | defineStore('auth', () => { 60 | const utils = useAuth({ api }) 61 | utils.reAuthenticate() 62 | return { ...utils } 63 | }) 64 | }) 65 | 66 | test('reAuthenticate', async () => { 67 | const useAuthStore = defineStore('auth', () => { 68 | const utils = useAuth({ api }) 69 | utils.reAuthenticate() 70 | return { ...utils } 71 | }) 72 | 73 | const authStore = useAuthStore() 74 | const expectedResponse = { 75 | accessToken: 'jwt-access-token', 76 | payload: { 77 | test: true, 78 | }, 79 | user: { 80 | email: 'test@test.com', 81 | id: 1, 82 | }, 83 | } 84 | const request = await authStore.reAuthenticate() 85 | expect(request).toEqual(expectedResponse) 86 | }) 87 | }) 88 | 89 | describe('useAuth in Pinia store with userStore', () => { 90 | const pinia = createPinia() 91 | const useAuthStore = defineStore('auth', () => { 92 | const utils = useAuth({ api, servicePath: 'users' }) 93 | return { ...utils } 94 | }) 95 | const authStore = useAuthStore(pinia) 96 | 97 | test('authenticate populates user', async () => { 98 | await authStore.authenticate({ strategy: 'jwt', accessToken: 'hi' }) 99 | expect(authStore.isAuthenticated).toBe(true) 100 | expect(authStore.user.email).toBeDefined() 101 | }) 102 | }) 103 | -------------------------------------------------------------------------------- /tests/use-find-get/use-find.test.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue-demi' 2 | import { toRefs } from '@vueuse/core' 3 | import { api, makeContactsData } from '../fixtures/index.js' 4 | import { resetService } from '../test-utils.js' 5 | 6 | const service = api.service('contacts') 7 | 8 | beforeEach(async () => { 9 | resetService(service) 10 | service.service.store = makeContactsData() 11 | }) 12 | afterEach(() => resetService(service)) 13 | 14 | describe('useFind', () => { 15 | test('correct default immediate values', async () => { 16 | const p = computed(() => { 17 | return { query: { name: 'Moose' } } 18 | }) 19 | const { 20 | allLocalData, 21 | data, 22 | error, 23 | haveBeenRequested, 24 | haveLoaded, 25 | isPending, 26 | isSsr, 27 | cachedQuery, 28 | currentQuery, 29 | latestQuery, 30 | previousQuery, 31 | qid, 32 | request, 33 | requestCount, 34 | limit, 35 | skip, 36 | total, 37 | // utils 38 | clearError, 39 | find, 40 | queryWhen, 41 | // pagination 42 | canNext, 43 | canPrev, 44 | currentPage, 45 | pageCount, 46 | next, 47 | prev, 48 | toEnd, 49 | toPage, 50 | toStart, 51 | } = toRefs(service.useFind(p)) 52 | expect(allLocalData.value).toEqual([]) 53 | expect(data.value).toEqual([]) 54 | expect(error.value).toBeNull() 55 | expect(haveBeenRequested.value).toBe(false) 56 | expect(haveLoaded.value).toBe(false) 57 | expect(isPending.value).toBe(false) 58 | expect(isSsr.value).toBe(false) 59 | expect(currentQuery.value).toBeNull() 60 | expect(cachedQuery.value).toBeNull() 61 | expect(latestQuery.value).toBeNull() 62 | expect(previousQuery.value).toBeNull() 63 | expect(qid.value).toBe('default') 64 | expect(request.value).toBeNull() 65 | expect(requestCount.value).toBe(0) 66 | expect(limit.value).toBe(20) 67 | expect(skip.value).toBe(0) 68 | expect(total.value).toBe(0) 69 | // utils 70 | expect(typeof clearError.value).toBe('function') 71 | expect(typeof find.value).toBe('function') 72 | expect(typeof queryWhen.value).toBe('function') 73 | // pagination 74 | expect(canNext.value).toEqual(false) 75 | expect(canPrev.value).toEqual(false) 76 | expect(currentPage.value).toBe(1) 77 | expect(pageCount.value).toBe(1) 78 | expect(typeof next.value).toBe('function') 79 | expect(typeof prev.value).toBe('function') 80 | expect(typeof toEnd.value).toBe('function') 81 | expect(typeof toPage.value).toBe('function') 82 | expect(typeof toStart.value).toBe('function') 83 | }) 84 | 85 | test('applies default limit to query', async () => { 86 | const result = await service.find() 87 | // defaultLimit is 20 but there are only 12 records. 88 | expect(result.data.length).toBe(12) 89 | }) 90 | }) 91 | -------------------------------------------------------------------------------- /tests/vue-service/base-methods-untyped.test.ts: -------------------------------------------------------------------------------- 1 | import { apiUntyped } from '../fixtures/index.js' 2 | import { resetService } from '../test-utils.js' 3 | 4 | // get a service from the untyped Feathers client. 5 | const service = apiUntyped.service('users') 6 | 7 | beforeEach(async () => { 8 | resetService(service) 9 | }) 10 | afterEach(() => resetService(service)) 11 | 12 | describe('The general types work for non-typed Feathers clients', () => { 13 | test('has all methods', async () => { 14 | // create data instances 15 | expect(service.new).toBeTruthy() 16 | 17 | // api methods 18 | expect(service.find).toBeTruthy() 19 | expect(service.findOne).toBeTruthy() 20 | expect(service.count).toBeTruthy() 21 | expect(service.get).toBeTruthy() 22 | expect(service.create).toBeTruthy() 23 | expect(service.patch).toBeTruthy() 24 | expect(service.remove).toBeTruthy() 25 | 26 | // store methods 27 | expect(service.findInStore).toBeTruthy() 28 | expect(service.findOneInStore).toBeTruthy() 29 | expect(service.countInStore).toBeTruthy() 30 | expect(service.getFromStore).toBeTruthy() 31 | expect(service.createInStore).toBeTruthy() 32 | expect(service.patchInStore).toBeTruthy() 33 | expect(service.removeFromStore).toBeTruthy() 34 | 35 | // hybrid methods 36 | expect(service.useFind).toBeTruthy() 37 | expect(service.useGet).toBeTruthy() 38 | expect(service.useGetOnce).toBeTruthy() 39 | 40 | // event methods 41 | expect(service.on).toBeTruthy() 42 | expect(service.emit).toBeTruthy() 43 | expect(service.removeListener).toBeTruthy() 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /tests/vue-service/base-methods.test.ts: -------------------------------------------------------------------------------- 1 | import { api } from '../fixtures/index.js' 2 | import { resetService } from '../test-utils.js' 3 | 4 | const service = api.service('users') 5 | 6 | beforeEach(async () => { 7 | resetService(service) 8 | }) 9 | afterEach(() => resetService(service)) 10 | describe('The general types work for typed Feathers clients', () => { 11 | test('has custom methods', async () => { 12 | // create data instances 13 | expect(service.new).toBeTruthy() 14 | 15 | // api methods 16 | expect(service.find).toBeTruthy() 17 | expect(service.findOne).toBeTruthy() 18 | expect(service.count).toBeTruthy() 19 | expect(service.get).toBeTruthy() 20 | expect(service.create).toBeTruthy() 21 | expect(service.patch).toBeTruthy() 22 | expect(service.remove).toBeTruthy() 23 | 24 | // store methods 25 | expect(service.findInStore).toBeTruthy() 26 | expect(service.findOneInStore).toBeTruthy() 27 | expect(service.countInStore).toBeTruthy() 28 | expect(service.getFromStore).toBeTruthy() 29 | expect(service.createInStore).toBeTruthy() 30 | expect(service.patchInStore).toBeTruthy() 31 | expect(service.removeFromStore).toBeTruthy() 32 | 33 | // hybrid methods 34 | expect(service.useFind).toBeTruthy() 35 | expect(service.useGet).toBeTruthy() 36 | expect(service.useGetOnce).toBeTruthy() 37 | 38 | // event methods 39 | expect(service.on).toBeTruthy() 40 | expect(service.emit).toBeTruthy() 41 | expect(service.removeListener).toBeTruthy() 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /tests/vue-service/custom-methods.test.ts: -------------------------------------------------------------------------------- 1 | import { api } from '../fixtures/index.js' 2 | import { resetService } from '../test-utils.js' 3 | 4 | const service = api.service('users') 5 | 6 | beforeEach(async () => { 7 | resetService(service) 8 | }) 9 | afterEach(() => resetService(service)) 10 | describe('Custom Service Methods', () => { 11 | test('has custom methods', async () => { 12 | expect(typeof service.customCreate).toBe('function') 13 | const result = await service.customCreate({ 14 | email: 'test@test.com', 15 | password: 'test', 16 | }) 17 | expect(result.custom).toBeTruthy() 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /tests/vue-service/custom-store.test.ts: -------------------------------------------------------------------------------- 1 | import { api, makeContactsData } from '../fixtures/index.js' 2 | import { resetService } from '../test-utils.js' 3 | 4 | const service = api.service('contacts') 5 | 6 | beforeEach(async () => { 7 | resetService(service) 8 | service.service.store = makeContactsData() 9 | await service.find({ query: { $limit: 100 } }) 10 | }) 11 | afterEach(() => resetService(service)) 12 | 13 | describe('Customizing the Store', () => { 14 | it('has global state', async () => { 15 | expect(api.service('users').store.globalCustom).toBe(true) 16 | }) 17 | 18 | it('can override global state', async () => { 19 | expect(api.service('contacts').store.globalCustom).toBe(false) 20 | }) 21 | 22 | it('can have service state', async () => { 23 | expect(service.store.serviceCustom).toBe(false) 24 | }) 25 | 26 | it('can have getters', async () => { 27 | expect(service.store.serviceCustomOpposite).toBe(true) 28 | }) 29 | 30 | it('can read from default store state', async () => { 31 | expect(service.store.itemsLength).toBe(12) 32 | }) 33 | 34 | it('can have actions', async () => { 35 | expect(service.store.serviceCustom).toBe(false) 36 | service.store.setServiceCustom(true) 37 | expect(service.store.serviceCustom).toBe(true) 38 | }) 39 | 40 | it('does not share top-level global state between services', async () => { 41 | expect(service.store.sharedGlobal).toBe(false) 42 | service.store.toggleSharedGlobal() 43 | expect(service.store.sharedGlobal).toBe(true) 44 | // other stores still have the original value because state is not shared. 45 | expect(api.service('users').store.sharedGlobal).toBe(false) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /tests/vue-service/pinia-service.test.ts: -------------------------------------------------------------------------------- 1 | import { api, makeContactsData } from '../fixtures/index.js' 2 | import { resetService } from '../test-utils.js' 3 | 4 | const service = api.service('contacts') 5 | 6 | beforeEach(async () => { 7 | resetService(service) 8 | service.service.store = makeContactsData() 9 | await service.find({ query: { $limit: 100 } }) 10 | }) 11 | afterEach(() => resetService(service)) 12 | describe('PiniaService', () => { 13 | test('includes model methods', () => { 14 | // create an instance 15 | expect(typeof service.new).toBe('function') 16 | 17 | // api methods 18 | expect(typeof service.find).toBe('function') 19 | expect(typeof service.findOne).toBe('function') 20 | expect(typeof service.count).toBe('function') 21 | expect(typeof service.get).toBe('function') 22 | expect(typeof service.create).toBe('function') 23 | expect(typeof service.patch).toBe('function') 24 | expect(typeof service.remove).toBe('function') 25 | 26 | // local query methods 27 | expect(typeof service.findInStore).toBe('function') 28 | expect(typeof service.findOneInStore).toBe('function') 29 | expect(typeof service.countInStore).toBe('function') 30 | expect(typeof service.getFromStore).toBe('function') 31 | expect(typeof service.createInStore).toBe('function') 32 | expect(typeof service.patchInStore).toBe('function') 33 | expect(typeof service.removeFromStore).toBe('function') 34 | 35 | // hybrid methods 36 | expect(typeof service.useFind).toBe('function') 37 | expect(typeof service.useGet).toBe('function') 38 | expect(typeof service.useGetOnce).toBe('function') 39 | 40 | expect(service.store).toBeDefined() 41 | expect(service.servicePath).toBe('contacts') 42 | }) 43 | 44 | test('count', async () => { 45 | const response = await service.count() 46 | expect(response.data.length).toBe(0) 47 | expect(response.total).toBe(12) 48 | expect(response.limit).toBe(0) 49 | expect(response.skip).toBe(0) 50 | }) 51 | 52 | test('count custom query', async () => { 53 | const response = await service.count({ query: { age: { $lt: 6 } } }) 54 | expect(response.data.length).toBe(0) 55 | expect(response.total).toBe(3) 56 | expect(response.limit).toBe(0) 57 | expect(response.skip).toBe(0) 58 | }) 59 | 60 | test('count cannot override limit', async () => { 61 | const response = await service.count({ query: { $limit: 5 } }) 62 | expect(response.data.length).toBe(0) 63 | expect(response.total).toBe(12) 64 | expect(response.limit).toBe(0) 65 | expect(response.skip).toBe(0) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /tests/vue-service/service-models.dates.test.ts: -------------------------------------------------------------------------------- 1 | import { api, makeContactsData } from '../fixtures/index.js' 2 | import { resetService } from '../test-utils.js' 3 | 4 | const service = api.service('contacts') 5 | 6 | beforeEach(async () => { 7 | resetService(service) 8 | service.service.store = makeContactsData() 9 | await service.find({ query: { $limit: 100 } }) 10 | }) 11 | afterEach(() => resetService(service)) 12 | 13 | describe('Working with Dates', () => { 14 | it('findInStore with Date Strings', async () => { 15 | const query = { 16 | birthdate: { 17 | $gt: '1979-03-1T07:00:00.000Z', 18 | $lt: '2018-03-1T07:00:00.000Z', 19 | }, 20 | } 21 | const results = service.findInStore({ query }) 22 | expect(results.data.length).toBe(5) 23 | }) 24 | 25 | it('findInStore with Date objects returns no results against string data', async () => { 26 | const query = { 27 | birthdate: { 28 | $gt: new Date('1979-03-1T07:00:00.000Z'), 29 | $lt: new Date('2018-03-1T07:00:00.000Z'), 30 | }, 31 | } 32 | const results = service.findInStore({ query }) 33 | expect(results.data.length).toBe(0) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "skipLibCheck": true, 4 | "target": "esnext", 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "outDir": "dist", 10 | "sourceMap": true, 11 | "resolveJsonModule": true, 12 | "esModuleInterop": true, 13 | "lib": ["esnext", "dom"], 14 | "types": ["vite/client", "node", "vitest/globals"], 15 | "declaration": true, 16 | "strictPropertyInitialization": false 17 | }, 18 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] 19 | } 20 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import path from 'node:path' 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | import dts from 'vite-plugin-dts' 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | plugins: [vue(), dts()], 10 | server: { 11 | hmr: { 12 | port: Number.parseInt(process.env.KUBERNETES_SERVICE_PORT as string, 10) || 3000, 13 | }, 14 | }, 15 | build: { 16 | lib: { 17 | entry: path.resolve(__dirname, 'src/index.ts'), 18 | name: 'feathersPinia', 19 | fileName: 'feathers-pinia', 20 | }, 21 | sourcemap: true, 22 | rollupOptions: { 23 | // make sure to externalize deps that shouldn't be bundled 24 | // into your library 25 | external: [ 26 | 'vue-demi', 27 | 'vue', 28 | 'pinia', 29 | '@feathersjs/commons', 30 | '@feathersjs/errors', 31 | '@feathersjs/adapter-commons', 32 | '@feathersjs/rest-client', 33 | '@feathersjs/feathers', 34 | ], 35 | output: { 36 | // Provide global variables to use in the UMD build 37 | // for externalized deps 38 | globals: { 39 | 'vue-demi': 'VueDemi', 40 | 'vue': 'Vue', 41 | 'pinia': 'pinia', 42 | '@feathersjs/commons': 'commons', 43 | '@feathersjs/errors': 'errors', 44 | '@feathersjs/adapter-commons': 'adapterCommons', 45 | '@feathersjs/rest-client': 'restClient', 46 | '@feathersjs/feathers': 'feathers', 47 | }, 48 | }, 49 | }, 50 | }, 51 | test: { 52 | globals: true, 53 | deps: { 54 | interopDefault: true, 55 | }, 56 | }, 57 | }) 58 | --------------------------------------------------------------------------------