diff --git a/examples/vue-datastore/README.md b/examples/vue-datastore/README.md new file mode 100644 index 000000000..9762d2439 --- /dev/null +++ b/examples/vue-datastore/README.md @@ -0,0 +1,102 @@ +# vue-datastore + +## Project setup + +``` +yarn install +``` + +### Compiles and hot-reloads for development + +``` +yarn serve +``` + +### Compiles and minifies for production + +``` +yarn build +``` + +### Lints and fixes files + +``` +yarn lint +``` + +### Customize configuration + +See [Configuration Reference](https://cli.vuejs.org/config/). + +# Offix - Vue3 Todo Example App + +This example demonstrates how to get started using Offix in a Vue project. The app is a simple +todo app making use of the `offix-client` and can be used as launch pad to getting started +with Offix and make use of the features in the library. + +## Getting started + +To get started, run: + +``` +yarn install +``` + +### Setting up a server + +For simplicity, a GraphQL Serve in-memory server has been provided. You can make changes to the GrapQL schema, by editing the `models/runtime.graphql` file. To start the server, run the following +command: + +``` +yarn startServer +``` + +Alternatively, you can implement your own backend server. + +### Starting the client + +Next, configure the GraphQL server address in the `src/clientConfig.js` file: + +``` + +... + +const wsLink = new WebSocketLink({ + uri: 'ws://', + ... +}); + +const httpLink = new HttpLink({ + uri: 'http://', +}); + +... + +``` + +### Starting the client + +Lastly, run the following commands from the React example folder. + +``` +yarn start +``` + +## Adding more models + +1. Edit runtime.graphql file in `src/model/runtime.graphql` +2. Generate models yarn generate +3. Review new models + +## Running as native capacitor application + +yarn build +yarn cap add ios +yarn cap copy ios +yarn cap open ios + +yarn build +yarn cap add android +// Swap main activity https://github.com/capacitor-community/sqlite +yarn cap copy android +yarn cap open android diff --git a/examples/vue-datastore/index.html b/examples/vue-datastore/index.html new file mode 100644 index 000000000..11603f878 --- /dev/null +++ b/examples/vue-datastore/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite App + + +
+ + + diff --git a/examples/vue-datastore/package.json b/examples/vue-datastore/package.json new file mode 100644 index 000000000..913db3e2f --- /dev/null +++ b/examples/vue-datastore/package.json @@ -0,0 +1,49 @@ +{ + "name": "vue-datastore", + "version": "0.0.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "upgrade-next": "yarn yarn-upgrade-all add vue@next ant-design-vue@next", + "generate": "offix generate --schema ./src/model/runtime.graphql --outputPath ./src/datastore/generated", + "startServer": "gqlserve serve --datasync --conflict=clientSideWins --port=5400 ./src/model/runtime.graphql", + "linkdatastore": "cd ../../packages/datastore/datastore && yarn link && cd - && yarn link offix-datastore && rm -Rf ./node_modules/react && && rm -Rf ./node_modules/react-dom", + "linkdatastorecli": "cd ../../packages/datastore/cli && yarn link && cd - && yarn link @offix/cli" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "dependencies": { + "@ant-design/colors": "^6.0.0", + "ant-design-vue": "^2.0.1", + "offix-datastore": "^0.4.0", + "vue": "^3.0.6", + "@ant-design/icons-vue": "^6.0.1" + }, + "devDependencies": { + "@offix/cli": "^0.3.3", + "@types/node": "^14.14.31", + "@vitejs/plugin-vue": "^1.1.4", + "@vitejs/plugin-vue-jsx": "^1.1.2", + "@vue/compiler-sfc": "^3.0.5", + "babel-plugin-import": "^1.13.3", + "eslint": "^7.21.0", + "eslint-plugin-vue": "^7.6.0", + "graphback-cli": "^1.1.2", + "graphql-serve": "1.1.2", + "sass": "^1.32.8", + "typescript": "^4.2.2", + "vite": "^2.0.4", + "yarn-upgrade-all": "^0.5.4" + } +} diff --git a/examples/vue-datastore/public/favicon.ico b/examples/vue-datastore/public/favicon.ico new file mode 100644 index 000000000..df36fcfb7 Binary files /dev/null and b/examples/vue-datastore/public/favicon.ico differ diff --git a/examples/vue-datastore/src/App.vue b/examples/vue-datastore/src/App.vue new file mode 100644 index 000000000..26a67cfb1 --- /dev/null +++ b/examples/vue-datastore/src/App.vue @@ -0,0 +1,93 @@ + + diff --git a/examples/vue-datastore/src/components/Todo/TodoItem.tsx b/examples/vue-datastore/src/components/Todo/TodoItem.tsx new file mode 100644 index 000000000..98e9db79d --- /dev/null +++ b/examples/vue-datastore/src/components/Todo/TodoItem.tsx @@ -0,0 +1,57 @@ +import { DeleteOutlined, EditOutlined } from "@ant-design/icons-vue"; +import { defineComponent, h, ref } from "@vue/runtime-core"; +import { PropType } from "vue"; +import { useDeleteTodo } from "../../datastore/hooks"; +import EditTodo from "../forms/EditTodo.vue"; +import ToggleTodo from "../forms/ToggleTodo.vue"; +import { Todo } from "/@/datastore/generated"; +export const TodoItem = defineComponent({ + props: { + todo: { + type: Object as PropType, + required: true, + }, + }, + components: { + ToggleTodo, + EditTodo, + }, + setup(props) { + const { remove: deleteTodo } = useDeleteTodo(); + const edit = ref(false); + const handleDelete = () => { + deleteTodo({ _id: props.todo._id }) + .then((res) => console.log("response", res)) + .catch((error: any) => console.log(error)); + }; + + return () => + h( + edit.value ? ( + (edit.value = !edit.value)} + /> + ) : ( + + + +

+ Description: +
+ {props.todo.description} +

+
+ + (edit.value = true)}> + + + + + + +
+ ) + ); + }, +}); diff --git a/examples/vue-datastore/src/components/Todo/TodoList.tsx b/examples/vue-datastore/src/components/Todo/TodoList.tsx new file mode 100644 index 000000000..c1e3545b8 --- /dev/null +++ b/examples/vue-datastore/src/components/Todo/TodoList.tsx @@ -0,0 +1,32 @@ +import { computed, defineComponent, h, PropType } from "vue"; +import { Todo } from "../../datastore/generated"; +import { Empty } from "../UI"; +import { TodoItem } from "./TodoItem"; + +export const TodoList = defineComponent({ + props: { + todos: { + type: Array as PropType, + required: true, + default: () => [], + }, + }, + components: { + TodoItem, + Empty, + }, + setup(props) { + const noTodos = computed(() => !props.todos || props.todos.length === 0); + return () => + h("div", {}, [ + noTodos.value ? h() : null, + ...props.todos.map((todo) => + h( + + + + ) + ), + ]); + }, +}); diff --git a/examples/vue-datastore/src/components/Todo/index.ts b/examples/vue-datastore/src/components/Todo/index.ts new file mode 100644 index 000000000..4022695b4 --- /dev/null +++ b/examples/vue-datastore/src/components/Todo/index.ts @@ -0,0 +1,2 @@ +export * from "./TodoItem"; +export * from "./TodoList"; diff --git a/examples/vue-datastore/src/components/UI/Empty.tsx b/examples/vue-datastore/src/components/UI/Empty.tsx new file mode 100644 index 000000000..d2570c171 --- /dev/null +++ b/examples/vue-datastore/src/components/UI/Empty.tsx @@ -0,0 +1,17 @@ +//@ts-nocheck //FIXME: remove ignore +import { defineComponent, h } from "@vue/runtime-core"; + +export const Empty = defineComponent({ + setup() { + return () => + h( +
+
+ +
+

