@livestore/livestore 0.0.47-dev.0 → 0.0.48-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/README.md +3 -1
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/MainDatabaseWrapper.d.ts +2 -2
  4. package/dist/MainDatabaseWrapper.d.ts.map +1 -1
  5. package/dist/MainDatabaseWrapper.js.map +1 -1
  6. package/dist/__tests__/react/fixture.d.ts +262 -158
  7. package/dist/__tests__/react/fixture.d.ts.map +1 -1
  8. package/dist/__tests__/react/fixture.js +14 -12
  9. package/dist/__tests__/react/fixture.js.map +1 -1
  10. package/dist/__tests__/react/utils/otel.d.ts +1 -1
  11. package/dist/__tests__/react/utils/otel.d.ts.map +1 -1
  12. package/dist/effect/LiveStore.d.ts +5 -5
  13. package/dist/effect/LiveStore.d.ts.map +1 -1
  14. package/dist/effect/LiveStore.js +5 -4
  15. package/dist/effect/LiveStore.js.map +1 -1
  16. package/dist/index.d.ts +3 -4
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +0 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/react/LiveStoreProvider.d.ts +4 -4
  21. package/dist/react/LiveStoreProvider.d.ts.map +1 -1
  22. package/dist/react/LiveStoreProvider.js +8 -8
  23. package/dist/react/LiveStoreProvider.js.map +1 -1
  24. package/dist/react/LiveStoreProvider.test.js +3 -4
  25. package/dist/react/LiveStoreProvider.test.js.map +1 -1
  26. package/dist/react/index.d.ts +1 -0
  27. package/dist/react/index.d.ts.map +1 -1
  28. package/dist/react/index.js +1 -0
  29. package/dist/react/index.js.map +1 -1
  30. package/dist/react/useAtom.d.ts +5 -2
  31. package/dist/react/useAtom.d.ts.map +1 -1
  32. package/dist/react/useAtom.js +20 -4
  33. package/dist/react/useAtom.js.map +1 -1
  34. package/dist/react/useLocalId.d.ts +10 -0
  35. package/dist/react/useLocalId.d.ts.map +1 -0
  36. package/dist/react/useLocalId.js +15 -0
  37. package/dist/react/useLocalId.js.map +1 -0
  38. package/dist/react/useQuery.test.js +7 -7
  39. package/dist/react/useQuery.test.js.map +1 -1
  40. package/dist/react/useRow.d.ts +3 -1
  41. package/dist/react/useRow.d.ts.map +1 -1
  42. package/dist/react/useRow.js +25 -8
  43. package/dist/react/useRow.js.map +1 -1
  44. package/dist/react/useRow.test.js +58 -29
  45. package/dist/react/useRow.test.js.map +1 -1
  46. package/dist/react/useTemporaryQuery.d.ts +1 -1
  47. package/dist/react/useTemporaryQuery.d.ts.map +1 -1
  48. package/dist/react/useTemporaryQuery.js.map +1 -1
  49. package/dist/react/useTemporaryQuery.test.js +3 -3
  50. package/dist/react/useTemporaryQuery.test.js.map +1 -1
  51. package/dist/react/utils/useStateRefWithReactiveInput.d.ts.map +1 -1
  52. package/dist/reactive.test.js +1 -1
  53. package/dist/reactive.test.js.map +1 -1
  54. package/dist/reactiveQueries/base-class.d.ts +1 -1
  55. package/dist/reactiveQueries/base-class.d.ts.map +1 -1
  56. package/dist/reactiveQueries/base-class.js.map +1 -1
  57. package/dist/reactiveQueries/graphql.d.ts +4 -4
  58. package/dist/reactiveQueries/graphql.d.ts.map +1 -1
  59. package/dist/reactiveQueries/graphql.js +2 -1
  60. package/dist/reactiveQueries/graphql.js.map +1 -1
  61. package/dist/reactiveQueries/js.d.ts +1 -1
  62. package/dist/reactiveQueries/js.d.ts.map +1 -1
  63. package/dist/reactiveQueries/js.js.map +1 -1
  64. package/dist/reactiveQueries/sql.d.ts +1 -1
  65. package/dist/reactiveQueries/sql.d.ts.map +1 -1
  66. package/dist/reactiveQueries/sql.js +2 -2
  67. package/dist/reactiveQueries/sql.js.map +1 -1
  68. package/dist/row-query.d.ts +3 -3
  69. package/dist/row-query.d.ts.map +1 -1
  70. package/dist/row-query.js +47 -34
  71. package/dist/row-query.js.map +1 -1
  72. package/dist/store.d.ts +20 -19
  73. package/dist/store.d.ts.map +1 -1
  74. package/dist/store.js +67 -41
  75. package/dist/store.js.map +1 -1
  76. package/dist/utils/bounded-collections.d.ts +1 -1
  77. package/dist/utils/bounded-collections.d.ts.map +1 -1
  78. package/dist/utils/util.d.ts +0 -1
  79. package/dist/utils/util.d.ts.map +1 -1
  80. package/dist/utils/util.js.map +1 -1
  81. package/package.json +16 -16
  82. package/src/MainDatabaseWrapper.ts +3 -3
  83. package/src/__tests__/react/fixture.tsx +32 -18
  84. package/src/effect/LiveStore.ts +9 -8
  85. package/src/index.ts +7 -5
  86. package/src/react/LiveStoreProvider.test.tsx +5 -5
  87. package/src/react/LiveStoreProvider.tsx +11 -11
  88. package/src/react/index.ts +1 -0
  89. package/src/react/useAtom.ts +31 -5
  90. package/src/react/useLocalId.ts +25 -0
  91. package/src/react/useQuery.test.tsx +8 -8
  92. package/src/react/useRow.test.tsx +60 -35
  93. package/src/react/useRow.ts +43 -12
  94. package/src/react/useTemporaryQuery.test.tsx +4 -4
  95. package/src/react/useTemporaryQuery.ts +1 -1
  96. package/src/reactive.test.ts +1 -1
  97. package/src/reactiveQueries/base-class.ts +1 -1
  98. package/src/reactiveQueries/graphql.ts +3 -2
  99. package/src/reactiveQueries/js.ts +1 -1
  100. package/src/reactiveQueries/sql.ts +3 -3
  101. package/src/row-query.ts +67 -55
  102. package/src/store.ts +89 -59
  103. package/src/utils/util.ts +0 -2
  104. package/dist/cud.d.ts +0 -28
  105. package/dist/cud.d.ts.map +0 -1
  106. package/dist/cud.js +0 -47
  107. package/dist/cud.js.map +0 -1
  108. package/dist/cud.test.d.ts +0 -2
  109. package/dist/cud.test.d.ts.map +0 -1
  110. package/dist/cud.test.js +0 -47
  111. package/dist/cud.test.js.map +0 -1
  112. package/dist/migrations.d.ts +0 -16
  113. package/dist/migrations.d.ts.map +0 -1
  114. package/dist/migrations.js +0 -98
  115. package/dist/migrations.js.map +0 -1
  116. package/dist/query-info.d.ts +0 -48
  117. package/dist/query-info.d.ts.map +0 -1
  118. package/dist/query-info.js +0 -39
  119. package/dist/query-info.js.map +0 -1
  120. package/src/cud.test.ts +0 -52
  121. package/src/cud.ts +0 -88
  122. package/src/migrations.ts +0 -151
  123. package/src/query-info.ts +0 -104
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { Store, createStore } from './store.js'
2
- export type { BaseGraphQLContext, QueryDebugInfo, RefreshReason, BootDb } from './store.js'
2
+ export type { BaseGraphQLContext, QueryDebugInfo, RefreshReason } from './store.js'
3
3
 
