Skip to content

Commit

Permalink
feat(client): add entity and query functions (#93)
Browse files Browse the repository at this point in the history
  • Loading branch information
becomingbabyman committed May 6, 2021
1 parent ff7a181 commit 36f00cc
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 26 deletions.
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

4 comments on commit 36f00cc

@vercel
Copy link

@vercel vercel bot commented on 36f00cc May 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 36f00cc May 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

homebase-example-todo – ./examples/todo

homebase-example-todo.vercel.app
homebase-example-todo-homebaseio.vercel.app
homebase-example-todo-git-master-homebaseio.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 36f00cc May 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 36f00cc May 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.