Skip to content

feat(client): add entity and query functions #93

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 15 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
26 changes: 15 additions & 11 deletions docs/0400|API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 2 additions & 0 deletions src/homebase/react.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -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 %))))))
Expand Down
37 changes: 34 additions & 3 deletions src/homebase/react.test.js
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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 (
<>
<div data-testid="client.dbToString()">{client.dbToString()}</div>
<div data-testid="client.dbToDatoms()">{JSON.stringify(client.dbToDatoms())}</div>
<button
onClick={() =>
client.transactSilently([{ order: { id: order.get('id'), name: 'order1' } }])}
client.transactSilently([{ order: { id: order.get('id'), name: 'order1' } }])
}
>
update|order.name
</button>
<div data-testid="order.name">{order.get('name')}</div>
<button onClick={() => client.dbFromString(initialDBString)}>client.dbFromString()</button>
<div data-testid="client.entity">{entityResultState}</div>
<div data-testid="client.query">{queryResultState}</div>
</>
)
}
Expand All @@ -330,7 +357,7 @@ describe('client', () => {
)

it('useClient', async () => {
expect.assertions(4)
expect.assertions(7)
render(<ClientApp />)
expect(screen.getByTestId('client.dbToString()')).toHaveTextContent(initialDBString)
expect(screen.getByTestId('client.dbToDatoms()')).toHaveTextContent(
Expand All @@ -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')
})
})
})

Expand Down
28 changes: 27 additions & 1 deletion types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Entity> - 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<Entity>,

/**
* 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]>
}

/**
Expand Down