4
4
  export type { QueryDefinition, LiveStoreCreateStoreOptions, LiveStoreContext } from './effect/LiveStore.js'
5
5
 
@@ -26,14 +26,16 @@ export { globalDbGraph, dynamicallyRegisteredTables } from './global-state.js'
26
26
 
27
27
  export { type RowResult, type RowResultEncoded, rowQuery, deriveColQuery } from './row-query.js'
28
28
 
29
- export * from './cud.js'
30
-
31
29
  export * from '@livestore/common/schema'
32
- export { sql } from '@livestore/common'
30
+ export { sql, type BootDb, type InMemoryDatabase } from '@livestore/common'
33
31
 
34
32
  export { SqliteAst, SqliteDsl } from 'effect-db-schema'
35
33
 
36
34
  export { prepareBindValues, type Bindable, type PreparedBindValues } from './utils/util.js'
37
35
  export { isEqual } from 'lodash-es'
38
36
 
39
- export type { DatabaseImpl, DatabaseFactory, PreparedStatement } from '@livestore/common'
37
+ export type {
38
+ StoreAdapter as DatabaseImpl,
39
+ StoreAdapterFactory as DatabaseFactory,
40
+ PreparedStatement,
41
+ } from '@livestore/common'
@@ -1,13 +1,13 @@
1
+ import type { BootDb } from '@livestore/common'
1
2
  import { sql } from '@livestore/common'
