Skip to content

Commit 36f00cc

Browse files
authored
feat(client): add entity and query functions (#93)
1 parent ff7a181 commit 36f00cc

File tree

5 files changed

+93
-26
lines changed

5 files changed

+93
-26
lines changed

README.md

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -173,17 +173,21 @@ todos
173173

174174
This hook returns the current database client with some helpful functions for syncing data with a backend.
175175

176-
- `client.dbToString()` serializes the whole db including the lookupHelpers to a string
177-
- `client.dbFromString('a serialized db string')` replaces the current db
178-
- `client.dbToDatoms()` returns an array of all the facts aka datoms saved in the db
179-
- datoms are the smallest unit of data in the database, like a key value pair but better
180-
- they are arrays of `[entityId, attribute, value, transactionId, isAddedBoolean]`
181-
- `client.addTransactListener((changedDatoms) => ...)` adds a listener function to all transactions
182-
- use this to save data to your backend
183-
- `client.removeTransactListener()` removes the transaction listener
184-
- please note that only 1 listener can be added per useClient scope
185-
- `client.transactSilently([{item: {name: ...}}])` like `transact()` only it will not trigger any listeners
186-
- use this to sync data from your backend into the client
176+
- `client.dbToString()` serializes the whole db including the lookupHelpers to a string.
177+
- `client.dbFromString('a serialized db string')` replaces the current db.
178+
- `client.dbToDatoms()` returns an array of all the facts aka datoms saved in the db.
179+
- Datoms are the smallest unit of data in the database, like a key value pair but better.
180+
- Datoms are arrays of `[entityId, attribute, value, transactionId, isAddedBoolean]`.
181+
- `client.addTransactListener((changedDatoms) => ...)` adds a listener function to all transactions.
182+
- Use this to save data to your backend.
183+
- `client.removeTransactListener()` removes the transaction listener.
184+
- Please note that only 1 listener can be added per useClient scope.
185+
- `client.transactSilently([{item: {name: ...}}])` like `transact()` only it will not trigger any listeners.
186+
- Use this to sync data from your backend into the client.
187+
- `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.
188+
- 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`.
189+
- `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.
190+
- 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`.
187191

188192
Check out the [Firebase example](https://homebaseio.github.io/homebase-react/#!/example.todo_firebase) for a demonstration of how you might integrate a backend.
189193

docs/0400|API.md

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -129,17 +129,21 @@ todos
129129

130130
This hook returns the current database client with some helpful functions for syncing data with a backend.
131131

132-
- `client.dbToString()` serializes the whole db including the lookupHelpers to a string
133-
- `client.dbFromString('a serialized db string')` replaces the current db
134-
- `client.dbToDatoms()` returns an array of all the facts aka datoms saved in the db
135-
- datoms are the smallest unit of data in the database, like a key value pair but better
136-
- they are arrays of `[entityId, attribute, value, transactionId, isAddedBoolean]`
137-
- `client.addTransactListener((changedDatoms) => ...)` adds a listener function to all transactions
138-
- use this to save data to your backend
139-
- `client.removeTransactListener()` removes the transaction listener
140-
- please note that only 1 listener can be added per useClient scope
141-
- `client.transactSilently([{item: {name: ...}}])` like `transact()` only it will not trigger any listeners
142-
- use this to sync data from your backend into the client
132+
- `client.dbToString()` serializes the whole db including the lookupHelpers to a string.
133+
- `client.dbFromString('a serialized db string')` replaces the current db.
134+
- `client.dbToDatoms()` returns an array of all the facts aka datoms saved in the db.
135+
- Datoms are the smallest unit of data in the database, like a key value pair but better.
136+
- Datoms are arrays of `[entityId, attribute, value, transactionId, isAddedBoolean]`.
137+
- `client.addTransactListener((changedDatoms) => ...)` adds a listener function to all transactions.
138+
- Use this to save data to your backend.
139+
- `client.removeTransactListener()` removes the transaction listener.
140+
- Please note that only 1 listener can be added per useClient scope.
141+
- `client.transactSilently([{item: {name: ...}}])` like `transact()` only it will not trigger any listeners.
142+
- Use this to sync data from your backend into the client.
143+
- `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.
144+
- 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`.
145+
- `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.
146+
- 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`.
143147

144148
Check out the [Firebase example](https://homebaseio.github.io/homebase-react/#!/example.todo_firebase) for a demonstration of how you might integrate a backend.
145149

src/homebase/react.cljs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@
150150
(d/transact! conn [] ::silent))
151151
"dbToDatoms" #(datoms->js (d/datoms @conn :eavt))
152152
;; "dbToJSON" #(clj->js (datoms->json (d/datoms @conn :eavt)))
153+
"entity" (fn [lookup] (js/Promise.resolve (hbjs/entity conn lookup)))
154+
"query" (fn [query & args] (js/Promise.resolve (apply hbjs/q query conn args)))
153155
"transactSilently" (fn [tx] (try-hook "useClient" #(hbjs/transact! conn tx ::silent)))
154156
"addTransactListener" (fn [listener-fn] (d/listen! conn key #(when (not= ::silent (:tx-meta %))
155157
(listener-fn (datoms->js (:tx-data %))))))

src/homebase/react.test.js

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
/* eslint-disable react/button-has-type */
22
import '@testing-library/jest-dom/extend-expect'
3-
import { fireEvent, render, screen } from '@testing-library/react'
3+
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
44
import 'jest-performance-testing'
55
import React from 'react'
6+
import { act } from 'react-dom/test-utils'
67
import { perf, wait } from 'react-performance-testing'
78
import {
89
HomebaseProvider,
@@ -307,18 +308,44 @@ describe('client', () => {
307308
const [order] = useEntity(1)
308309
// TODO: test client.addTransactListener()
309310
// TODO: test client.removeTransactListener()
311+
312+
const [entityResultState, setEntityResultState] = React.useState()
313+
async function runEntity() {
314+
const entityResult = await client.entity(7)
315+
act(() => {
316+
setEntityResultState(entityResult.get('name'))
317+
})
318+
}
319+
const [queryResultState, setQueryResultState] = React.useState()
320+
async function runQuery() {
321+
const queryResult = await client.query({
322+
$find: 'item',
323+
$where: { item: { name: '$any' } },
324+
})
325+
act(() => {
326+
setQueryResultState(queryResult[0].get('name'))
327+
})
328+
}
329+
React.useEffect(() => {
330+
runQuery()
331+
runEntity()
332+
}, [client])
333+
310334
return (
311335
<>
312336
<div data-testid="client.dbToString()">{client.dbToString()}</div>
313337
<div data-testid="client.dbToDatoms()">{JSON.stringify(client.dbToDatoms())}</div>
314338
<button
315339
onClick={() =>
316-
client.transactSilently([{ order: { id: order.get('id'), name: 'order1' } }])}
340+
client.transactSilently([{ order: { id: order.get('id'), name: 'order1' } }])
341+
}
317342
>
318343
update|order.name
319344
</button>
320345
<div data-testid="order.name">{order.get('name')}</div>
321346
<button onClick={() => client.dbFromString(initialDBString)}>client.dbFromString()</button>
347+
<div data-testid="client.entity">{entityResultState}</div>
348+
<div data-testid="client.query">{queryResultState}</div>
322349
</>
323350
)
324351
}
@@ -330,7 +357,7 @@ describe('client', () => {
330357
)
331358

332359
it('useClient', async () => {
333-
expect.assertions(4)
360+
expect.assertions(7)
334361
render(<ClientApp />)
335362
expect(screen.getByTestId('client.dbToString()')).toHaveTextContent(initialDBString)
336363
expect(screen.getByTestId('client.dbToDatoms()')).toHaveTextContent(
@@ -340,6 +367,10 @@ describe('client', () => {
340367
expect(screen.getByTestId('order.name')).toHaveTextContent('order1')
341368
fireEvent.click(screen.getByText('client.dbFromString()'))
342369
expect(screen.getByTestId('order.name')).toBeEmptyDOMElement()
370+
await waitFor(() => {
371+
expect(screen.getByTestId('client.entity')).toHaveTextContent('name lookup')
372+
expect(screen.getByTestId('client.query')).toHaveTextContent('id lookup')
373+
})
343374
})
344375
})
345376

types/index.d.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,33 @@ export type homebaseClient = {
9595
* Transacts data without triggering any listeners. Typically used to sync data from your backend into the client.
9696
* @param transaction - A database transaction.
9797
*/
98-
transactSilently: (transaction: Transaction) => any
98+
transactSilently: (transaction: Transaction) => any,
99+
100+
/**
101+
* Returns a promise that contains a single entity by `lookup`.
102+
* @param lookup - an entity id or lookup object.
103+
* @returns Promise<Entity> - A promise wrapping an entity.
104+
* @example const entity = await client.entity(10)
105+
* @example const entity = await client.entity({ identity: "a unique lookup key" })
106+
* @example
107+
* const project = await client.entity({ project: { name: "a unique name" }})
108+
* project.get('name')
109+
*/
110+
entity: (lookup: object | number) => Promise<Entity>,
111+
112+
/**
113+
* Returns a promise that contains a collection of entities by `query`.
114+
* @param query - a query object or datalog string.
115+
* @param args - optional query arguments.
116+
* @returns Promise<[Entity]> - A promise wrapping an array of entities.
117+
* @example
118+
* const todos = await client.query({
119+
* $find: 'todo',
120+
* $where: { todo: { name: '$any' } }
121+
* })
122+
* todos.map(todo => todo.get('name'))
123+
*/
124+
query: (query: object | string, ...args: any) => Promise<[Entity]>
99125
}
100126

101127
/**

0 commit comments

Comments
 (0)