]> git.ipfire.org Git - thirdparty/vuejs/pinia.git/commitdiff
refactor: put actions inside the store
authorEduardo San Martin Morote <posva13@gmail.com>
Thu, 16 Jan 2020 17:10:25 +0000 (18:10 +0100)
committerEduardo San Martin Morote <posva13@gmail.com>
Mon, 20 Jan 2020 18:21:18 +0000 (19:21 +0100)
13 files changed:
__tests__/getters.spec.ts
__tests__/pinia/stores/cart.ts
__tests__/pinia/stores/user.ts
__tests__/ssr/app/store.ts
__tests__/store.patch.spec.ts
__tests__/store.spec.ts
__tests__/subscriptions.spec.ts
__tests__/tds/store.test-d.ts
src/index.ts
src/pinia.ts
src/store.ts
src/types.ts
tsconfig.json

index 61b585e52467ddcc445af6b9d4b27de919fc0caf..a70d09f06290d5d9339722374192c4eaf1da8a65 100644 (file)
@@ -1,15 +1,19 @@
-import { createStore } from '../src'
+import { createStore, setActiveReq } from '../src'
 
 describe('Store', () => {
-  const useStore = createStore(
-    'main',
-    () => ({
-      name: 'Eduardo',
-    }),
-    {
-      upperCaseName: ({ name }) => name.toUpperCase(),
-    }
-  ).bind(null, true)
+  const useStore = () => {
+    // create a new store
+    setActiveReq({})
+    return createStore({
+      id: 'main',
+      state: () => ({
+        name: 'Eduardo',
+      }),
+      getters: {
+        upperCaseName: ({ name }) => name.toUpperCase(),
+      },
+    })()
+  }
 
   it('adds getters to the store', () => {
     const store = useStore()
index 1f93406b4ba1bab7ae8bbeba388b42ade829deb0..b1d2dece14c9ac71f3cf27a8ca2afb950242a6f4 100644 (file)
@@ -1,12 +1,13 @@
 import { createStore } from '../../../src'
-import { useUserStore } from './user'
+import { useUserStore, UserStore } from './user'
+import { PiniaStore, ExtractGettersFromStore } from 'src/store'
 
-export const useCartStore = createStore(
-  'cart',
-  () => ({
+export const useCartStore = createStore({
+  id: 'cart',
+  state: () => ({
     rawItems: [] as string[],
   }),
-  {
+  getters: {
     items: state =>
       state.rawItems.reduce((items, item) => {
         const existingItem = items.find(it => it.name === item)
@@ -19,8 +20,21 @@ export const useCartStore = createStore(
 
         return items
       }, [] as { name: string; amount: number }[]),
-  }
-)
+  },
+})
+
+export type CartStore = ReturnType<typeof useCartStore>
+
+// const a: PiniaStore<{
+//   u: UserStore
+//   c: CartStore
+// }>
+
+// a.cart
+
+// const getters: ExtractGettersFromStore<CartStore>
+
+// getters.items
 
 export function addItem(name: string) {
   const store = useCartStore()
index 4697483a9d008baa8facf8af23418b6e9ce63451..5f97a49c5a8dcb927372be84f5283216451605df 100644 (file)
@@ -1,13 +1,49 @@
-import { createStore } from '../../../src'
+import { createStore, WrapStoreWithId } from 'src/store'
 
-export const useUserStore = createStore('user', () => ({
-  name: 'Eduardo',
-  isAdmin: true,
-}))
+function apiLogin(a: string, p: string) {
+  if (a === 'ed' && p === 'ed') return Promise.resolve({ isAdmin: true })
+  return Promise.reject(new Error('invalid credentials'))
+}
+
+export const useUserStore = createStore({
+  id: 'user',
+  state: () => ({
+    name: 'Eduardo',
+    isAdmin: true,
+  }),
+  actions: {
+    async login(user: string, password: string) {
+      const userData = await apiLogin(user, password)
+
+      this.patch({
+        name: user,
+        ...userData,
+      })
+    },
+
+    logout() {
+      this.login('a', 'b').then(() => {})
+
+      this.patch({
+        name: '',
+        isAdmin: false,
+      })
+    },
+  },
+  getters: {
+    test: state => state.name.toUpperCase(),
+  },
+})
+
+export type UserStore = ReturnType<typeof useUserStore>
+
+// let a: WrapStoreWithId<UserStore>
 
 export function logout() {
   const store = useUserStore()
 
+  store.login('e', 'e').then(() => {})
+
   store.patch({
     name: '',
     isAdmin: false,
@@ -15,18 +51,3 @@ export function logout() {
 
   // we could do other stuff like redirecting the user
 }
-
-function apiLogin(a: string, p: string) {
-  if (a === 'ed' && p === 'ed') return Promise.resolve({ isAdmin: true })
-  return Promise.reject(new Error('invalid credentials'))
-}
-
-export async function login(user: string, password: string) {
-  const store = useUserStore()
-  const userData = await apiLogin(user, password)
-
-  store.patch({
-    name: user,
-    ...userData,
-  })
-}
index ca1cbf3b89e325a338beaf725f1e27b30f932690..4a40174c3048e64441e5ece28f16469d9d3df41b 100644 (file)
@@ -1,6 +1,9 @@
 import { createStore } from '../../../src'
 
-export const useStore = createStore('main', () => ({
-  counter: 0,
-  name: 'anon',
-}))
+export const useStore = createStore({
+  id: 'main',
+  state: () => ({
+    counter: 0,
+    name: 'anon',
+  }),
+})
index 3c181a42c2d7889dec5bca8ad2c6e1e78b06533d..90352926d7e17c260a8a95ae0ad299d5edfe3415 100644 (file)
@@ -4,13 +4,16 @@ describe('store.patch', () => {
   const useStore = () => {
     // create a new store
     setActiveReq({})
-    return createStore('main', () => ({
-      a: true,
-      nested: {
-        foo: 'foo',
-        a: { b: 'string' },
-      },
-    }))()
+    return createStore({
+      id: 'main',
+      state: () => ({
+        a: true,
+        nested: {
+          foo: 'foo',
+          a: { b: 'string' },
+        },
+      }),
+    })()
   }
 
   it('patches a property without touching the rest', () => {
index 124af976c3af8cedd1a0634f0b1f5679acb0bd6d..e2048f70c6c54e3e0a9460a196114a4cd98404a1 100644 (file)
@@ -6,13 +6,16 @@ describe('Store', () => {
     // create a new store
     req = {}
     setActiveReq(req)
-    return createStore('main', () => ({
-      a: true,
-      nested: {
-        foo: 'foo',
-        a: { b: 'string' },
-      },
-    }))()
+    return createStore({
+      id: 'main',
+      state: () => ({
+        a: true,
+        nested: {
+          foo: 'foo',
+          a: { b: 'string' },
+        },
+      }),
+    })()
   }
 
   it('sets the initial state', () => {
@@ -28,13 +31,16 @@ describe('Store', () => {
 
   it('can hydrate the state', () => {
     setActiveReq({})
-    const useStore = createStore('main', () => ({
-      a: true,
-      nested: {
-        foo: 'foo',
-        a: { b: 'string' },
-      },
-    }))
+    const useStore = createStore({
+      id: 'main',
+      state: () => ({
+        a: true,
+        nested: {
+          foo: 'foo',
+          a: { b: 'string' },
+        },
+      }),
+    })
 
     setStateProvider({
       set: () => {},
index 6b867a934c360b72923f0d012fcc4336ef328e5f..bdf393eff0deff53f7e4e03357f60a22f47bdb69 100644 (file)
@@ -4,9 +4,12 @@ describe('Subscriptions', () => {
   const useStore = () => {
     // create a new store
     setActiveReq({})
-    return createStore('main', () => ({
-      name: 'Eduardo',
-    }))()
+    return createStore({
+      id: 'main',
+      state: () => ({
+        name: 'Eduardo',
+      }),
+    })()
   }
 
   let store: ReturnType<typeof useStore>
index b018fb45e347e0cb9b93b2edf64727eb185314a9..d41b6875c1a0c58f313877abcb2eb18aeac64164 100644 (file)
@@ -1,8 +1,12 @@
 import { createStore } from '../../src'
 import { expectType, expectError } from 'tsd'
 
-const useStore = createStore('name', () => ({ a: 'on' as 'on' | 'off' }), {
-  upper: state => state.a.toUpperCase(),
+const useStore = createStore({
+  id: 'name',
+  state: () => ({ a: 'on' as 'on' | 'off' }),
+  getters: {
+    upper: state => state.a.toUpperCase(),
+  },
 })
 
 const store = useStore()
index 0640b0caef2552df2d06f01d9744b45e5f31a6c6..e451ba6abc031d863cdcb756696717570ed657bd 100644 (file)
@@ -1,7 +1,2 @@
-export {
-  createStore,
-  CombinedStore,
-  setActiveReq,
-  setStateProvider,
-} from './store'
+export { createStore, Store, setActiveReq, setStateProvider } from './store'
 export { StateTree, StoreGetter } from './types'
index 24f22188461eaec13bfb7eb921e360d725d7ba57..49907b3e7c26d6b0fb6e9a463d06e0f4ae146d1c 100644 (file)
@@ -1,4 +1,4 @@
-import { Store, StoreGetter, StateTree, StoreGetters } from './types'
+import { Store, StoreGetter, StateTree, StoreWithGetters } from './types'
 import { CombinedStore, buildStore } from './store'
 
 export type CombinedState<
@@ -39,7 +39,7 @@ export type CombinedGetters<
   [k in keyof S]: S[k] extends (
     ...args: any[]
   ) => CombinedStore<string, infer State, infer Getters>
-    ? StoreGetters<State, Getters>
+    ? StoreWithGetters<State, Getters>
     : never
 }
 
index e0b40c9619410514e9d183c4621eac43952e15ea..548fa3afbc583fed69990e6cf288b8c096df0e28 100644 (file)
@@ -1,12 +1,12 @@
 import { ref, watch, computed } from '@vue/composition-api'
-import { Ref } from '@vue/composition-api/dist/reactivity'
+import { Ref, UnwrapRef } from '@vue/composition-api/dist/reactivity'
 import {
   StateTree,
-  Store,
+  StoreWithState,
   SubscriptionCallback,
   DeepPartial,
   isPlainObject,
-  StoreGetters,
+  StoreWithGetters,
   StoreGetter,
 } from './types'
 import { useStoreDevtools } from './devtools'
@@ -32,18 +32,58 @@ function innerPatch<T extends StateTree>(
   return target
 }
 
-/**
- * NOTE: by allowing users to name stores correctly, they can nest them the way
- * they want, no? like user/cart
- */
+export interface StoreAction {
+  (...args: any[]): any
+}
 
-export type CombinedStore<
+// in this type we forget about this because otherwise the type is recursive
+type StoreWithActions<A extends Record<string, StoreAction>> = {
+  [k in keyof A]: A[k] extends (this: infer This, ...args: infer P) => infer R
+    ? (this: This, ...args: P) => R
+    : never
+}
+
+// has the actions without the context (this) for typings
+export type Store<
   Id extends string,
   S extends StateTree,
-  G extends Record<string, StoreGetter<S>>
-> = Store<Id, S> & StoreGetters<S, G>
+  G extends Record<string, StoreGetter<S>>,
+  A extends Record<string, StoreAction>
+> = StoreWithState<Id, S> & StoreWithGetters<S, G> & StoreWithActions<A>
 
-// TODO: allow buildStore to start with an initial state for hydration
+export type WrapStoreWithId<
+  S extends Store<any, any, any, any>
+> = S extends Store<infer Id, infer S, infer G, infer A>
+  ? {
+      [k in Id]: Store<Id, S, G, A>
+    }
+  : never
+
+export type ExtractGettersFromStore<S> = S extends Store<
+  any,
+  infer S,
+  infer G,
+  any
+>
+  ? {
+      [k in keyof G]: ReturnType<G[k]>
+    }
+  : never
+
+export type PiniaStore<
+  P extends Record<string, Store<any, any, any, any>>
+> = P extends Record<infer name, any>
+  ? {
+      [Id in P[name]['id']]: P[name] extends Store<
+        Id,
+        infer S,
+        infer G,
+        infer A
+      >
+        ? StoreWithGetters<S, G>
+        : never
+    }
+  : never
 
 /**
  * Creates a store instance
@@ -53,14 +93,15 @@ export type CombinedStore<
 export function buildStore<
   Id extends string,
   S extends StateTree,
-  G extends Record<string, StoreGetter<S>>
+  G extends Record<string, StoreGetter<S>>,
+  A extends Record<string, StoreAction>
 >(
   id: Id,
   buildState: () => S,
   getters: G = {} as G,
+  actions: A = {} as A,
   initialState?: S | undefined
-  // methods: Record<string | symbol, StoreMethod>
-): CombinedStore<Id, S, G> {
+): Store<Id, S, G, A> {
   const state: Ref<S> = ref(initialState || buildState())
 
   let isListening = true
@@ -108,7 +149,7 @@ export function buildStore<
     state.value = buildState()
   }
 
-  const storeWithState: Store<Id, S> = {
+  const storeWithState: StoreWithState<Id, S> = {
     id,
     // it is replaced below by a getter
     state: state.value,
@@ -119,7 +160,7 @@ export function buildStore<
   }
 
   // @ts-ignore we have to build it
-  const computedGetters: StoreGetters<S, G> = {}
+  const computedGetters: StoreWithGetters<S, G> = {}
   for (const getterName in getters) {
     const method = getters[getterName]
     // @ts-ignore
@@ -143,6 +184,7 @@ export function buildStore<
     },
   })
 
+  // @ts-ignore TODO: actions
   return store
 }
 
@@ -165,7 +207,7 @@ export const getActiveReq = () => activeReq
 
 const storesMap = new WeakMap<
   NonNullObject,
-  Record<string, CombinedStore<any, any, any>>
+  Record<string, Store<any, any, any, any>>
 >()
 
 /**
@@ -173,7 +215,7 @@ const storesMap = new WeakMap<
  */
 interface StateProvider {
   get(): Record<string, StateTree>
-  set(store: CombinedStore<any, any, any>): any
+  set(store: Store<any, any, any, any>): any
 }
 
 /**
@@ -190,23 +232,30 @@ function getInitialState(id: string): StateTree | undefined {
   return provider && provider.get()[id]
 }
 
-function setInitialState(store: CombinedStore<any, any, any>): void {
+function setInitialState(store: Store<any, any, any, any>): void {
   const provider = stateProviders.get(getActiveReq())
   if (provider) provider.set(store)
 }
 
 /**
  * Creates a `useStore` function that retrieves the store instance
- * @param id id of the store we are creating
- * @param buildState function that returns a state
- * @param getters optional object of getters
+ * @param options
  */
 export function createStore<
   Id extends string,
   S extends StateTree,
-  G extends Record<string, StoreGetter<S>>
->(id: Id, buildState: () => S, getters: G = {} as G) {
-  return function useStore(): CombinedStore<Id, S, G> {
+  G extends Record<string, StoreGetter<S>>,
+  A extends Record<string, StoreAction>
+>(options: {
+  id: Id
+  state: () => S
+  getters?: G
+  // allow actions use other actions
+  actions?: A & ThisType<A & StoreWithState<Id, S> & StoreWithGetters<S, G>>
+}) {
+  const { id, state: buildState, getters, actions } = options
+
+  return function useStore(): Store<Id, S, G, A> {
     const req = getActiveReq()
     let stores = storesMap.get(req)
     if (!stores) storesMap.set(req, (stores = {}))
@@ -217,8 +266,11 @@ export function createStore<
         id,
         buildState,
         getters,
+        actions,
         getInitialState(id)
       )
+      // save a reference to the initial state
+      // TODO: this implies that replacing the store cannot be done by the user because we are relying on the object reference
       setInitialState(store)
       if (isClient) useStoreDevtools(store)
     }
index ea88e470e047c714313deff66dec6937bc54a6b1..c8776ad80856f3dee9fe453fa566b103b5776278 100644 (file)
@@ -28,14 +28,14 @@ export type SubscriptionCallback<S> = (
   state: S
 ) => void
 
-export type StoreGetters<
+export type StoreWithGetters<
   S extends StateTree,
   G extends Record<string, StoreGetter<S>>
 > = {
   [k in keyof G]: G[k] extends StoreGetter<S, infer V> ? Ref<V> : never
 }
 
-export interface Store<Id extends string, S extends StateTree> {
+export interface StoreWithState<Id extends string, S extends StateTree> {
   /**
    * Unique identifier of the store
    */
index 585906157d86e1439937ba1958d05a1bda59be96..ebd97242fbe960e2b319ccc01c5ec8091adbf9ef 100644 (file)
@@ -4,6 +4,7 @@
     "module": "esnext",
     "noEmit": true,
     "strict": true,
+    "noImplicitThis": true,
     "composite": true,
     "esModuleInterop": true,
     "moduleResolution": "node",