2
- import { makeDb } from '@livestore/web'
3
- import { InMemoryStorage } from '@livestore/web/storage/in-memory'
3
+ import { makeInMemoryAdapter } from '@livestore/web'
4
4
  import { render, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react'
5
5
  import React from 'react'
6
6
  import { describe, expect, it } from 'vitest'
7
7
 
8
8
  import { parseTodos, schema } from '../__tests__/react/fixture.js'
9
9
  import { querySQL } from '../reactiveQueries/sql.js'
10
- import type { BootDb, Store } from '../store.js'
10
+ import type { Store } from '../store.js'
11
11
  import * as LiveStoreReact from './index.js'
12
12
  import { LiveStoreProvider } from './LiveStoreProvider.js'
13
13
 
@@ -35,9 +35,9 @@ describe('LiveStoreProvider', () => {
35
35
  [],
36
36
  )
37
37
  // eslint-disable-next-line react-hooks/exhaustive-deps
38
- const makeDbMemo = React.useMemo(() => makeDb(() => InMemoryStorage.load()), [forceUpdate])
38
+ const adapterMemo = React.useMemo(() => makeInMemoryAdapter(), [forceUpdate])
39
39
  return (
40
- <LiveStoreProvider schema={schema} fallback={<div>Loading LiveStore</div>} makeDb={makeDbMemo} boot={bootCb}>
40
+ <LiveStoreProvider schema={schema} fallback={<div>Loading LiveStore</div>} adapter={adapterMemo} boot={bootCb}>
41
41
  <App />
42
42
  </LiveStoreProvider>
43
43
  )
@@ -1,4 +1,4 @@
1
- import type { DatabaseFactory } from '@livestore/common'
1
+ import type { BootDb, StoreAdapterFactory } from '@livestore/common'
2
2
  import type { LiveStoreSchema } from '@livestore/common/schema'
3
3
  import { shouldNeverHappen } from '@livestore/utils'
4
4
  import type * as otel from '@opentelemetry/api'
@@ -7,7 +7,7 @@ import React from 'react'
7
7
 
8
8
  // TODO refactor so the `react` module doesn't depend on `effect` module
9
9
  import type { LiveStoreContext as StoreContext_, LiveStoreCreateStoreOptions } from '../effect/LiveStore.js'
10
- import type { BaseGraphQLContext, BootDb, GraphQLOptions, Store } from '../store.js'
10
+ import type { BaseGraphQLContext, GraphQLOptions, Store } from '../store.js'
11
11
  import { createStore } from '../store.js'
12
12
  import { LiveStoreContext } from './LiveStoreContext.js'
13
13
 
@@ -18,7 +18,7 @@ interface LiveStoreProviderProps<GraphQLContext> {
18
18
  otelTracer?: otel.Tracer
19
19
  otelRootSpanContext?: otel.Context
20
20
  fallback: ReactElement
21
- makeDb: DatabaseFactory
21
+ adapter: StoreAdapterFactory
22
22
  batchUpdates?: (run: () => void) => void
23
23
  }
24
24
 
@@ -30,7 +30,7 @@ export const LiveStoreProvider = <GraphQLContext extends BaseGraphQLContext>({
30
30
  children,
31
31
  schema,
32
32
  boot,
33
- makeDb,
33
+ adapter,
34
34
  batchUpdates,
35
35
  }: LiveStoreProviderProps<GraphQLContext> & { children?: ReactNode }): JSX.Element => {
36
36
  const storeCtx = useCreateStore({
@@ -39,7 +39,7 @@ export const LiveStoreProvider = <GraphQLContext extends BaseGraphQLContext>({
39
39
  otelTracer,
40
40
  otelRootSpanContext,
41
41
  boot,
42
- makeDb,
42
+ adapter,
43
43
  batchUpdates,
44
44
  })
45
45
 
@@ -58,7 +58,7 @@ const useCreateStore = <GraphQLContext extends BaseGraphQLContext>({
58
58
  otelTracer,
59
59
  otelRootSpanContext,
60
60
  boot,
61
- makeDb,
61
+ adapter,
62
62
  batchUpdates,
63
63
  }: LiveStoreCreateStoreOptions<GraphQLContext>) => {
64
64
  const [_, rerender] = React.useState(0)
@@ -69,7 +69,7 @@ const useCreateStore = <GraphQLContext extends BaseGraphQLContext>({
69
69
  otelTracer,
70
70
  otelRootSpanContext,
71
71
  boot,
72
- makeDb,
72
+ adapter,
73
73
  batchUpdates,
74
74
  })
75
75
  const oldStoreAlreadyDestroyedRef = React.useRef(false)
@@ -80,7 +80,7 @@ const useCreateStore = <GraphQLContext extends BaseGraphQLContext>({
80
80
  inputPropsCacheRef.current.otelTracer !== otelTracer ||
81
81
  inputPropsCacheRef.current.otelRootSpanContext !== otelRootSpanContext ||
82
82
  inputPropsCacheRef.current.boot !== boot ||
83
- inputPropsCacheRef.current.makeDb !== makeDb ||
83
+ inputPropsCacheRef.current.adapter !== adapter ||
84
84
  inputPropsCacheRef.current.batchUpdates !== batchUpdates
85
85
  ) {
86
86
  inputPropsCacheRef.current = {
@@ -89,7 +89,7 @@ const useCreateStore = <GraphQLContext extends BaseGraphQLContext>({
89
89
  otelTracer,
90
90
  otelRootSpanContext,
91
91
  boot,
92
- makeDb,
92
+ adapter,
93
93
  batchUpdates,
94
94
  }
95
95
  ctxValueRef.current?.store.destroy()
@@ -108,7 +108,7 @@ const useCreateStore = <GraphQLContext extends BaseGraphQLContext>({
108
108
  otelTracer,
109
109
  otelRootSpanContext,
110
110
  boot,
111
- makeDb,
111
+ adapter,
112
112
  batchUpdates,
113
113
  })
114
114
  ctxValueRef.current = { store }
@@ -124,7 +124,7 @@ const useCreateStore = <GraphQLContext extends BaseGraphQLContext>({
124
124
  store?.destroy()
125
125
  }
126
126
  }
127
- }, [schema, graphQLOptions, otelTracer, otelRootSpanContext, boot, makeDb, batchUpdates])
127
+ }, [schema, graphQLOptions, otelTracer, otelRootSpanContext, boot, adapter, batchUpdates])
128
128
 
129
129
  return ctxValueRef.current
130
130
  }
@@ -11,6 +11,7 @@ export {
11
11
  type UseRowResult as UseStateResult,
12
12
  } from './useRow.js'
13
13
  export { useAtom } from './useAtom.js'
14
+ export { useLocalId, getLocalId } from './useLocalId.js'
14
15
 
15
16
  export { LiveList, type LiveListProps } from './components/LiveList.js'
16
17
 
@@ -1,26 +1,52 @@
1
+ import { type QueryInfoCol, type QueryInfoRow } from '@livestore/common'
2
+ import type { DbSchema } from '@livestore/common/schema'
1
3
  import React from 'react'
2
4
 
3
- import { mutationForQueryInfo, type QueryInfoCol, type QueryInfoRow } from '../query-info.js'
4
5
  import type { LiveQuery } from '../reactiveQueries/base-class.js'
5
6
  import { useStore } from './LiveStoreContext.js'
6
7
  import { useQueryRef } from './useQuery.js'
7
8
  import type { Dispatch, SetStateAction } from './useRow.js'
8
9
 
9
- export const useAtom = <TQuery extends LiveQuery<any, QueryInfoRow<any> | QueryInfoCol<any, any>>>(
10
+ export const useAtom = <
11
+ TQuery extends LiveQuery<any, QueryInfoRow<TTableDef> | QueryInfoCol<TTableDef, any>>,
12
+ TTableDef extends DbSchema.TableDef<
13
+ DbSchema.DefaultSqliteTableDefConstrained,
14
+ boolean,
15
+ DbSchema.TableOptions & { deriveMutations: true }
16
+ >,
17
+ >(
10
18
  query$: TQuery,
11
19
  ): [value: TQuery['__result!'], setValue: Dispatch<SetStateAction<Partial<TQuery['__result!']>>>] => {
12
20
  const query$Ref = useQueryRef(query$)
13
21
 
14
22
  const { store } = useStore()
15
23
 
24
+ // TODO make API equivalent to useRow
16
25
  const setValue = React.useMemo<Dispatch<SetStateAction<TQuery['__result!']>>>(() => {
17
26
  return (newValueOrFn: any) => {
18
27
  const newValue = typeof newValueOrFn === 'function' ? newValueOrFn(query$Ref.current) : newValueOrFn
19
28
 
20
- if (query$.queryInfo._tag === 'Row' && query$.queryInfo.table.isSingleColumn) {
21
- store.mutate(mutationForQueryInfo(query$.queryInfo!, { value: newValue }))
29
+ if (query$.queryInfo._tag === 'Row') {
30
+ if (query$.queryInfo.table.options.isSingleton && query$.queryInfo.table.isSingleColumn) {
31
+ store.mutate(query$.queryInfo.table.update(newValue))
32
+ } else if (query$.queryInfo.table.options.isSingleColumn) {
33
+ store.mutate(
34
+ query$.queryInfo.table.update({ where: { id: query$.queryInfo.id }, values: { value: newValue } }),
35
+ )
36
+ } else {
37
+ store.mutate(query$.queryInfo.table.update({ where: { id: query$.queryInfo.id }, values: newValue }))
38
+ }
22
39
  } else {
23
- store.mutate(mutationForQueryInfo(query$.queryInfo!, newValue))
40
+ if (query$.queryInfo.table.options.isSingleton && query$.queryInfo.table.isSingleColumn) {
41
+ store.mutate(query$.queryInfo.table.update({ [query$.queryInfo.column]: newValue }))
42
+ } else {
43
+ store.mutate(
44
+ query$.queryInfo.table.update({
45
+ where: { id: query$.queryInfo.id },
46
+ values: { [query$.queryInfo.column]: newValue },
47
+ }),
48
+ )
49
+ }
24
50
  }
25
51
  }
26
52
  }, [query$.queryInfo, query$Ref, store])
@@ -0,0 +1,25 @@
1
+ import { cuid } from '@livestore/utils/cuid'
2
+ import React from 'react'
3
+
4
+ type LocalIdOptions = {
5
+ key: string
6
+ storageType: 'session' | 'local'
7
+ storageKeyPrefix: string
8
+ makeId: () => string
9
+ }
10
+
11
+ export const useLocalId = (opts?: Partial<LocalIdOptions>) => React.useMemo(() => getLocalId(opts), [opts])
12
+
13
+ export const getLocalId = (opts?: Partial<LocalIdOptions>) => {
14
+ const { key = '', storageType = 'session', storageKeyPrefix = 'livestore:localid:', makeId = cuid } = opts ?? {}
15
+
16
+ const storage = storageType === 'session' ? window.sessionStorage : window.localStorage
17
+ const fullKey = `${storageKeyPrefix}:${key}`
18
+ const storedKey = storage.getItem(fullKey)
19
+
20
+ if (storedKey) return storedKey
21
+
22
+ const newKey = makeId()
23
+ storage.setItem(fullKey, newKey)
24
+ return newKey
25
+ }
@@ -1,14 +1,14 @@
1
- import { act, renderHook } from '@testing-library/react'
1
+ import { renderHook } from '@testing-library/react'
2
2
  import React from 'react'
3
3
  import { describe, expect, it } from 'vitest'
4
4
 
5
- import { makeTodoMvc, parseTodos } from '../__tests__/react/fixture.js'
5
+ import { makeTodoMvc, parseTodos, todos } from '../__tests__/react/fixture.js'
6
6
  import { querySQL } from '../reactiveQueries/sql.js'
7
7
  import * as LiveStoreReact from './index.js'
8
8
 
9
9
  describe('useQuery', () => {
10
10
  it('simple', async () => {
11
- const { wrapper, store, cud, makeRenderCount } = await makeTodoMvc()
11
+ const { wrapper, store, makeRenderCount } = await makeTodoMvc()
12
12
 
13
13
  const renderCount = makeRenderCount()
14
14
 
@@ -26,7 +26,7 @@ describe('useQuery', () => {
26
26
  expect(result.current.length).toBe(0)
27
27
  expect(renderCount.val).toBe(1)
28
28
 
29
- act(() => store.mutate(cud.todos.insert({ id: 't1', text: 'buy milk', completed: false })))
29
+ React.act(() => store.mutate(todos.insert({ id: 't1', text: 'buy milk', completed: false })))
30
30
 
31
31
  expect(result.current.length).toBe(1)
32
32
  expect(result.current[0]!.text).toBe('buy milk')
@@ -34,7 +34,7 @@ describe('useQuery', () => {
34
34
  })
35
35
 
36
36
  it('same `useQuery` hook invoked with different queries', async () => {
37
- const { wrapper, store, cud, makeRenderCount } = await makeTodoMvc()
37
+ const { wrapper, store, makeRenderCount } = await makeTodoMvc()
38
38
 
39
39
  const renderCount = makeRenderCount()
40
40
 
@@ -42,8 +42,8 @@ describe('useQuery', () => {
42
42
  const todo2$ = querySQL(`select * from todos where id = 't2'`, { label: 'libraryTracksView2', map: parseTodos })
43
43
 
44
44
  store.mutate(
45
- cud.todos.insert({ id: 't1', text: 'buy milk', completed: false }),
46
- cud.todos.insert({ id: 't2', text: 'buy eggs', completed: false }),
45
+ todos.insert({ id: 't1', text: 'buy milk', completed: false }),
46
+ todos.insert({ id: 't2', text: 'buy eggs', completed: false }),
47
47
  )
48
48
 
49
49
  const { result, rerender } = renderHook(
@@ -60,7 +60,7 @@ describe('useQuery', () => {
60
60
  expect(result.current).toBe('buy milk')
61
61
  expect(renderCount.val).toBe(1)
62
62
 
63
- act(() => store.mutate(cud.todos.update({ where: { id: 't1' }, values: { text: 'buy soy milk' } })))
63
+ React.act(() => store.mutate(todos.update({ where: { id: 't1' }, values: { text: 'buy soy milk' } })))
64
64
 
65
65
  expect(result.current).toBe('buy soy milk')
66
66
  expect(renderCount.val).toBe(2)
@@ -1,7 +1,7 @@
1
1
  import { ReadonlyRecord } from '@livestore/utils/effect'
2
2
  import * as otel from '@opentelemetry/api'
3
3
  import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
4
- import { act, render, renderHook } from '@testing-library/react'
4
+ import { render, renderHook } from '@testing-library/react'
5
5
  import React from 'react'
6
6
  import { describe, expect, it } from 'vitest'
7
7
 
@@ -9,7 +9,6 @@ import type { Todo } from '../__tests__/react/fixture.js'
9
9
  import { makeTodoMvc, todos } from '../__tests__/react/fixture.js'
10
10
  import { getSimplifiedRootSpan } from '../__tests__/react/utils/otel.js'
11
11
  import * as LiveStore from '../index.js'
12
- import { mutationForQueryInfo } from '../query-info.js'
13
12
  import * as LiveStoreReact from './index.js'
14
13
  import type { StackInfo } from './utils/stack-info.js'
15
14
 
@@ -36,7 +35,7 @@ describe.concurrent('useRow', () => {
36
35
  expect(result.current.state.username).toBe('')
37
36
  expect(renderCount.val).toBe(1)
38
37
 
39
- act(() =>
38
+ React.act(() =>
40
39
  store.mutate(
41
40
  LiveStore.rawSqlMutation({
42
41
  sql: LiveStore.sql`INSERT INTO UserInfo (id, username) VALUES ('u2', 'username_u2')`,
@@ -51,6 +50,8 @@ describe.concurrent('useRow', () => {
51
50
  expect(renderCount.val).toBe(2)
52
51
  })
53
52
 
53
+ // TODO add a test that makes sure React doesn't re-render when a setter is used to set the same value
54
+
54
55
  it('should update the data reactively - via setState', async () => {
55
56
  using inputs = await makeTodoMvc({ useGlobalDbGraph: false })
56
57
 
@@ -72,7 +73,7 @@ describe.concurrent('useRow', () => {
72
73
  expect(result.current.state.username).toBe('')
73
74
  expect(renderCount.val).toBe(1)
74
75
 
75
- act(() => result.current.setState.username('username_u1_hello'))
76
+ React.act(() => result.current.setState.username('username_u1_hello'))
76
77
 
77
78
  expect(result.current.state.id).toBe('u1')
78
79
  expect(result.current.state.username).toBe('username_u1_hello')
@@ -100,7 +101,7 @@ describe.concurrent('useRow', () => {
100
101
  expect(result.current.state.username).toBe('')
101
102
  expect(renderCount.val).toBe(1)
102
103
 
103
- act(() =>
104
+ React.act(() =>
104
105
  store.mutate(
105
106
  LiveStore.rawSqlMutation({
106
107
  sql: LiveStore.sql`UPDATE UserInfo SET username = 'username_u1_hello' WHERE id = 'u1';`,
@@ -115,18 +116,10 @@ describe.concurrent('useRow', () => {
115
116
 
116
117
  it('should work for a larger app', async () => {
117
118
  using inputs = await makeTodoMvc({ useGlobalDbGraph: false })
118
- const { wrapper, store, dbGraph, makeRenderCount } = inputs
119
+ const { wrapper, store, dbGraph, makeRenderCount, AppRouterSchema } = inputs
119
120
 
120
121
  const allTodos$ = LiveStore.querySQL<Todo[]>(`select * from todos`, { label: 'allTodos', dbGraph })
121
122
 
122
- const AppRouterSchema = LiveStore.DbSchema.table(
123
- 'AppRouter',
124
- {
125
- currentTaskId: LiveStore.DbSchema.text({ default: null, nullable: true }),
126
- },
127
- { isSingleton: true },
128
- )
129
-
130
123
  const appRouterRenderCount = makeRenderCount()
131
124
  let globalSetState: LiveStoreReact.StateSetters<typeof AppRouterSchema> | undefined
132
125
  const AppRouter: React.FC = () => {
@@ -168,7 +161,7 @@ describe.concurrent('useRow', () => {
168
161
 
169
162
  expect(appRouterRenderCount.val).toBe(1)
170
163
 
171
- act(() =>
164
+ React.act(() =>
172
165
  store.mutate(
173
166
  LiveStore.rawSqlMutation({
174
167
  sql: LiveStore.sql`INSERT INTO todos (id, text, completed) VALUES ('t1', 'buy milk', 0)`,
@@ -179,7 +172,7 @@ describe.concurrent('useRow', () => {
179
172
  expect(appRouterRenderCount.val).toBe(1)
180
173
  expect(renderResult.getByRole('current-id').innerHTML).toMatchInlineSnapshot('"Current Task Id: -"')
181
174
 
182
- act(() => globalSetState!.currentTaskId('t1'))
175
+ React.act(() => globalSetState!.currentTaskId('t1'))
183
176
 
184
177
  expect(appRouterRenderCount.val).toBe(2)
185
178
  expect(renderResult.getByRole('content').innerHTML).toMatchInlineSnapshot(
@@ -188,12 +181,12 @@ describe.concurrent('useRow', () => {
188
181
 
189
182
  expect(renderResult.getByRole('current-id').innerHTML).toMatchInlineSnapshot('"Current Task Id: t1"')
190
183
 
191
- act(() =>
184
+ React.act(() =>
192
185
  store.mutate(
193
186
  LiveStore.rawSqlMutation({
194
187
  sql: LiveStore.sql`INSERT INTO todos (id, text, completed) VALUES ('t2', 'buy eggs', 0)`,
195
188
  }),
196
- mutationForQueryInfo({ _tag: 'Col', table: AppRouterSchema, column: 'currentTaskId', id: 'singleton' }, 't2'),
189
+ AppRouterSchema.update({ where: { id: 'singleton' }, values: { currentTaskId: 't2' } }),
197
190
  LiveStore.rawSqlMutation({
198
191
  sql: LiveStore.sql`INSERT INTO todos (id, text, completed) VALUES ('t3', 'buy bread', 0)`,
199
192
  }),
@@ -206,12 +199,12 @@ describe.concurrent('useRow', () => {
206
199
 
207
200
  it('should work for a useRow query chained with a useTemporary query', async () => {
208
201
  using inputs = await makeTodoMvc({ useGlobalDbGraph: false })
209
- const { store, wrapper, AppComponentSchema, dbGraph, makeRenderCount, cud } = inputs
202
+ const { store, wrapper, AppComponentSchema, dbGraph, makeRenderCount } = inputs
210
203
  const renderCount = makeRenderCount()
211
204
 
212
205
  store.mutate(
213
- cud.todos.insert({ id: 't1', text: 'buy milk', completed: false }),
214
- cud.todos.insert({ id: 't2', text: 'buy bread', completed: false }),
206
+ todos.insert({ id: 't1', text: 'buy milk', completed: false }),
207
+ todos.insert({ id: 't2', text: 'buy bread', completed: false }),
215
208
  )
216
209
 
217
210
  const { result, unmount, rerender } = renderHook(
@@ -233,7 +226,7 @@ describe.concurrent('useRow', () => {
233
226
  { wrapper, initialProps: 'u1' },
234
227
  )
235
228
 
236
- act(() =>
229
+ React.act(() =>
237
230
  store.mutate(
238
231
  LiveStore.rawSqlMutation({
239
232
  sql: LiveStore.sql`INSERT INTO UserInfo (id, username, text) VALUES ('u2', 'username_u2', 'milk')`,
@@ -289,7 +282,7 @@ describe.concurrent('useRow', () => {
289
282
  expect(result.current.state.username).toBe('')
290
283
  expect(renderCount.val).toBe(1)
291
284
 
292
- act(() =>
285
+ React.act(() =>
293
286
  store.mutate(
294
287
  LiveStore.rawSqlMutation({
295
288
  sql: LiveStore.sql`INSERT INTO UserInfo (id, username) VALUES ('u2', 'username_u2')`,
@@ -346,10 +339,18 @@ describe.concurrent('useRow', () => {
346
339
  "_name": "sql-in-memory-select",
347
340
  "attributes": {
348
341
  "sql.cached": false,
349
- "sql.query": "SELECT schemaHash FROM __livestore_schema WHERE tableName = 'UserInfo'",
342
+ "sql.query": "select 1 from UserInfo where id = 'u1'",
350
343
  "sql.rowsCount": 0,
351
344
  },
352
345
  },
346
+ {
347
+ "_name": "sql-in-memory-select",
348
+ "attributes": {
349
+ "sql.cached": false,
350
+ "sql.query": "select 1 from UserInfo where id = 'u2'",
351
+ "sql.rowsCount": 1,
352
+ },
353
+ },
353
354
  {
354
355
  "_name": "LiveStore:mutations",
355
356
  "children": [
@@ -415,10 +416,21 @@ describe.concurrent('useRow', () => {
415
416
  },
416
417
  "children": [
417
418
  {
418
- "_name": "livestore.in-memory-db:execute",
419
+ "_name": "LiveStore:mutatetWithoutRefresh",
419
420
  "attributes": {
420
- "sql.query": "insert into UserInfo (username, text, id) select $username, $text, $id where not exists(select 1 from UserInfo where id = 'u1')",
421
+ "livestore.args": "{
422
+ "id": "u1"
423
+ }",
424
+ "livestore.mutation": "_Derived_Create_UserInfo",
421
425
  },
426
+ "children": [
427
+ {
428
+ "_name": "livestore.in-memory-db:execute",
429
+ "attributes": {
430
+ "sql.query": "INSERT INTO UserInfo (username, text, id) VALUES ($username, $text, $id)",
431
+ },
432
+ },
433
+ ],
422
434
  },
423
435
  {
424
436
  "_name": "LiveStore:useQuery:sql(rowQuery:query:UserInfo:u1)",
@@ -490,10 +502,18 @@ describe.concurrent('useRow', () => {
490
502
  "_name": "sql-in-memory-select",
491
503
  "attributes": {
492
504
  "sql.cached": false,
493
- "sql.query": "SELECT schemaHash FROM __livestore_schema WHERE tableName = 'UserInfo'",
505
+ "sql.query": "select 1 from UserInfo where id = 'u1'",
494
506
  "sql.rowsCount": 0,
495
507
  },
496
508
  },
509
+ {
510
+ "_name": "sql-in-memory-select",
511
+ "attributes": {
512
+ "sql.cached": false,
513
+ "sql.query": "select 1 from UserInfo where id = 'u2'",
514
+ "sql.rowsCount": 1,
515
+ },
516
+ },
497
517
  {
498
518
  "_name": "LiveStore:mutations",
499
519
  "children": [
@@ -559,10 +579,21 @@ describe.concurrent('useRow', () => {
559
579
  },
560
580
  "children": [
561
581
  {
562
- "_name": "livestore.in-memory-db:execute",
582
+ "_name": "LiveStore:mutatetWithoutRefresh",
563
583
  "attributes": {
564
- "sql.query": "insert into UserInfo (username, text, id) select $username, $text, $id where not exists(select 1 from UserInfo where id = 'u1')",
584
+ "livestore.args": "{
585
+ "id": "u1"
586
+ }",
587
+ "livestore.mutation": "_Derived_Create_UserInfo",
565
588
  },
589
+ "children": [
590
+ {
591
+ "_name": "livestore.in-memory-db:execute",
592
+ "attributes": {
593
+ "sql.query": "INSERT INTO UserInfo (username, text, id) VALUES ($username, $text, $id)",
594
+ },
595
+ },
596
+ ],
566
597
  },
567
598
  {
568
599
  "_name": "LiveStore:useQuery:sql(rowQuery:query:UserInfo:u1)",
@@ -605,12 +636,6 @@ describe.concurrent('useRow', () => {
605
636
  "id": "u2",
606
637
  },
607
638
  "children": [
608
- {
609
- "_name": "livestore.in-memory-db:execute",
610
- "attributes": {
611
- "sql.query": "insert into UserInfo (username, text, id) select $username, $text, $id where not exists(select 1 from UserInfo where id = 'u2')",
612
- },
613
- },
614
639
  {
615
640
  "_name": "LiveStore:useQuery:sql(rowQuery:query:UserInfo:u2)",
616
641
  "attributes": {
@@ -1,11 +1,14 @@
1
+ import type { QueryInfo } from '@livestore/common'
2
+ import {} from // deriveCreateMutationDef as deriveCreateMutationDef_,
3
+ // updateMutationForQueryInfo as updateMutationForQueryInfo_,
4
+ '@livestore/common'
1
5
  import { DbSchema } from '@livestore/common/schema'
6
+ import { shouldNeverHappen } from '@livestore/utils'
2
7
  import type { SqliteDsl } from 'effect-db-schema'
3
8
  import { mapValues } from 'lodash-es'
4
9
  import React from 'react'
5
10
 
6
11
  import type { DbGraph, LiveQuery } from '../index.js'
7
- import type { QueryInfo } from '../query-info.js'
8
- import { mutationForQueryInfo } from '../query-info.js'
9
12
  import type { RowResult } from '../row-query.js'
10
13
  import { rowQuery } from '../row-query.js'
11
14
  import { useStore } from './LiveStoreContext.js'
@@ -40,7 +43,7 @@ export const useRow: {
40
43
  TTableDef extends DbSchema.TableDef<
41
44
  DbSchema.DefaultSqliteTableDef,
42
45
  boolean,
43
- DbSchema.TableOptions & { isSingleton: true }
46
+ DbSchema.TableOptions & { isSingleton: true; deriveMutations: true }
44
47
  >,
45
48
  >(
46
49
  table: TTableDef,
@@ -50,7 +53,7 @@ export const useRow: {
50
53
  TTableDef extends DbSchema.TableDef<
51
54
  DbSchema.DefaultSqliteTableDef,
52
55
  boolean,
53
- DbSchema.TableOptions & { isSingleton: false }
56
+ DbSchema.TableOptions & { isSingleton: false; deriveMutations: true }
54
57
  >,
55
58
  >(
56
59
  table: TTableDef,
@@ -58,7 +61,13 @@ export const useRow: {
58
61
  id: string,
59
62
  options?: UseRowOptionsBase & UseRowOptionsDefaulValues<TTableDef>,
60
63
  ): UseRowResult<TTableDef>
61
- } = <TTableDef extends DbSchema.TableDef>(
64
+ } = <
65
+ TTableDef extends DbSchema.TableDef<
66
+ DbSchema.DefaultSqliteTableDefConstrained,
67
+ boolean,
68
+ DbSchema.TableOptions & { deriveMutations: true }
69
+ >,
70
+ >(
62
71
  table: TTableDef,
63
72
  idOrOptions?: string | UseRowOptionsBase,
64
73
  options_?: UseRowOptionsBase & UseRowOptionsDefaulValues<TTableDef>,
@@ -68,11 +77,25 @@ export const useRow: {
68
77
  const options: (UseRowOptionsBase & UseRowOptionsDefaulValues<TTableDef>) | undefined =
69
78
  typeof idOrOptions === 'string' ? options_ : idOrOptions
70
79
  const { defaultValues, dbGraph } = options ?? {}
80
+
71
81
  type TComponentState = SqliteDsl.FromColumns.RowDecoded<TTableDef['sqliteDef']['columns']>
72
82
 
83
+ const tableName = table.sqliteDef.name
84
+
85
+ if (DbSchema.tableHasDerivedMutations(table) === false) {
86
+ shouldNeverHappen(`useRow called on table "${tableName}" which does not have 'deriveMutations: true' set`)
87
+ }
88
+
73
89
  const { store } = useStore()
74
90
 
75
- // console.debug('useRow', table.sqliteDef.name, id)
91
+ if (
92
+ store.schema.tables.has(table.sqliteDef.name) === false &&
93
+ table.sqliteDef.name.startsWith('__livestore') === false
94
+ ) {
95
+ shouldNeverHappen(`Table "${table.sqliteDef.name}" not found in schema`)
96
+ }
97
+
98
+ // console.debug('useRow', tableName, id)
76
99
 
77
100
  const { query$, otelContext } = useMakeTemporaryQuery(
78
101
  (otelContext) =>
@@ -83,10 +106,10 @@ export const useRow: {
83
106
  defaultValues: defaultValues!,
84
107
  dbGraph,
85
108
  }) as any as LiveQuery<RowResult<TTableDef>, QueryInfo>),
86
- [id!, table.sqliteDef.name],
109
+ [id!, tableName],
87
110
  {
88
111
  otel: {
89
- spanName: `LiveStore:useRow:${table.sqliteDef.name}${id === undefined ? '' : `:${id}`}`,
112
+ spanName: `LiveStore:useRow:${tableName}${id === undefined ? '' : `:${id}`}`,
90
113
  attributes: { id },
91
114
  },
92
115
  },
@@ -100,7 +123,13 @@ export const useRow: {
100
123
  const newValue = typeof newValueOrFn === 'function' ? newValueOrFn(query$Ref.current) : newValueOrFn
101
124
  if (query$Ref.current === newValue) return
102
125
 
103
- store.mutate(mutationForQueryInfo(query$.queryInfo!, { value: newValue }))
126
+ // NOTE we need to account for the short-hand syntax for single-column+singleton tables
127
+ if (table.options.isSingleton) {
128
+ store.mutate(table.update(newValue))
129
+ } else {
130
+ store.mutate(table.update({ where: { id }, values: { value: newValue } }))
131
+ }
132
+ // store.mutate(updateMutationForQueryInfo(query$.queryInfo!, { value: newValue }))
104
133
  }
105
134
  } else {
106
135
  const setState = // TODO: do we have a better type for the values that can go in SQLite?
@@ -113,7 +142,8 @@ export const useRow: {
113
142
  // @ts-expect-error TODO fix typing
114
143
  if (query$Ref.current[columnName] === newValue) return
115
144
 
116
- store.mutate(mutationForQueryInfo(query$.queryInfo!, { [columnName]: newValue }))
145
+ store.mutate(table.update({ where: { id: id ?? 'singleton' }, values: { [columnName]: newValue } }))
146
+ // store.mutate(updateMutationForQueryInfo(query$.queryInfo!, { [columnName]: newValue }))
117
147
  })
118
148
 
119
149
  setState.setMany = (columnValuesOrFn: Partial<TComponentState>) => {
@@ -130,12 +160,13 @@ export const useRow: {
130
160
  return
131
161
  }
132
162
 
133
- store.mutate(mutationForQueryInfo(query$.queryInfo!, columnValues))
163
+ store.mutate(table.update({ where: { id: id ?? 'singleton' }, values: columnValues }))
164
+ // store.mutate(updateMutationForQueryInfo(query$.queryInfo!, columnValues))
134
165
  }
135
166
 
136
167
  return setState as any
137
168
  }
138
- }, [query$.queryInfo, query$Ref, sqliteTableDef.columns, store, table.isSingleColumn])
169
+ }, [id, query$Ref, sqliteTableDef.columns, store, table])
139
170
 
140
171
  return [query$Ref.current, setState, query$]
141
172
  }