You have no todo items

+

Click the button to create a new task

+
+ ); + } +}); diff --git a/examples/vue-datastore/src/components/UI/Error.tsx b/examples/vue-datastore/src/components/UI/Error.tsx new file mode 100644 index 000000000..647f70f75 --- /dev/null +++ b/examples/vue-datastore/src/components/UI/Error.tsx @@ -0,0 +1,22 @@ +import { defineComponent, h } from "vue"; + +export const Error = defineComponent({ + name: "Error", + props: { + message: { + type: String, + required: false, + default: "", + }, + }, + setup(props) { + return () => + h( + + ); + }, +}); diff --git a/examples/vue-datastore/src/components/UI/Loading.vue b/examples/vue-datastore/src/components/UI/Loading.vue new file mode 100644 index 000000000..328b5fb6b --- /dev/null +++ b/examples/vue-datastore/src/components/UI/Loading.vue @@ -0,0 +1,13 @@ + + diff --git a/examples/vue-datastore/src/components/UI/index.ts b/examples/vue-datastore/src/components/UI/index.ts new file mode 100644 index 000000000..b7f93e1a7 --- /dev/null +++ b/examples/vue-datastore/src/components/UI/index.ts @@ -0,0 +1,2 @@ +export { Empty } from "./Empty"; +export { Error } from "./Error"; diff --git a/examples/vue-datastore/src/components/forms/AddTodo.vue b/examples/vue-datastore/src/components/forms/AddTodo.vue new file mode 100644 index 000000000..89c241f9c --- /dev/null +++ b/examples/vue-datastore/src/components/forms/AddTodo.vue @@ -0,0 +1,39 @@ + + diff --git a/examples/vue-datastore/src/components/forms/EditTodo.vue b/examples/vue-datastore/src/components/forms/EditTodo.vue new file mode 100644 index 000000000..7aa687530 --- /dev/null +++ b/examples/vue-datastore/src/components/forms/EditTodo.vue @@ -0,0 +1,47 @@ + + diff --git a/examples/vue-datastore/src/components/forms/ToggleTodo.vue b/examples/vue-datastore/src/components/forms/ToggleTodo.vue new file mode 100644 index 000000000..644c15db0 --- /dev/null +++ b/examples/vue-datastore/src/components/forms/ToggleTodo.vue @@ -0,0 +1,40 @@ + + diff --git a/examples/vue-datastore/src/components/forms/formSchema.ts b/examples/vue-datastore/src/components/forms/formSchema.ts new file mode 100644 index 000000000..eb0e40fc2 --- /dev/null +++ b/examples/vue-datastore/src/components/forms/formSchema.ts @@ -0,0 +1,33 @@ +// import { GraphQLBridge } from 'uniforms-bridge-graphql'; +// import { buildASTSchema } from 'graphql'; +// import { loader } from 'graphql.macro'; +// import { Todo } from '../../datastore/generated'; + +// // import the grapqhl model +// const model = loader('../../model/runtime.graphql'); + +// const validator = (model: Todo) => { +// const details = []; + +// if (!model.title) { +// details.push({ name: 'title' }); +// } + +// if (details.length) { +// // eslint-disable-next-line +// throw { details }; +// } +// }; + +// const data = { +// title: { +// required: true, +// errorMessage: 'Title is required', +// } +// } + +// export const schema = new GraphQLBridge( +// buildASTSchema((model)).getType('Todo'), +// validator, +// data, +// ); diff --git a/examples/vue-datastore/src/components/index.ts b/examples/vue-datastore/src/components/index.ts new file mode 100644 index 000000000..566d0f985 --- /dev/null +++ b/examples/vue-datastore/src/components/index.ts @@ -0,0 +1,2 @@ +export * from "./Todo"; +export * from "./UI"; diff --git a/examples/vue-datastore/src/datastore/config.ts b/examples/vue-datastore/src/datastore/config.ts new file mode 100644 index 000000000..ad7d7439a --- /dev/null +++ b/examples/vue-datastore/src/datastore/config.ts @@ -0,0 +1,32 @@ +import { DataStore } from "offix-datastore"; +import { schema, Todo, User } from "./generated"; + +export const datastore = new DataStore({ + dbName: "offix-datasync", + replicationConfig: { + client: { + url: "http://localhost:5400/graphql", + wsUrl: "ws://localhost:5400/graphql", + }, + delta: { enabled: true, pullInterval: 20000 }, + mutations: { enabled: true }, + liveupdates: { enabled: true }, + }, +}); + +export const TodoModel = datastore.setupModel(schema.Todo); +export const UserModel = datastore.setupModel(schema.User); + +datastore.init(); + +// After init we can start replication immediately with: +// datastore.startReplication() +// Or we can start replication at a later stage. +// +// we can also execute operations freely using hooks in components and plain js. +// const user = { name: "User" + new Date().getTime() }; +// UserModel.save(user).then(async (result) => { +// result.name = "NewUser"; +// await UserModel.updateById(result); +// await UserModel.removeById(result.id as string); +// }).catch(console.log) diff --git a/examples/vue-datastore/src/datastore/generated/index.ts b/examples/vue-datastore/src/datastore/generated/index.ts new file mode 100644 index 000000000..38c469346 --- /dev/null +++ b/examples/vue-datastore/src/datastore/generated/index.ts @@ -0,0 +1,5 @@ +import { GeneratedModelSchema } from "offix-datastore"; +import jsonSchema from "./schema.json"; + +export const schema = jsonSchema as GeneratedModelSchema; +export * from "./types"; \ No newline at end of file diff --git a/examples/vue-datastore/src/datastore/generated/schema.json b/examples/vue-datastore/src/datastore/generated/schema.json new file mode 100644 index 000000000..9a435673b --- /dev/null +++ b/examples/vue-datastore/src/datastore/generated/schema.json @@ -0,0 +1,67 @@ +{ + "Todo": { + "name": "Todo", + "version": 1, + "type": "object", + "primaryKey": "_id", + "properties": { + "_id": { + "type": "string", + "key": "_id", + "isRequired": true, + "primary": true + }, + "title": { + "type": "string", + "key": "title" + }, + "description": { + "type": "string", + "key": "description" + }, + "completed": { + "type": "boolean", + "key": "completed" + }, + "_version": { + "type": "string", + "key": "_version", + "isRequired": true + }, + "_lastUpdatedAt": { + "type": "number", + "key": "_lastUpdatedAt", + "isRequired": true + } + } + }, + "User": { + "name": "User", + "version": 1, + "type": "object", + "primaryKey": "_id", + "properties": { + "_id": { + "type": "string", + "key": "_id", + "isRequired": true, + "primary": true + }, + "name": { + "type": "string", + "key": "name", + "isRequired": true + }, + "_version": { + "type": "string", + "key": "_version", + "isRequired": true + }, + "_lastUpdatedAt": { + "type": "number", + "key": "_lastUpdatedAt", + "isRequired": true + } + } + } +} \ No newline at end of file diff --git a/examples/vue-datastore/src/datastore/generated/types.ts b/examples/vue-datastore/src/datastore/generated/types.ts new file mode 100644 index 000000000..e49297bdc --- /dev/null +++ b/examples/vue-datastore/src/datastore/generated/types.ts @@ -0,0 +1,20 @@ +export interface Todo { + _id: string; + title?: string; + description?: string; + completed?: boolean; + _version: string; + _lastUpdatedAt: number +} + +export type TodoCreate = Omit; +export type TodoChange = Pick & Partial; +export interface User { + _id: string; + name: string; + _version: string; + _lastUpdatedAt: number +} + +export type UserCreate = Omit; +export type UserChange = Pick & Partial; \ No newline at end of file diff --git a/examples/vue-datastore/src/datastore/hooks.ts b/examples/vue-datastore/src/datastore/hooks.ts new file mode 100644 index 000000000..4faf9fb53 --- /dev/null +++ b/examples/vue-datastore/src/datastore/hooks.ts @@ -0,0 +1,23 @@ +import { Filter } from "offix-datastore"; +import { Model } from "../../../../packages/datastore/datastore/src/Model"; +import { + useQuery, + useRemove, + useSave, + useUpdate, +} from "../../../../packages/datastore/src/vue"; +import { TodoModel } from "./config"; +import { Todo, TodoChange, TodoCreate } from "./generated"; +// FIXME: how to handle wrong model type from package and from monorepo? +const castModel = (model: unknown) => { + return model as Model; +}; +const castedTodoModel = castModel(TodoModel); +export const useFindTodos = (filter?: Filter) => + useQuery({ model: castedTodoModel, filter: filter }); + +export const useAddTodo = () => useSave(castedTodoModel); + +export const useEditTodo = () => useUpdate(castedTodoModel); + +export const useDeleteTodo = () => useRemove(castedTodoModel); diff --git a/examples/vue-datastore/src/main.ts b/examples/vue-datastore/src/main.ts new file mode 100644 index 000000000..fade0d8fe --- /dev/null +++ b/examples/vue-datastore/src/main.ts @@ -0,0 +1,9 @@ +// import "@/styles/index.scss"; +import Antd from "ant-design-vue"; +import "ant-design-vue/dist/antd.css"; +import { createApp } from "vue"; +import App from "./App.vue"; + +const app = createApp(App); +app.use(Antd); +app.mount("#app"); diff --git a/examples/vue-datastore/src/model/runtime.graphql b/examples/vue-datastore/src/model/runtime.graphql new file mode 100644 index 000000000..dfcd9f323 --- /dev/null +++ b/examples/vue-datastore/src/model/runtime.graphql @@ -0,0 +1,30 @@ +scalar GraphbackObjectID +scalar GraphbackTimestamp + +""" +@model +@datasync +@versioned +""" +type Todo { + _id: GraphbackObjectID! + title: String + description: String + completed: Boolean + _version: String + _lastUpdatedAt: GraphbackTimestamp +} + +""" +@model +@datasync +@versioned +""" +type User { + _id: GraphbackObjectID! + name: String! + _version: String + _lastUpdatedAt: GraphbackTimestamp + # TODO + # todos: [Todo!]! +} diff --git a/examples/vue-datastore/src/shims-vue.d.ts b/examples/vue-datastore/src/shims-vue.d.ts new file mode 100644 index 000000000..ac1ded792 --- /dev/null +++ b/examples/vue-datastore/src/shims-vue.d.ts @@ -0,0 +1,5 @@ +declare module '*.vue' { + import { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/examples/vue-datastore/src/types.ts b/examples/vue-datastore/src/types.ts new file mode 100644 index 000000000..979af5c6a --- /dev/null +++ b/examples/vue-datastore/src/types.ts @@ -0,0 +1,39 @@ +import { Todo } from "./datastore/generated"; + +export type TodoProps = { + todo: Todo; +}; + +export type TodoListProps = { + todos: Array; +}; + +export type AddTodoProps = { + cancel: () => void; +}; + +export type EditTodoProps = { + todo: Todo; + toggleEdit: () => void; +}; + +export type ToggleTodoProps = { + todo: Todo; +}; + +export type HookState = { + data: any | null; + loading: boolean; + error: Error | null; +}; + +export enum ActionType { + REQ_START = 0, + REQ_SUCCESS = 1, + REQ_FAILED = 2 +} + +export type ReducerAction = { + type: ActionType; + payload?: any; +}; diff --git a/examples/vue-datastore/tsconfig.json b/examples/vue-datastore/tsconfig.json new file mode 100644 index 000000000..2ac9574ad --- /dev/null +++ b/examples/vue-datastore/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "moduleResolution": "node", + "strict": true, + "jsx": "preserve", + "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "lib": ["esnext", "dom"], + "types": ["vite/client"], + "importHelpers": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "baseUrl": ".", + "paths": { + "/@/*": ["src/*"] + } + }, + "exclude": ["node_modules", "dist"], + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/examples/vue-datastore/vite.config.ts b/examples/vue-datastore/vite.config.ts new file mode 100644 index 000000000..785fa8039 --- /dev/null +++ b/examples/vue-datastore/vite.config.ts @@ -0,0 +1,22 @@ +import vue from "@vitejs/plugin-vue"; +import vueJsx from "@vitejs/plugin-vue-jsx"; +import { resolve } from "path"; +import { defineConfig } from "vite"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + vue(), + vueJsx({ + // options are passed on to @vue/babel-plugin-jsx + }), + ], + resolve: { + alias: { + "@/": resolve(__dirname, "src"), + }, + }, + optimizeDeps: { + include: ["@ant-design/icons-vue"], + }, +}); diff --git a/packages/datastore/package.json b/packages/datastore/package.json index 55f12e34f..a7f89a431 100644 --- a/packages/datastore/package.json +++ b/packages/datastore/package.json @@ -21,7 +21,8 @@ "size:why": "size-limit --why" }, "peerDependencies": { - "react": "17.0.1" + "react": "17.0.1", + "vue": "3.0.6" }, "peerDependenciesMeta": { "react": { @@ -47,7 +48,8 @@ "size-limit": "4.9.2", "supports-color": "8.1.1", "ts-jest": "26.5.1", - "typescript": "4.1.5" + "typescript": "4.1.5", + "vue": "3.0.6" }, "dependencies": { "graphql-tag": "2.11.0", diff --git a/packages/datastore/src/index.ts b/packages/datastore/src/index.ts index a5115dac4..a49ad3982 100644 --- a/packages/datastore/src/index.ts +++ b/packages/datastore/src/index.ts @@ -1,5 +1,6 @@ -export * from "./storage"; -export * from "./ModelSchema"; export * from "./DataStore"; export * from "./filters"; +export * from "./ModelSchema"; export * from "./react"; +export * from "./storage"; +export * as useDatastoreHooks from "./vue"; diff --git a/packages/datastore/src/react/ReducerUtils.ts b/packages/datastore/src/react/ReducerUtils.ts index d4394bb03..4899c769b 100644 --- a/packages/datastore/src/react/ReducerUtils.ts +++ b/packages/datastore/src/react/ReducerUtils.ts @@ -1,38 +1,40 @@ -export enum ActionType { - INITIATE_REQUEST, - REQUEST_COMPLETE, - UPDATE_RESULT, - DELTA_FORCED -} +import { ActionType } from "../utils/ActionsTypes"; export interface Action { - type: ActionType; - data?: any; - error?: any; + type: ActionType; + data?: any; + error?: any; } export interface ResultState { - loading: boolean; - data?: any; - error?: any; + loading: boolean; + data?: any; + error?: any; } export const InitialState: ResultState = { loading: false }; export const reducer = (state: ResultState, action: Action) => { - switch (action.type) { - case ActionType.INITIATE_REQUEST: - return { ...state, loading: true, error: null }; + switch (action.type) { + case ActionType.INITIATE_REQUEST: + return { ...state, loading: true, error: null }; - case ActionType.REQUEST_COMPLETE: - return { ...state, loading: false, data: action.data, error: action.error }; + case ActionType.REQUEST_COMPLETE: + return { + ...state, + loading: false, + data: action.data, + error: action.error + }; - case ActionType.UPDATE_RESULT: - // Don't update result when request is loading - if (state.loading) { return state; } - return { ...state, data: action.data }; + case ActionType.UPDATE_RESULT: + // Don't update result when request is loading + if (state.loading) { + return state; + } + return { ...state, data: action.data }; - default: - return state; - } + default: + return state; + } }; diff --git a/packages/datastore/src/react/hooks/delete.ts b/packages/datastore/src/react/hooks/delete.ts index 0cebfdabd..133dd3063 100644 --- a/packages/datastore/src/react/hooks/delete.ts +++ b/packages/datastore/src/react/hooks/delete.ts @@ -1,23 +1,26 @@ import { useReducer } from "react"; -import { Model } from "../../Model"; -import { reducer, InitialState, ActionType } from "../ReducerUtils"; import { Filter } from "../../filters"; +import { Model } from "../../Model"; +import { ActionType } from "../../utils/ActionsTypes"; +import { InitialState, reducer } from "../ReducerUtils"; export const useRemove = (model: Model) => { - const [state, dispatch] = useReducer(reducer, InitialState); + const [state, dispatch] = useReducer(reducer, InitialState); - const remove = async (filter: Filter) => { - if (state.loading) { return; } + const remove = async (filter: Filter) => { + if (state.loading) { + return; + } - dispatch({ type: ActionType.INITIATE_REQUEST }); - try { - const results = await model.remove(filter); - dispatch({ type: ActionType.REQUEST_COMPLETE, data: results }); - return results; - } catch (error) { - dispatch({ type: ActionType.REQUEST_COMPLETE, error }); - } - }; + dispatch({ type: ActionType.INITIATE_REQUEST }); + try { + const results = await model.remove(filter); + dispatch({ type: ActionType.REQUEST_COMPLETE, data: results }); + return results; + } catch (error) { + dispatch({ type: ActionType.REQUEST_COMPLETE, error }); + } + }; - return { ...state, remove }; + return { ...state, remove }; }; diff --git a/packages/datastore/src/react/hooks/query.ts b/packages/datastore/src/react/hooks/query.ts index 8b404bcad..1069471b0 100644 --- a/packages/datastore/src/react/hooks/query.ts +++ b/packages/datastore/src/react/hooks/query.ts @@ -1,129 +1,170 @@ -import { useEffect, useReducer, useCallback, Dispatch } from "react"; +import { Dispatch, useCallback, useEffect, useReducer } from "react"; +import { Filter } from "../../filters"; import { Model } from "../../Model"; -import { reducer, InitialState, ActionType, Action, ResultState } from "../ReducerUtils"; import { CRUDEvents, StoreChangeEvent } from "../../storage"; -import { Filter } from "../../filters"; +import { ActionType } from "../../utils/ActionsTypes"; +import { Action, InitialState, reducer, ResultState } from "../ReducerUtils"; const onAdded = (currentData: any[], newData: any[]) => { - if (!currentData) { return newData; } - return [...currentData, ...newData]; + if (!currentData) { + return newData; + } + return [...currentData, ...newData]; }; const onChanged = (currentData: any[], newData: any[], primaryKey: string) => { - if (!currentData) { return []; } - // What happens to data that get's updated and falls outside original query filter? - return currentData.map((d) => { - const index = newData.findIndex((newD) => newD[primaryKey] === d[primaryKey]); - if (index === -1) { return d; } - return newData[index]; - }); + if (!currentData) { + return []; + } + // What happens to data that get's updated and falls outside original query filter? + return currentData.map((d) => { + const index = newData.findIndex( + (newD) => newD[primaryKey] === d[primaryKey] + ); + if (index === -1) { + return d; + } + return newData[index]; + }); }; -const onIdSwapped = (currentData: any[], newData: any[], primaryKey: string) => { - if (!currentData) { return []; } - - return currentData.map((d) => { - const index = newData.findIndex((newD) => newD.previous[primaryKey] === d[primaryKey]); - if (index === -1) { return d; } - return newData[index].current; - }); +const onIdSwapped = ( + currentData: any[], + newData: any[], + primaryKey: string +) => { + if (!currentData) { + return []; + } + + return currentData.map((d) => { + const index = newData.findIndex( + (newD) => newD.previous[primaryKey] === d[primaryKey] + ); + if (index === -1) { + return d; + } + return newData[index].current; + }); }; -const onRemoved = (currentData: any[], removedData: any[], primaryKey: string) => { - if (!currentData) { return []; } - return currentData - .filter( - (d) => removedData.findIndex((newD) => newD[primaryKey] === d[primaryKey]) - ); +const onRemoved = ( + currentData: any[], + removedData: any[], + primaryKey: string +) => { + if (!currentData) { + return []; + } + return currentData.filter((d) => + removedData.findIndex((newD) => newD[primaryKey] === d[primaryKey]) + ); }; -export const updateResult = (state: ResultState, event: StoreChangeEvent, primaryKey: string) => { - switch (event.eventType) { - case CRUDEvents.ADD: - return onAdded(state.data, event.data); +export const updateResult = ( + state: ResultState, + event: StoreChangeEvent, + primaryKey: string +) => { + switch (event.eventType) { + case CRUDEvents.ADD: + return onAdded(state.data, event.data); - case CRUDEvents.UPDATE: - return onChanged(state.data, event.data, primaryKey); + case CRUDEvents.UPDATE: + return onChanged(state.data, event.data, primaryKey); - case CRUDEvents.ID_SWAP: - return onIdSwapped(state.data, event.data, primaryKey); + case CRUDEvents.ID_SWAP: + return onIdSwapped(state.data, event.data, primaryKey); - case CRUDEvents.DELETE: - return onRemoved(state.data, event.data, primaryKey); + case CRUDEvents.DELETE: + return onRemoved(state.data, event.data, primaryKey); - default: - throw new Error(`Invalid event ${event.eventType} received`); - } + default: + throw new Error(`Invalid event ${event.eventType} received`); + } }; -const createSubscribeToUpdates = (state: ResultState, model: Model, dispatch: Dispatch) => { - return (eventsToWatch?: CRUDEvents[], customEventHandler?: (state: ResultState, data: any) => any) => { - const subscription = model.subscribe((event) => { - let newData; - - if (customEventHandler) { - newData = customEventHandler(state, event.data); - } - newData = updateResult(state, event, model.getSchema().getPrimaryKey()); - - if (!subscription.closed) { - // Important to check beacuse Componnent could be unmounted - dispatch({ type: ActionType.UPDATE_RESULT, data: newData }); - } - }, eventsToWatch); - return subscription; - }; +const createSubscribeToUpdates = ( + state: ResultState, + model: Model, + dispatch: Dispatch +) => { + return ( + eventsToWatch?: CRUDEvents[], + customEventHandler?: (state: ResultState, data: any) => any + ) => { + const subscription = model.subscribe((event) => { + let newData; + + if (customEventHandler) { + newData = customEventHandler(state, event.data); + } + newData = updateResult(state, event, model.getSchema().getPrimaryKey()); + + if (!subscription.closed) { + // Important to check beacuse Componnent could be unmounted + dispatch({ type: ActionType.UPDATE_RESULT, data: newData }); + } + }, eventsToWatch); + return subscription; + }; }; export const useQuery = (model: Model, selector?: Filter | string) => { - const [state, dispatch] = useReducer(reducer, InitialState); - const subscribeToUpdates = useCallback( - createSubscribeToUpdates(state, model, dispatch), [state, model, dispatch] - ); - - useEffect(() => { - (async () => { - if (state.loading) { return; } - - dispatch({ type: ActionType.INITIATE_REQUEST }); - try { - let results; - if ((typeof selector) === "string") { - results = await model.queryById(selector as string); - } else { - results = await model.query(selector); - } - dispatch({ type: ActionType.REQUEST_COMPLETE, data: results }); - } catch (error) { - dispatch({ type: ActionType.REQUEST_COMPLETE, error }); - } - })(); - }, [model, selector]); - return { ...state, subscribeToUpdates }; + const [state, dispatch] = useReducer(reducer, InitialState); + const subscribeToUpdates = useCallback( + createSubscribeToUpdates(state, model, dispatch), + [state, model, dispatch] + ); + + useEffect(() => { + (async () => { + if (state.loading) { + return; + } + + dispatch({ type: ActionType.INITIATE_REQUEST }); + try { + let results; + if (typeof selector === "string") { + results = await model.queryById(selector as string); + } else { + results = await model.query(selector); + } + dispatch({ type: ActionType.REQUEST_COMPLETE, data: results }); + } catch (error) { + dispatch({ type: ActionType.REQUEST_COMPLETE, error }); + } + })(); + }, [model, selector]); + return { ...state, subscribeToUpdates }; }; export const useLazyQuery = (model: Model) => { - const [state, dispatch] = useReducer(reducer, InitialState); - const subscribeToUpdates = useCallback( - createSubscribeToUpdates(state, model, dispatch), [state, model, dispatch] - ); + const [state, dispatch] = useReducer(reducer, InitialState); + const subscribeToUpdates = useCallback( + createSubscribeToUpdates(state, model, dispatch), + [state, model, dispatch] + ); + + const query = async (selector?: Filter | string) => { + if (state.loading) { + return; + } - const query = async (selector?: Filter | string) => { - if (state.loading) { return; } - - dispatch({ type: ActionType.INITIATE_REQUEST }); - try { - let results; - if ((typeof selector) === "string") { - results = await model.queryById(selector as string); - } else { - results = await model.query(selector); - } - dispatch({ type: ActionType.REQUEST_COMPLETE, data: results }); - } catch (error) { - dispatch({ type: ActionType.REQUEST_COMPLETE, error }); - } - }; + dispatch({ type: ActionType.INITIATE_REQUEST }); + try { + let results; + if (typeof selector === "string") { + results = await model.queryById(selector as string); + } else { + results = await model.query(selector); + } + dispatch({ type: ActionType.REQUEST_COMPLETE, data: results }); + } catch (error) { + dispatch({ type: ActionType.REQUEST_COMPLETE, error }); + } + }; - return { ...state, query, subscribeToUpdates }; + return { ...state, query, subscribeToUpdates }; }; diff --git a/packages/datastore/src/react/hooks/save.ts b/packages/datastore/src/react/hooks/save.ts index ffa668b60..9753281a2 100644 --- a/packages/datastore/src/react/hooks/save.ts +++ b/packages/datastore/src/react/hooks/save.ts @@ -1,22 +1,25 @@ import { useReducer } from "react"; import { Model } from "../../Model"; -import { reducer, InitialState, ActionType } from "../ReducerUtils"; +import { ActionType } from "../../utils/ActionsTypes"; +import { InitialState, reducer } from "../ReducerUtils"; export const useSave = (model: Model) => { - const [state, dispatch] = useReducer(reducer, InitialState); + const [state, dispatch] = useReducer(reducer, InitialState); - const save = async (input: any) => { - if (state.loading) { return; } + const save = async (input: any) => { + if (state.loading) { + return; + } - dispatch({ type: ActionType.INITIATE_REQUEST }); - try { - const result = await model.save(input); - dispatch({ type: ActionType.REQUEST_COMPLETE, data: result }); - return result; - } catch (error) { - dispatch({ type: ActionType.REQUEST_COMPLETE, error }); - } - }; + dispatch({ type: ActionType.INITIATE_REQUEST }); + try { + const result = await model.save(input); + dispatch({ type: ActionType.REQUEST_COMPLETE, data: result }); + return result; + } catch (error) { + dispatch({ type: ActionType.REQUEST_COMPLETE, error }); + } + }; - return { ...state, save }; + return { ...state, save }; }; diff --git a/packages/datastore/src/react/hooks/subscription.ts b/packages/datastore/src/react/hooks/subscription.ts index 29cb17118..49d17c67e 100644 --- a/packages/datastore/src/react/hooks/subscription.ts +++ b/packages/datastore/src/react/hooks/subscription.ts @@ -1,7 +1,8 @@ import { useEffect, useReducer } from "react"; -import { Model } from "../../Model"; import { CRUDEvents } from "../.."; -import { InitialState, reducer, ActionType } from "../ReducerUtils"; +import { Model } from "../../Model"; +import { ActionType } from "../../utils/ActionsTypes"; +import { InitialState, reducer } from "../ReducerUtils"; export const useSubscription = (model: Model, eventTypes: CRUDEvents[]) => { const [state, dispatch] = useReducer(reducer, InitialState); diff --git a/packages/datastore/src/react/hooks/update.ts b/packages/datastore/src/react/hooks/update.ts index 422127ee6..c353a894e 100644 --- a/packages/datastore/src/react/hooks/update.ts +++ b/packages/datastore/src/react/hooks/update.ts @@ -1,24 +1,30 @@ import { useReducer } from "react"; import { Model } from "../../Model"; -import { reducer, InitialState, ActionType } from "../ReducerUtils"; +import { ActionType } from "../../utils/ActionsTypes"; +import { InitialState, reducer } from "../ReducerUtils"; export const useUpdate = (model: Model) => { - const [state, dispatch] = useReducer(reducer, InitialState); + const [state, dispatch] = useReducer(reducer, InitialState); - const update = async (input: any, upsert: boolean = false) => { - if (state.loading) { return; } - if (state.data) { return; } + const update = async (input: any, upsert: boolean = false) => { + if (state.loading) { + return; + } + if (state.data) { + return; + } - dispatch({ type: ActionType.INITIATE_REQUEST }); - try { - const results = await (upsert ? - model.saveOrUpdate(input): model.updateById(input)); - dispatch({ type: ActionType.REQUEST_COMPLETE, data: results }); - return results; - } catch (error) { - dispatch({ type: ActionType.REQUEST_COMPLETE, error }); - } - }; + dispatch({ type: ActionType.INITIATE_REQUEST }); + try { + const results = await (upsert + ? model.saveOrUpdate(input) + : model.updateById(input)); + dispatch({ type: ActionType.REQUEST_COMPLETE, data: results }); + return results; + } catch (error) { + dispatch({ type: ActionType.REQUEST_COMPLETE, error }); + } + }; - return { ...state, update }; + return { ...state, update }; }; diff --git a/packages/datastore/src/utils/ActionsTypes.ts b/packages/datastore/src/utils/ActionsTypes.ts new file mode 100644 index 000000000..f46c965d8 --- /dev/null +++ b/packages/datastore/src/utils/ActionsTypes.ts @@ -0,0 +1,6 @@ +export enum ActionType { + INITIATE_REQUEST, + REQUEST_COMPLETE, + UPDATE_RESULT, + DELTA_FORCED, +} diff --git a/packages/datastore/src/vue/StateUtils.ts b/packages/datastore/src/vue/StateUtils.ts new file mode 100644 index 000000000..844c4267d --- /dev/null +++ b/packages/datastore/src/vue/StateUtils.ts @@ -0,0 +1,56 @@ +import { Maybe } from "graphql/jsutils/Maybe"; +import { Ref, ref } from "vue"; +import { ActionType } from "../utils/ActionsTypes"; + +export interface Action { + type: ActionType; + data?: Maybe[] | TModel>; + error?: Maybe; +} + +export interface IdSwap { + previous: TModel; + current: TModel; +} +export interface ReactiveState { + loading: boolean; + data: Maybe[]; + error: Maybe; +} +export const initialState = (): Ref> => + ref>({ + loading: false, + data: [], + error: null + }) as Ref>; +export const changeState = ({ + action, + state +}: { + state: Ref>; + action: Action; +}) => { + const data = (() => { + if (action.data == null) {return [];} + if (Array.isArray(action.data)) {return action.data;} + return [action.data]; + })(); + switch (action.type) { + case ActionType.INITIATE_REQUEST: + state.value.loading = true; + state.value.error = null; + break; + case ActionType.REQUEST_COMPLETE: + state.value.loading = false; + state.value.data = data; + state.value.error = action.error; + break; + case ActionType.UPDATE_RESULT: + // Don't update result when request is loading + if (!state.value.loading) { + state.value.data = data; + } + break; + } + return state; +}; diff --git a/packages/datastore/src/vue/hooks/delete.ts b/packages/datastore/src/vue/hooks/delete.ts new file mode 100644 index 000000000..9626d6a75 --- /dev/null +++ b/packages/datastore/src/vue/hooks/delete.ts @@ -0,0 +1,36 @@ +import { Filter } from "../../filters"; +import { Model } from "../../Model"; +import { ActionType } from "../../utils/ActionsTypes"; +import { changeState, initialState } from "../StateUtils"; + +export const useRemove = (model: Model) => { + const state = initialState(); + + const remove = async ( + filter: Filter> + ) => { + if (state.value.loading) {return;} + + changeState({ + state, + action: { type: ActionType.INITIATE_REQUEST } + }); + try { + const results = (await model.remove( + (filter as unknown) as Filter + )) as TModel[]; + changeState({ + state, + action: { type: ActionType.REQUEST_COMPLETE, data: results } + }); + return results; + } catch (error) { + changeState({ + state, + action: { type: ActionType.REQUEST_COMPLETE, error } + }); + } + }; + + return { state: state, remove }; +}; diff --git a/packages/datastore/src/vue/hooks/query.ts b/packages/datastore/src/vue/hooks/query.ts new file mode 100644 index 000000000..28543e0c1 --- /dev/null +++ b/packages/datastore/src/vue/hooks/query.ts @@ -0,0 +1,251 @@ +import { Maybe } from "graphql/jsutils/Maybe"; +import { ref, Ref, watch } from "vue"; +import { Filter } from "../../filters"; +import { Model } from "../../Model"; +import { CRUDEvents, StoreChangeEvent } from "../../storage"; +import { ActionType } from "../../utils/ActionsTypes"; +import { + changeState, + IdSwap, + initialState, + ReactiveState +} from "../StateUtils"; +interface UpdateArr { + oldArr: T[]; + newArr: T[]; + primaryKeyName: string; +} + +const updateArr = ({ oldArr, newArr, primaryKeyName }: UpdateArr) => { + const finalArr = [...oldArr]; + const map = new Map( + oldArr.map((el, i) => [(el as Record)[primaryKeyName], i]) + ); + for (const newItem of newArr) { + const newItemKey = (newItem as Record)[primaryKeyName]; + const i = map.get(newItemKey); + if (i != null && i >= 0) { + finalArr.splice(i, 1, newItem); + } else { + finalArr.push(newItem); + } + } + return finalArr; +}; + +const onAdded = ( + state: Ref>, + newData: TItem[], + primaryKeyName: string +) => + updateArr({ + newArr: newData, + oldArr: state.value.data, + primaryKeyName + }); + +const onChanged = ( + state: Ref>, + newData: TItem[], + primaryKeyName: string +) => { + if (state.value.data.length === 0) { + return state.value.data; + } + return updateArr({ + newArr: newData, + oldArr: state.value.data, + primaryKeyName + }); +}; + +const onIdSwapped = ( + state: Ref>, + newData: IdSwap[], + primaryKeyName: string +) => { + if (state.value.data.length === 0) { + return state.value.data; + } + + const changedData = state.value.data.map((d) => { + const dPrimaryKey = (d as Record)[primaryKeyName]; + const index = newData.findIndex( + (newD) => + (newD.previous as Record)[primaryKeyName] === + dPrimaryKey + ); + if (index === -1) { + return d; + } + return newData[index].current; + }); + return changedData; +}; + +const onRemoved = ( + state: Ref>, + removedData: TItem[], + primaryKeyName: string +) => { + if (state.value.data.length === 0) { + return state.value.data; + } + const changedData = state.value.data.filter((d) => { + const dPrimaryKey = (d as Record)[primaryKeyName]; + return removedData.findIndex( + (newD) => + (newD as Record)[primaryKeyName] === dPrimaryKey + ); + }); + return changedData; +}; + +export const updateResult = ( + state: Ref>, + event: StoreChangeEvent, + primaryKeyName: string +) => { + const data = event.data; + switch (event.eventType) { + case CRUDEvents.ADD: + return onAdded(state, data, primaryKeyName); + case CRUDEvents.UPDATE: + return onChanged(state, data, primaryKeyName); + case CRUDEvents.ID_SWAP: + return onIdSwapped(state, data, primaryKeyName); + case CRUDEvents.DELETE: + return onRemoved(state, data, primaryKeyName); + default: + throw new Error(`Invalid event ${event.eventType} received`); + } +}; + +const createSubscribeToUpdates = ( + state: Ref>, + model: Model +) => { + return ( + eventsToWatch?: CRUDEvents[], + customEventHandler?: ( + state: Ref>, + // FIXME: investigate type + data: Maybe[]> + ) => Maybe[]> + ) => { + const subscription = model.subscribe((event) => { + let newData; + + if (customEventHandler) { + newData = customEventHandler(state, event.data); + } + const primaryKeyName = model.getSchema().getPrimaryKey(); + newData = updateResult(state, event, primaryKeyName); + + if (!subscription.closed) { + // Important to check beacuse Componnent could be unmounted + changeState({ + state, + action: { type: ActionType.UPDATE_RESULT, data: newData } + }); + } + }, eventsToWatch); + return subscription; + }; +}; + +interface QueryResults extends UseQuery { + state: Ref>; +} +const queryResults = async ({ + state, + filter, + model +}: QueryResults) => { + if (state.value.loading) { + return; + } + + changeState({ state, action: { type: ActionType.INITIATE_REQUEST } }); + try { + let results; + const filterValue = filter; + if (typeof filterValue === "string") { + results = await model.queryById(filterValue); + } else { + results = await model.query(filterValue); + } + changeState({ + state, + action: { type: ActionType.REQUEST_COMPLETE, data: results } + }); + } catch (error) { + changeState({ + state, + action: { type: ActionType.REQUEST_COMPLETE, error } + }); + } + return state; +}; + +interface UseQuery { + model: Model; + filter: Filter | string | undefined; +} +const subscribeQueryToUpdates = ({ + state, + model +}: { + state: Ref>; + model: Ref>; +}) => { + let subscriptionFn = createSubscribeToUpdates(state, model.value); + watch( + model, + () => { + subscriptionFn = createSubscribeToUpdates(state, model.value); + }, + { deep: true, immediate: true } + ); + return subscriptionFn; +}; + +export const useQuery = (arg: UseQuery) => { + const argRef = ref(arg); + const modelRef = ref(arg.model) as Ref>; + + const state = initialState(); + const subscribeToUpdates = subscribeQueryToUpdates({ + model: modelRef, + state + }); + const runQuery = async () => + await queryResults({ + model: arg.model, + state, + filter: arg.filter + }); + watch(argRef, runQuery, { deep: true, immediate: true }); + return { state: state, subscribeToUpdates }; +}; + +export const useLazyQuery = ({ model }: { model: Model }) => { + const modelRef = ref(model) as Ref>; + + const state = initialState(); + const subscribeToUpdates = subscribeQueryToUpdates({ + model: modelRef, + state + }); + const query = async ({ + filter + }: { + filter: Filter | string | undefined; + }) => + await queryResults({ + model, + filter, + state + }); + return { state: state, query, subscribeToUpdates }; +}; diff --git a/packages/datastore/src/vue/hooks/save.ts b/packages/datastore/src/vue/hooks/save.ts new file mode 100644 index 000000000..0006eb2e8 --- /dev/null +++ b/packages/datastore/src/vue/hooks/save.ts @@ -0,0 +1,32 @@ +import { Model } from "../../Model"; +import { ActionType } from "../../utils/ActionsTypes"; +import { changeState, initialState } from "../StateUtils"; + +export const useSave = (model: Model) => { + const state = initialState(); + + const save = async (input: Omit) => { + if (state.value.loading) {return;} + changeState({ + state, + action: { type: ActionType.INITIATE_REQUEST } + }); + try { + const results = (await model.save( + (input as unknown) as Partial + )) as TModel; + changeState({ + state, + action: { type: ActionType.REQUEST_COMPLETE, data: results } + }); + return results; + } catch (error) { + changeState({ + state, + action: { type: ActionType.REQUEST_COMPLETE, error } + }); + } + }; + + return { state: state, save }; +}; diff --git a/packages/datastore/src/vue/hooks/subscription.ts b/packages/datastore/src/vue/hooks/subscription.ts new file mode 100644 index 000000000..1fe243027 --- /dev/null +++ b/packages/datastore/src/vue/hooks/subscription.ts @@ -0,0 +1,40 @@ +import { Maybe } from "graphql/jsutils/Maybe"; +import { onMounted, onUnmounted, ref, Ref, watch } from "vue"; +import { CRUDEvents } from "../.."; +import { Model } from "../../Model"; +import { ActionType } from "../../utils/ActionsTypes"; +import { Subscription } from "../../utils/PushStream"; +import { changeState, initialState } from "../StateUtils"; + +export const useSubscription = ( + model: Ref>, + eventTypes: CRUDEvents[] +) => { + const state = initialState(); + const subscription = ref>(); + + const subscribe = () => { + subscription.value = model.value.subscribe((event) => { + changeState({ + state, + action: { type: ActionType.REQUEST_COMPLETE, data: event.data } + }); + }, eventTypes); + }; + const unsubscribe = () => { + subscription.value?.unsubscribe(); + }; + + watch( + model, + () => { + unsubscribe(); + subscribe(); + }, + { deep: true, immediate: true } + ); + + onMounted(subscribe); + onUnmounted(unsubscribe); + return { state: state }; +}; diff --git a/packages/datastore/src/vue/hooks/update.ts b/packages/datastore/src/vue/hooks/update.ts new file mode 100644 index 000000000..dc4dbecfd --- /dev/null +++ b/packages/datastore/src/vue/hooks/update.ts @@ -0,0 +1,33 @@ +import { Model } from "../../Model"; +import { ActionType } from "../../utils/ActionsTypes"; +import { changeState, initialState } from "../StateUtils"; + +export const useUpdate = (model: Model) => { + const state = initialState(); + + const update = async (input: TInput, upsert: boolean = false) => { + if (state.value.loading) {return;} + + changeState({ + state, + action: { type: ActionType.INITIATE_REQUEST } + }); + try { + const results = (await (upsert + ? model.saveOrUpdate(input) + : model.updateById(input))) as TModel; + changeState({ + state, + action: { type: ActionType.REQUEST_COMPLETE, data: results } + }); + return results; + } catch (error) { + changeState({ + state, + action: { type: ActionType.REQUEST_COMPLETE, error } + }); + } + }; + + return { state: state, update }; +}; diff --git a/packages/datastore/src/vue/index.ts b/packages/datastore/src/vue/index.ts new file mode 100644 index 000000000..4a1b06a75 --- /dev/null +++ b/packages/datastore/src/vue/index.ts @@ -0,0 +1,4 @@ +export * from "./hooks/delete"; +export * from "./hooks/query"; +export * from "./hooks/save"; +export * from "./hooks/update"; diff --git a/packages/datastore/tests/Vue.test.ts b/packages/datastore/tests/Vue.test.ts new file mode 100644 index 000000000..31f06bc36 --- /dev/null +++ b/packages/datastore/tests/Vue.test.ts @@ -0,0 +1,69 @@ +import { Maybe } from "graphql/jsutils/Maybe"; +import { ref } from "vue"; +import { CRUDEvents, useDatastoreHooks } from "../src"; +import { ReactiveState } from "../src/vue/StateUtils"; + +describe("vue hooks test suite", () => { + const originState: ReactiveState> = { + data: [], + error: null, + loading: false + }; + const stateCopy = (title?: Maybe) => { + const copy = JSON.parse(JSON.stringify(originState)) as ReactiveState< + Record + >; + if (title) {copy.data.push({ title });} + return copy; + }; + + test("it should update result for add", () => { + const state = ref(stateCopy()); + const event = { + eventType: CRUDEvents.ADD, + data: [{ title: "Test" }] + }; + const result = useDatastoreHooks.updateResult(state, event, "title"); + + expect(result).toEqual(event.data); + }); + + test("it should update result for update", () => { + const state = ref(stateCopy("Test")); + const event = { + eventType: CRUDEvents.UPDATE, + data: [{ title: "Test", pass: true }] + }; + const result = useDatastoreHooks.updateResult(state, event, "title"); + + expect(result).toEqual(event.data); + }); + + test("it should update id on ID_SWAP event", () => { + const state = ref(stateCopy("Test")); + const event = { + eventType: CRUDEvents.ID_SWAP, + data: [ + { + previous: { title: "Test", pass: true }, + current: { title: "NewTest", pass: true } + } + ] + }; + const result = useDatastoreHooks.updateResult(state, event, "title"); + + expect(result.length).toEqual(1); + expect(result[0]).toEqual(event.data[0].current); + }); + + test("it should update result for delete", () => { + const state = ref(stateCopy()); + const event = { + eventType: CRUDEvents.DELETE, + data: [{ title: "Test" }] + }; + const result = useDatastoreHooks.updateResult(state, event, "title"); + + expect(result).toEqual([]); + }); +}); diff --git a/packages/datastore/tsconfig.json b/packages/datastore/tsconfig.json index 80ca5539c..7f18c8ad7 100644 --- a/packages/datastore/tsconfig.json +++ b/packages/datastore/tsconfig.json @@ -1,23 +1,13 @@ { "extends": "../../tsconfig.json", - "include": [ - "./src/**/*.ts", "*.d.ts" - ], + "include": ["./src/**/*.ts", "*.d.ts"], "compilerOptions": { "esModuleInterop": true, "sourceMap": false, "outDir": "./dist/", + "skipLibCheck": true, "declarationDir": "./types", - "lib": [ - "esnext.asynciterable", - "es2015", - "dom", - "es2016" - ], - "types": [ - "websql", - "jest", - "node" - ], + "lib": ["esnext.asynciterable", "es2015", "dom", "es2016", "es2020"], + "types": ["websql", "jest", "node"] } }