diff --git a/.github/workflows/gh-branch-pages.yml b/.github/workflows/gh-branch-pages.yml new file mode 100644 index 00000000..1a5de8c5 --- /dev/null +++ b/.github/workflows/gh-branch-pages.yml @@ -0,0 +1,76 @@ +name: Publish Branch Examples + +on: push + +jobs: + publish-examples: + name: Publish Examples + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-node@v1 + with: + node-version: '12' + + - uses: actions/setup-java@v1 + with: + java-version: 1.8 + + - uses: DeLaGuardo/setup-clojure@3.2 + with: + cli: 1.10.1.693 + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn config get cacheFolder)" + + - name: Cache yarn packages + uses: actions/cache@v2 + id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Cache maven packages + uses: actions/cache@v2 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + + - name: Cache shadow-cljs + uses: actions/cache@v2 + with: + path: .shadow-cljs + key: ${{ runner.os }}-shadow-cljs-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-shadow-cljs + + - run: yarn install --frozen-lockfile + + - run: yarn shadow-cljs release dev + + - name: Extract branch name + shell: bash + run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" + id: extract_branch + + - name: Publish to GitHub Pages 🚀 + uses: JamesIves/github-pages-deploy-action@4.1.0 + with: + branch: gh-pages + folder: public + target-folder: branches/${{ steps.extract_branch.outputs.branch }} + + - name: Slack Notification + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_CHANNEL: proj-dev-homebase-react + SLACK_COLOR: ${{ job.status }} # or a specific color like 'green' or '#ff00ff' + SLACK_ICON: https://github.com/homebaseio.png?size=200 + SLACK_MESSAGE: "- :github: Branch: \n- :card_file_box: devcards: https://homebaseio.github.io/homebase-react/branches/${{ steps.extract_branch.outputs.branch }}/index.html" + SLACK_TITLE: "Published ${{ steps.extract_branch.outputs.branch }} to GitHub Pages :rocket:" + SLACK_USERNAME: Homebase + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 22cfd251..173ba356 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -1,6 +1,8 @@ name: Publish Examples -on: push +on: + push: + branches: [ master ] jobs: publish-examples: @@ -52,25 +54,8 @@ jobs: - run: yarn shadow-cljs release dev - - name: Extract branch name - shell: bash - run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" - id: extract_branch - - name: Publish to GitHub Pages 🚀 uses: JamesIves/github-pages-deploy-action@4.1.0 with: branch: gh-pages folder: public - target-folder: branches/${{ steps.extract_branch.outputs.branch }} - - - name: Slack Notification - uses: rtCamp/action-slack-notify@v2 - env: - SLACK_CHANNEL: proj-dev-homebase-react - SLACK_COLOR: ${{ job.status }} # or a specific color like 'green' or '#ff00ff' - SLACK_ICON: https://github.com/homebaseio.png?size=200 - SLACK_MESSAGE: "- :github: Branch: \n- :card_file_box: devcards: https://homebaseio.github.io/homebase-react/branches/${{ steps.extract_branch.outputs.branch }}/index.html" - SLACK_TITLE: "Published ${{ steps.extract_branch.outputs.branch }} to GitHub Pages :rocket:" - SLACK_USERNAME: Homebase - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} diff --git a/README.md b/README.md index 3e4b87dd..af7c03d2 100644 --- a/README.md +++ b/README.md @@ -173,17 +173,21 @@ todos This hook returns the current database client with some helpful functions for syncing data with a backend. -- `client.dbToString()` serializes the whole db including the lookupHelpers to a string -- `client.dbFromString('a serialized db string')` replaces the current db -- `client.dbToDatoms()` returns an array of all the facts aka datoms saved in the db - - datoms are the smallest unit of data in the database, like a key value pair but better - - they are arrays of `[entityId, attribute, value, transactionId, isAddedBoolean]` -- `client.addTransactListener((changedDatoms) => ...)` adds a listener function to all transactions - - use this to save data to your backend -- `client.removeTransactListener()` removes the transaction listener - - please note that only 1 listener can be added per useClient scope -- `client.transactSilently([{item: {name: ...}}])` like `transact()` only it will not trigger any listeners - - use this to sync data from your backend into the client +- `client.dbToString()` serializes the whole db including the lookupHelpers to a string. +- `client.dbFromString('a serialized db string')` replaces the current db. +- `client.dbToDatoms()` returns an array of all the facts aka datoms saved in the db. + - Datoms are the smallest unit of data in the database, like a key value pair but better. + - Datoms are arrays of `[entityId, attribute, value, transactionId, isAddedBoolean]`. +- `client.addTransactListener((changedDatoms) => ...)` adds a listener function to all transactions. + - Use this to save data to your backend. +- `client.removeTransactListener()` removes the transaction listener. + - Please note that only 1 listener can be added per useClient scope. +- `client.transactSilently([{item: {name: ...}}])` like `transact()` only it will not trigger any listeners. + - Use this to sync data from your backend into the client. +- `client.entity(id or { thing: { attr: 'unique value' } })` like `useEntity`, but **returns a promise**. Get an entity in a callback or other places where a React hook does not make sense. + - The entity returned by this function **will NOT live update the parent React component** when its data changes. If you want reactive updates we recommend using `useEntity`. +- `client.query({ $find: 'thing', $where: { thing: { name: '$any' } } })` like `useQuery`, but **returns a promise**. Perform a query in a callback or other places where a React hook does not make sense. + - The entities returned by this function **will NOT live update the parent React component** when their data changes. If you want reactive updates we recommend using `useQuery`. Check out the [Firebase example](https://homebaseio.github.io/homebase-react/#!/example.todo_firebase) for a demonstration of how you might integrate a backend. diff --git a/docs/0400|API.md b/docs/0400|API.md index 868dd994..abdc9dea 100644 --- a/docs/0400|API.md +++ b/docs/0400|API.md @@ -129,17 +129,21 @@ todos This hook returns the current database client with some helpful functions for syncing data with a backend. -- `client.dbToString()` serializes the whole db including the lookupHelpers to a string -- `client.dbFromString('a serialized db string')` replaces the current db -- `client.dbToDatoms()` returns an array of all the facts aka datoms saved in the db - - datoms are the smallest unit of data in the database, like a key value pair but better - - they are arrays of `[entityId, attribute, value, transactionId, isAddedBoolean]` -- `client.addTransactListener((changedDatoms) => ...)` adds a listener function to all transactions - - use this to save data to your backend -- `client.removeTransactListener()` removes the transaction listener - - please note that only 1 listener can be added per useClient scope -- `client.transactSilently([{item: {name: ...}}])` like `transact()` only it will not trigger any listeners - - use this to sync data from your backend into the client +- `client.dbToString()` serializes the whole db including the lookupHelpers to a string. +- `client.dbFromString('a serialized db string')` replaces the current db. +- `client.dbToDatoms()` returns an array of all the facts aka datoms saved in the db. + - Datoms are the smallest unit of data in the database, like a key value pair but better. + - Datoms are arrays of `[entityId, attribute, value, transactionId, isAddedBoolean]`. +- `client.addTransactListener((changedDatoms) => ...)` adds a listener function to all transactions. + - Use this to save data to your backend. +- `client.removeTransactListener()` removes the transaction listener. + - Please note that only 1 listener can be added per useClient scope. +- `client.transactSilently([{item: {name: ...}}])` like `transact()` only it will not trigger any listeners. + - Use this to sync data from your backend into the client. +- `client.entity(id or { thing: { attr: 'unique value' } })` like `useEntity`, but **returns a promise**. Get an entity in a callback or other places where a React hook does not make sense. + - The entity returned by this function **will NOT live update the parent React component** when its data changes. If you want reactive updates we recommend using `useEntity`. +- `client.query({ $find: 'thing', $where: { thing: { name: '$any' } } })` like `useQuery`, but **returns a promise**. Perform a query in a callback or other places where a React hook does not make sense. + - The entities returned by this function **will NOT live update the parent React component** when their data changes. If you want reactive updates we recommend using `useQuery`. Check out the [Firebase example](https://homebaseio.github.io/homebase-react/#!/example.todo_firebase) for a demonstration of how you might integrate a backend. diff --git a/examples/counter/package.json b/examples/counter/package.json index 7d4d0a9a..8727f493 100644 --- a/examples/counter/package.json +++ b/examples/counter/package.json @@ -6,7 +6,7 @@ "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", - "homebase-react": "^0.5.3", + "homebase-react": "^0.5.4", "react": "^17.0.1", "react-dom": "^17.0.1", "react-scripts": "4.0.0", diff --git a/examples/counter/yarn.lock b/examples/counter/yarn.lock index 4939b49a..55260a26 100644 --- a/examples/counter/yarn.lock +++ b/examples/counter/yarn.lock @@ -5344,10 +5344,10 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -homebase-react@^0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/homebase-react/-/homebase-react-0.5.3.tgz#bffcce2d7f5b1d33391623dbd754f5e22df3920a" - integrity sha512-3Uya6yD6N57skVbrpSb1ia8eRZAnDJNvqyxMbvqaFtUyJGFqyfqWN9Qiz5h17WSnkoeC/YMs1ejFRrNPLx0jIw== +homebase-react@^0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/homebase-react/-/homebase-react-0.5.4.tgz#b6c31ebb2c8852503c752ed81a010b104b9cd751" + integrity sha512-7TRm3ofQVRjc94PuVpT8UtFTD42L3ULC5o7fPrnY/7pYOBupU2pgGmV6upYmTf/WWK8T5wlcDtKl6OnBhbJnfw== hoopy@^0.1.4: version "0.1.4" diff --git a/examples/roam/package.json b/examples/roam/package.json index 4bfd1c9f..59739bf8 100644 --- a/examples/roam/package.json +++ b/examples/roam/package.json @@ -14,7 +14,7 @@ "autoprefixer": "^9", "firebase": "^8.2.6", "firebaseui": "^4.7.3", - "homebase-react": "^0.5.3", + "homebase-react": "^0.5.4", "lodash": "^4.17.20", "nanoid": "^3.1.20", "postcss": "^7", diff --git a/examples/roam/yarn.lock b/examples/roam/yarn.lock index e99aa260..da1aa81d 100644 --- a/examples/roam/yarn.lock +++ b/examples/roam/yarn.lock @@ -6298,10 +6298,10 @@ hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0: dependencies: react-is "^16.7.0" -homebase-react@^0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/homebase-react/-/homebase-react-0.5.3.tgz#bffcce2d7f5b1d33391623dbd754f5e22df3920a" - integrity sha512-3Uya6yD6N57skVbrpSb1ia8eRZAnDJNvqyxMbvqaFtUyJGFqyfqWN9Qiz5h17WSnkoeC/YMs1ejFRrNPLx0jIw== +homebase-react@^0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/homebase-react/-/homebase-react-0.5.4.tgz#b6c31ebb2c8852503c752ed81a010b104b9cd751" + integrity sha512-7TRm3ofQVRjc94PuVpT8UtFTD42L3ULC5o7fPrnY/7pYOBupU2pgGmV6upYmTf/WWK8T5wlcDtKl6OnBhbJnfw== hoopy@^0.1.4: version "0.1.4" diff --git a/examples/todo/package.json b/examples/todo/package.json index e86d49dc..0f8eb4d0 100644 --- a/examples/todo/package.json +++ b/examples/todo/package.json @@ -6,7 +6,7 @@ "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", - "homebase-react": "^0.5.3", + "homebase-react": "^0.5.4", "react": "^17.0.1", "react-dom": "^17.0.1", "react-scripts": "4.0.0", diff --git a/examples/todo/yarn.lock b/examples/todo/yarn.lock index 1cabc195..735c11c7 100644 --- a/examples/todo/yarn.lock +++ b/examples/todo/yarn.lock @@ -5348,10 +5348,10 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -homebase-react@^0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/homebase-react/-/homebase-react-0.5.3.tgz#bffcce2d7f5b1d33391623dbd754f5e22df3920a" - integrity sha512-3Uya6yD6N57skVbrpSb1ia8eRZAnDJNvqyxMbvqaFtUyJGFqyfqWN9Qiz5h17WSnkoeC/YMs1ejFRrNPLx0jIw== +homebase-react@^0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/homebase-react/-/homebase-react-0.5.4.tgz#b6c31ebb2c8852503c752ed81a010b104b9cd751" + integrity sha512-7TRm3ofQVRjc94PuVpT8UtFTD42L3ULC5o7fPrnY/7pYOBupU2pgGmV6upYmTf/WWK8T5wlcDtKl6OnBhbJnfw== hoopy@^0.1.4: version "0.1.4" @@ -11389,9 +11389,9 @@ xtend@^4.0.0, xtend@~4.0.1: integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== y18n@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz" - integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + version "4.0.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4" + integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ== yallist@^3.0.2: version "3.1.1" diff --git a/examples/typescript-firebase-todo/package.json b/examples/typescript-firebase-todo/package.json index e56ab422..3d4902c9 100644 --- a/examples/typescript-firebase-todo/package.json +++ b/examples/typescript-firebase-todo/package.json @@ -12,7 +12,7 @@ "@types/react-dom": "^16.9.8", "firebase": "^8.1.1", "firebaseui": "^4.7.1", - "homebase-react": "^0.5.3", + "homebase-react": "^0.5.4", "react": "^17.0.1", "react-dom": "^17.0.1", "react-scripts": "4.0.0", diff --git a/examples/typescript-firebase-todo/yarn.lock b/examples/typescript-firebase-todo/yarn.lock index 5e119c7d..57da630c 100644 --- a/examples/typescript-firebase-todo/yarn.lock +++ b/examples/typescript-firebase-todo/yarn.lock @@ -5821,10 +5821,10 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -homebase-react@^0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/homebase-react/-/homebase-react-0.5.3.tgz#bffcce2d7f5b1d33391623dbd754f5e22df3920a" - integrity sha512-3Uya6yD6N57skVbrpSb1ia8eRZAnDJNvqyxMbvqaFtUyJGFqyfqWN9Qiz5h17WSnkoeC/YMs1ejFRrNPLx0jIw== +homebase-react@^0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/homebase-react/-/homebase-react-0.5.4.tgz#b6c31ebb2c8852503c752ed81a010b104b9cd751" + integrity sha512-7TRm3ofQVRjc94PuVpT8UtFTD42L3ULC5o7fPrnY/7pYOBupU2pgGmV6upYmTf/WWK8T5wlcDtKl6OnBhbJnfw== hoopy@^0.1.4: version "0.1.4" diff --git a/src/homebase/react.cljs b/src/homebase/react.cljs index d5863f21..6f407727 100644 --- a/src/homebase/react.cljs +++ b/src/homebase/react.cljs @@ -150,6 +150,8 @@ (d/transact! conn [] ::silent)) "dbToDatoms" #(datoms->js (d/datoms @conn :eavt)) ;; "dbToJSON" #(clj->js (datoms->json (d/datoms @conn :eavt))) + "entity" (fn [lookup] (js/Promise.resolve (hbjs/entity conn lookup))) + "query" (fn [query & args] (js/Promise.resolve (apply hbjs/q query conn args))) "transactSilently" (fn [tx] (try-hook "useClient" #(hbjs/transact! conn tx ::silent))) "addTransactListener" (fn [listener-fn] (d/listen! conn key #(when (not= ::silent (:tx-meta %)) (listener-fn (datoms->js (:tx-data %)))))) diff --git a/src/homebase/react.test.js b/src/homebase/react.test.js index ae84c39c..11d14e3e 100644 --- a/src/homebase/react.test.js +++ b/src/homebase/react.test.js @@ -1,8 +1,9 @@ /* eslint-disable react/button-has-type */ import '@testing-library/jest-dom/extend-expect' -import { fireEvent, render, screen } from '@testing-library/react' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' import 'jest-performance-testing' import React from 'react' +import { act } from 'react-dom/test-utils' import { perf, wait } from 'react-performance-testing' import { HomebaseProvider, @@ -307,18 +308,44 @@ describe('client', () => { const [order] = useEntity(1) // TODO: test client.addTransactListener() // TODO: test client.removeTransactListener() + + const [entityResultState, setEntityResultState] = React.useState() + async function runEntity() { + const entityResult = await client.entity(7) + act(() => { + setEntityResultState(entityResult.get('name')) + }) + } + const [queryResultState, setQueryResultState] = React.useState() + async function runQuery() { + const queryResult = await client.query({ + $find: 'item', + $where: { item: { name: '$any' } }, + }) + act(() => { + setQueryResultState(queryResult[0].get('name')) + }) + } + React.useEffect(() => { + runQuery() + runEntity() + }, [client]) + return ( <>
{client.dbToString()}
{JSON.stringify(client.dbToDatoms())}
{order.get('name')}
+
{entityResultState}
+
{queryResultState}
) } @@ -330,7 +357,7 @@ describe('client', () => { ) it('useClient', async () => { - expect.assertions(4) + expect.assertions(7) render() expect(screen.getByTestId('client.dbToString()')).toHaveTextContent(initialDBString) expect(screen.getByTestId('client.dbToDatoms()')).toHaveTextContent( @@ -340,6 +367,10 @@ describe('client', () => { expect(screen.getByTestId('order.name')).toHaveTextContent('order1') fireEvent.click(screen.getByText('client.dbFromString()')) expect(screen.getByTestId('order.name')).toBeEmptyDOMElement() + await waitFor(() => { + expect(screen.getByTestId('client.entity')).toHaveTextContent('name lookup') + expect(screen.getByTestId('client.query')).toHaveTextContent('id lookup') + }) }) }) diff --git a/types/index.d.ts b/types/index.d.ts index 3d5fc332..0489c0b0 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -95,7 +95,33 @@ export type homebaseClient = { * Transacts data without triggering any listeners. Typically used to sync data from your backend into the client. * @param transaction - A database transaction. */ - transactSilently: (transaction: Transaction) => any + transactSilently: (transaction: Transaction) => any, + + /** + * Returns a promise that contains a single entity by `lookup`. + * @param lookup - an entity id or lookup object. + * @returns Promise - A promise wrapping an entity. + * @example const entity = await client.entity(10) + * @example const entity = await client.entity({ identity: "a unique lookup key" }) + * @example + * const project = await client.entity({ project: { name: "a unique name" }}) + * project.get('name') + */ + entity: (lookup: object | number) => Promise, + + /** + * Returns a promise that contains a collection of entities by `query`. + * @param query - a query object or datalog string. + * @param args - optional query arguments. + * @returns Promise<[Entity]> - A promise wrapping an array of entities. + * @example + * const todos = await client.query({ + * $find: 'todo', + * $where: { todo: { name: '$any' } } + * }) + * todos.map(todo => todo.get('name')) + */ + query: (query: object | string, ...args: any) => Promise<[Entity]> } /**