@livestore/livestore 0.0.55-dev.3 → 0.0.56-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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/react/fixture.d.ts +0 -5
- package/dist/__tests__/react/fixture.d.ts.map +1 -1
- package/dist/__tests__/react/fixture.js +1 -2
- package/dist/__tests__/react/fixture.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/react/LiveStoreProvider.d.ts +4 -2
- package/dist/react/LiveStoreProvider.d.ts.map +1 -1
- package/dist/react/LiveStoreProvider.js +20 -5
- package/dist/react/LiveStoreProvider.js.map +1 -1
- package/dist/react/LiveStoreProvider.test.js +3 -2
- package/dist/react/LiveStoreProvider.test.js.map +1 -1
- package/dist/react/useQuery.test.js +11 -4
- package/dist/react/useQuery.test.js.map +1 -1
- package/dist/react/useRow.test.js +13 -5
- package/dist/react/useRow.test.js.map +1 -1
- package/dist/react/useTemporaryQuery.test.js +5 -2
- package/dist/react/useTemporaryQuery.test.js.map +1 -1
- package/dist/reactiveQueries/graphql.d.ts.map +1 -1
- package/dist/reactiveQueries/graphql.js.map +1 -1
- package/dist/reactiveQueries/sql.d.ts +9 -12
- package/dist/reactiveQueries/sql.d.ts.map +1 -1
- package/dist/reactiveQueries/sql.js +39 -69
- package/dist/reactiveQueries/sql.js.map +1 -1
- package/dist/reactiveQueries/sql.test.js +9 -5
- package/dist/reactiveQueries/sql.test.js.map +1 -1
- package/dist/row-query.d.ts +6 -4
- package/dist/row-query.d.ts.map +1 -1
- package/dist/row-query.js +5 -12
- package/dist/row-query.js.map +1 -1
- package/dist/store-devtools.d.ts +5 -4
- package/dist/store-devtools.d.ts.map +1 -1
- package/dist/store-devtools.js +4 -9
- package/dist/store-devtools.js.map +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +2 -2
- package/dist/store.js.map +1 -1
- package/package.json +5 -9
- package/src/__tests__/react/fixture.tsx +1 -3
- package/src/index.ts +1 -1
- package/src/react/LiveStoreProvider.test.tsx +3 -2
- package/src/react/LiveStoreProvider.tsx +28 -7
- package/src/react/useQuery.test.tsx +11 -4
- package/src/react/useRow.test.tsx +13 -9
- package/src/react/useTemporaryQuery.test.tsx +5 -2
- package/src/reactiveQueries/graphql.ts +5 -1
- package/src/reactiveQueries/sql.test.ts +9 -5
- package/src/reactiveQueries/sql.ts +61 -86
- package/src/row-query.ts +17 -21
- package/src/store-devtools.ts +11 -10
- package/src/store.ts +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livestore/livestore",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.56-dev.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@graphql-typed-document-node/core": "^3.2.0",
|
|
32
32
|
"@opentelemetry/api": "^1.9.0",
|
|
33
|
-
"@livestore/
|
|
34
|
-
"
|
|
35
|
-
"
|
|
33
|
+
"@livestore/utils": "0.0.56-dev.0",
|
|
34
|
+
"effect-db-schema": "0.0.56-dev.0",
|
|
35
|
+
"@livestore/common": "0.0.56-dev.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@opentelemetry/sdk-trace-base": "1.25.1",
|
|
@@ -46,18 +46,14 @@
|
|
|
46
46
|
"typescript": "5.5.4",
|
|
47
47
|
"vite": "5.3.5",
|
|
48
48
|
"vitest": "^2.0.4",
|
|
49
|
-
"@livestore/web": "0.0.
|
|
49
|
+
"@livestore/web": "0.0.56-dev.0"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"@tauri-apps/api": "^1.4.0",
|
|
53
52
|
"graphql": "16.x",
|
|
54
53
|
"react": "^18",
|
|
55
54
|
"react-dom": "^18"
|
|
56
55
|
},
|
|
57
56
|
"peerDependenciesMeta": {
|
|
58
|
-
"@tauri-apps/api": {
|
|
59
|
-
"optional": true
|
|
60
|
-
},
|
|
61
57
|
"graphql": {
|
|
62
58
|
"optional": true
|
|
63
59
|
}
|
|
@@ -5,7 +5,7 @@ import React from 'react'
|
|
|
5
5
|
|
|
6
6
|
import { globalReactivityGraph } from '../../global-state.js'
|
|
7
7
|
import type { LiveStoreContext } from '../../index.js'
|
|
8
|
-
import { createStorePromise, DbSchema, makeReactivityGraph, makeSchema,
|
|
8
|
+
import { createStorePromise, DbSchema, makeReactivityGraph, makeSchema, sql } from '../../index.js'
|
|
9
9
|
import * as LiveStoreReact from '../../react/index.js'
|
|
10
10
|
|
|
11
11
|
export type Todo = {
|
|
@@ -57,8 +57,6 @@ const AppRouterSchema = DbSchema.table(
|
|
|
57
57
|
export const tables = { todos, app, userInfo, AppRouterSchema }
|
|
58
58
|
export const schema = makeSchema({ tables })
|
|
59
59
|
|
|
60
|
-
export const parseTodos = ParseUtils.many(todos)
|
|
61
|
-
|
|
62
60
|
export const makeTodoMvc = async ({
|
|
63
61
|
otelTracer,
|
|
64
62
|
otelContext,
|
package/src/index.ts
CHANGED
|
@@ -22,7 +22,7 @@ export type {
|
|
|
22
22
|
Effect,
|
|
23
23
|
} from './reactive.js'
|
|
24
24
|
export { LiveStoreJSQuery, computed } from './reactiveQueries/js.js'
|
|
25
|
-
export { LiveStoreSQLQuery, querySQL
|
|
25
|
+
export { LiveStoreSQLQuery, querySQL } from './reactiveQueries/sql.js'
|
|
26
26
|
export { LiveStoreGraphQLQuery, queryGraphQL } from './reactiveQueries/graphql.js'
|
|
27
27
|
export {
|
|
28
28
|
type GetAtomResult,
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { BootDb } from '@livestore/common'
|
|
2
2
|
import { sql } from '@livestore/common'
|
|
3
|
+
import { Schema } from '@livestore/utils/effect'
|
|
3
4
|
import { makeInMemoryAdapter } from '@livestore/web'
|
|
4
5
|
import { render, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react'
|
|
5
6
|
import React from 'react'
|
|
6
7
|
import { describe, expect, it } from 'vitest'
|
|
7
8
|
|
|
8
|
-
import {
|
|
9
|
+
import { schema, tables } from '../__tests__/react/fixture.js'
|
|
9
10
|
import { querySQL } from '../reactiveQueries/sql.js'
|
|
10
11
|
import * as LiveStoreReact from './index.js'
|
|
11
12
|
import { LiveStoreProvider } from './LiveStoreProvider.js'
|
|
@@ -14,7 +15,7 @@ describe('LiveStoreProvider', () => {
|
|
|
14
15
|
it('simple', async () => {
|
|
15
16
|
let appRenderCount = 0
|
|
16
17
|
|
|
17
|
-
const allTodos$ = querySQL(`select * from todos`, {
|
|
18
|
+
const allTodos$ = querySQL(`select * from todos`, { schema: Schema.Array(tables.todos.schema) })
|
|
18
19
|
|
|
19
20
|
const App = () => {
|
|
20
21
|
appRenderCount++
|
|
@@ -21,14 +21,23 @@ interface LiveStoreProviderProps<GraphQLContext> {
|
|
|
21
21
|
graphQLOptions?: GraphQLOptions<GraphQLContext>
|
|
22
22
|
otelOptions?: OtelOptions
|
|
23
23
|
renderLoading: (status: BootStatus) => ReactElement
|
|
24
|
+
renderError?: (error: UnexpectedError | unknown) => ReactElement
|
|
25
|
+
renderShutdown?: () => ReactElement
|
|
24
26
|
adapter: StoreAdapterFactory
|
|
25
27
|
batchUpdates?: (run: () => void) => void
|
|
26
28
|
disableDevtools?: boolean
|
|
27
29
|
signal?: AbortSignal
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
const defaultRenderError = (error: UnexpectedError | unknown) => (
|
|
33
|
+
<>{Schema.is(UnexpectedError)(error) ? error.toString() : errorToString(error)}</>
|
|
34
|
+
)
|
|
35
|
+
const defaultRenderShutdown = () => <>LiveStore Shutdown</>
|
|
36
|
+
|
|
30
37
|
export const LiveStoreProvider = <GraphQLContext extends BaseGraphQLContext>({
|
|
31
38
|
renderLoading,
|
|
39
|
+
renderError = defaultRenderError,
|
|
40
|
+
renderShutdown = defaultRenderShutdown,
|
|
32
41
|
graphQLOptions,
|
|
33
42
|
otelOptions,
|
|
34
43
|
children,
|
|
@@ -51,19 +60,15 @@ export const LiveStoreProvider = <GraphQLContext extends BaseGraphQLContext>({
|
|
|
51
60
|
})
|
|
52
61
|
|
|
53
62
|
if (storeCtx.stage === 'error') {
|
|
54
|
-
return (
|
|
55
|
-
<div>
|
|
56
|
-
{Schema.is(UnexpectedError)(storeCtx.error) ? storeCtx.error.toString() : errorToString(storeCtx.error)}
|
|
57
|
-
</div>
|
|
58
|
-
)
|
|
63
|
+
return renderError(storeCtx.error)
|
|
59
64
|
}
|
|
60
65
|
|
|
61
66
|
if (storeCtx.stage === 'shutdown') {
|
|
62
|
-
return
|
|
67
|
+
return renderShutdown()
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
if (storeCtx.stage !== 'running') {
|
|
66
|
-
return
|
|
71
|
+
return renderLoading(storeCtx)
|
|
67
72
|
}
|
|
68
73
|
|
|
69
74
|
window.__debugLiveStore = storeCtx.store
|
|
@@ -71,6 +76,18 @@ export const LiveStoreProvider = <GraphQLContext extends BaseGraphQLContext>({
|
|
|
71
76
|
return <LiveStoreContext.Provider value={storeCtx}>{children}</LiveStoreContext.Provider>
|
|
72
77
|
}
|
|
73
78
|
|
|
79
|
+
type SchemaKey = string
|
|
80
|
+
const semaphoreMap = new Map<SchemaKey, Effect.Semaphore>()
|
|
81
|
+
|
|
82
|
+
const withSemaphore = (schemaKey: SchemaKey) => {
|
|
83
|
+
let semaphore = semaphoreMap.get(schemaKey)
|
|
84
|
+
if (!semaphore) {
|
|
85
|
+
semaphore = Effect.makeSemaphore(1).pipe(Effect.runSync)
|
|
86
|
+
semaphoreMap.set(schemaKey, semaphore)
|
|
87
|
+
}
|
|
88
|
+
return semaphore.withPermits(1)
|
|
89
|
+
}
|
|
90
|
+
|
|
74
91
|
const useCreateStore = <GraphQLContext extends BaseGraphQLContext>({
|
|
75
92
|
schema,
|
|
76
93
|
graphQLOptions,
|
|
@@ -195,6 +212,10 @@ const useCreateStore = <GraphQLContext extends BaseGraphQLContext>({
|
|
|
195
212
|
)
|
|
196
213
|
}).pipe(
|
|
197
214
|
Effect.scoped,
|
|
215
|
+
// NOTE we're running the code above in a semaphore to make sure a previous store is always fully
|
|
216
|
+
// shutdown before a new one is created - especially when shutdown logic is async. You can't trust `React.useEffect`.
|
|
217
|
+
// Thank you to Mattia Manzati for this idea.
|
|
218
|
+
withSemaphore(schema.key),
|
|
198
219
|
Effect.tapCauseLogPretty,
|
|
199
220
|
Effect.annotateLogs({ thread: 'window' }),
|
|
200
221
|
Effect.provide(Logger.pretty),
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { Schema } from '@livestore/utils/effect'
|
|
1
2
|
import { renderHook } from '@testing-library/react'
|
|
2
3
|
import React from 'react'
|
|
3
4
|
import { describe, expect, it } from 'vitest'
|
|
4
5
|
|
|
5
|
-
import { makeTodoMvc,
|
|
6
|
+
import { makeTodoMvc, tables, todos } from '../__tests__/react/fixture.js'
|
|
6
7
|
import { querySQL } from '../reactiveQueries/sql.js'
|
|
7
8
|
import * as LiveStoreReact from './index.js'
|
|
8
9
|
|
|
@@ -12,7 +13,7 @@ describe('useQuery', () => {
|
|
|
12
13
|
|
|
13
14
|
const renderCount = makeRenderCount()
|
|
14
15
|
|
|
15
|
-
const allTodos$ = querySQL(`select * from todos`, {
|
|
16
|
+
const allTodos$ = querySQL(`select * from todos`, { schema: Schema.Array(tables.todos.schema) })
|
|
16
17
|
|
|
17
18
|
const { result } = renderHook(
|
|
18
19
|
() => {
|
|
@@ -38,8 +39,14 @@ describe('useQuery', () => {
|
|
|
38
39
|
|
|
39
40
|
const renderCount = makeRenderCount()
|
|
40
41
|
|
|
41
|
-
const todo1$ = querySQL(`select * from todos where id = 't1'`, {
|
|
42
|
-
|
|
42
|
+
const todo1$ = querySQL(`select * from todos where id = 't1'`, {
|
|
43
|
+
label: 'libraryTracksView1',
|
|
44
|
+
schema: Schema.Array(tables.todos.schema),
|
|
45
|
+
})
|
|
46
|
+
const todo2$ = querySQL(`select * from todos where id = 't2'`, {
|
|
47
|
+
label: 'libraryTracksView2',
|
|
48
|
+
schema: Schema.Array(tables.todos.schema),
|
|
49
|
+
})
|
|
43
50
|
|
|
44
51
|
store.mutate(
|
|
45
52
|
todos.insert({ id: 't1', text: 'buy milk', completed: false }),
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { ReadonlyRecord } from '@livestore/utils/effect'
|
|
1
|
+
import { ReadonlyRecord, Schema } from '@livestore/utils/effect'
|
|
2
2
|
import * as otel from '@opentelemetry/api'
|
|
3
3
|
import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
|
|
4
4
|
import { render, renderHook } from '@testing-library/react'
|
|
5
5
|
import React from 'react'
|
|
6
6
|
import { describe, expect, it } from 'vitest'
|
|
7
7
|
|
|
8
|
-
import
|
|
9
|
-
import { makeTodoMvc, todos } from '../__tests__/react/fixture.js'
|
|
8
|
+
import { makeTodoMvc, tables, todos } from '../__tests__/react/fixture.js'
|
|
10
9
|
import { getSimplifiedRootSpan } from '../__tests__/react/utils/otel.js'
|
|
11
10
|
import * as LiveStore from '../index.js'
|
|
12
11
|
import * as LiveStoreReact from './index.js'
|
|
@@ -118,7 +117,11 @@ describe.concurrent('useRow', () => {
|
|
|
118
117
|
using inputs = await makeTodoMvc({ useGlobalReactivityGraph: false })
|
|
119
118
|
const { wrapper, store, reactivityGraph, makeRenderCount, AppRouterSchema } = inputs
|
|
120
119
|
|
|
121
|
-
const allTodos$ = LiveStore.querySQL
|
|
120
|
+
const allTodos$ = LiveStore.querySQL(`select * from todos`, {
|
|
121
|
+
label: 'allTodos',
|
|
122
|
+
schema: Schema.Array(tables.todos.schema),
|
|
123
|
+
reactivityGraph,
|
|
124
|
+
})
|
|
122
125
|
|
|
123
126
|
const appRouterRenderCount = makeRenderCount()
|
|
124
127
|
let globalSetState: LiveStoreReact.StateSetters<typeof AppRouterSchema> | undefined
|
|
@@ -176,7 +179,7 @@ describe.concurrent('useRow', () => {
|
|
|
176
179
|
|
|
177
180
|
expect(appRouterRenderCount.val).toBe(2)
|
|
178
181
|
expect(renderResult.getByRole('content').innerHTML).toMatchInlineSnapshot(
|
|
179
|
-
`"{"id":"t1","text":"buy milk"
|
|
182
|
+
`"{"completed":false,"id":"t1","text":"buy milk"}"`,
|
|
180
183
|
)
|
|
181
184
|
|
|
182
185
|
expect(renderResult.getByRole('current-id').innerHTML).toMatchInlineSnapshot('"Current Task Id: t1"')
|
|
@@ -214,10 +217,11 @@ describe.concurrent('useRow', () => {
|
|
|
214
217
|
const [_row, _setRow, rowState$] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
|
|
215
218
|
const todos = LiveStoreReact.useTemporaryQuery(
|
|
216
219
|
() =>
|
|
217
|
-
LiveStore.querySQL
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
220
|
+
LiveStore.querySQL((get) => LiveStore.sql`select * from todos where text like '%${get(rowState$).text}%'`, {
|
|
221
|
+
schema: Schema.Array(tables.todos.schema),
|
|
222
|
+
reactivityGraph,
|
|
223
|
+
label: 'todosFiltered',
|
|
224
|
+
}),
|
|
221
225
|
userId,
|
|
222
226
|
)
|
|
223
227
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { Schema } from '@livestore/utils/effect'
|
|
1
2
|
import { renderHook } from '@testing-library/react'
|
|
2
3
|
import { describe, expect, it } from 'vitest'
|
|
3
4
|
|
|
4
|
-
import { makeTodoMvc,
|
|
5
|
+
import { makeTodoMvc, tables, todos } from '../__tests__/react/fixture.js'
|
|
5
6
|
import type * as LiveStore from '../index.js'
|
|
6
7
|
import { querySQL } from '../reactiveQueries/sql.js'
|
|
7
8
|
import * as LiveStoreReact from './index.js'
|
|
@@ -24,7 +25,9 @@ describe('useTemporaryQuery', () => {
|
|
|
24
25
|
renderCount.inc()
|
|
25
26
|
|
|
26
27
|
return LiveStoreReact.useTemporaryQuery(() => {
|
|
27
|
-
const query$ = querySQL(`select * from todos where id = '${id}'`, {
|
|
28
|
+
const query$ = querySQL(`select * from todos where id = '${id}'`, {
|
|
29
|
+
schema: Schema.Array(tables.todos.schema),
|
|
30
|
+
})
|
|
28
31
|
queryMap.set(id, query$)
|
|
29
32
|
return query$
|
|
30
33
|
}, id)
|
|
@@ -25,7 +25,11 @@ export const queryGraphQL = <
|
|
|
25
25
|
label,
|
|
26
26
|
reactivityGraph,
|
|
27
27
|
map,
|
|
28
|
-
}: {
|
|
28
|
+
}: {
|
|
29
|
+
label?: string
|
|
30
|
+
reactivityGraph?: ReactivityGraph
|
|
31
|
+
map?: MapResult<TResultMapped, TResult>
|
|
32
|
+
} = {},
|
|
29
33
|
): LiveQuery<TResultMapped, QueryInfoNone> =>
|
|
30
34
|
new LiveStoreGraphQLQuery({ document, genVariableValues, label, reactivityGraph, map })
|
|
31
35
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { Schema } from '@livestore/utils/effect'
|
|
1
2
|
import * as otel from '@opentelemetry/api'
|
|
2
3
|
import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
|
|
3
4
|
import { describe, expect, it } from 'vitest'
|
|
4
5
|
|
|
5
|
-
import { makeTodoMvc,
|
|
6
|
+
import { makeTodoMvc, tables } from '../__tests__/react/fixture.js'
|
|
6
7
|
import { getSimplifiedRootSpan } from '../__tests__/react/utils/otel.js'
|
|
7
|
-
import { computed,
|
|
8
|
+
import { computed, querySQL, rawSqlMutation, sql } from '../index.js'
|
|
8
9
|
|
|
9
10
|
/*
|
|
10
11
|
TODO write tests for:
|
|
@@ -45,7 +46,10 @@ describe('otel', () => {
|
|
|
45
46
|
using inputs = await makeQuery()
|
|
46
47
|
const { store, exporter, span } = inputs
|
|
47
48
|
|
|
48
|
-
const query = querySQL(`select * from todos`, {
|
|
49
|
+
const query = querySQL(`select * from todos`, {
|
|
50
|
+
schema: Schema.Array(tables.todos.schema),
|
|
51
|
+
queriedTables: new Set(['todos']),
|
|
52
|
+
})
|
|
49
53
|
expect(query.run()).toMatchInlineSnapshot('[]')
|
|
50
54
|
|
|
51
55
|
store.mutate(rawSqlMutation({ sql: sql`INSERT INTO todos (id, text, completed) VALUES ('t1', 'buy milk', 0)` }))
|
|
@@ -53,7 +57,7 @@ describe('otel', () => {
|
|
|
53
57
|
expect(query.run()).toMatchInlineSnapshot(`
|
|
54
58
|
[
|
|
55
59
|
{
|
|
56
|
-
"completed":
|
|
60
|
+
"completed": false,
|
|
57
61
|
"id": "t1",
|
|
58
62
|
"text": "buy milk",
|
|
59
63
|
},
|
|
@@ -172,7 +176,7 @@ describe('otel', () => {
|
|
|
172
176
|
const filter = computed(() => `where completed = 0`, { label: 'where-filter' })
|
|
173
177
|
const query = querySQL((get) => `select * from todos ${get(filter)}`, {
|
|
174
178
|
label: 'all todos',
|
|
175
|
-
|
|
179
|
+
schema: Schema.Array(tables.todos.schema).pipe(Schema.headOrElse(() => defaultTodo)),
|
|
176
180
|
})
|
|
177
181
|
|
|
178
182
|
expect(query.run()).toMatchInlineSnapshot(`
|
|
@@ -1,26 +1,24 @@
|
|
|
1
1
|
import { type Bindable, prepareBindValues, type QueryInfo, type QueryInfoNone } from '@livestore/common'
|
|
2
2
|
import { shouldNeverHappen } from '@livestore/utils'
|
|
3
|
-
import { Schema, TreeFormatter } from '@livestore/utils/effect'
|
|
3
|
+
import { Schema, SchemaEquivalence, TreeFormatter } from '@livestore/utils/effect'
|
|
4
4
|
import * as otel from '@opentelemetry/api'
|
|
5
5
|
|
|
6
6
|
import { globalReactivityGraph } from '../global-state.js'
|
|
7
7
|
import type { Thunk } from '../reactive.js'
|
|
8
|
+
import { NOT_REFRESHED_YET } from '../reactive.js'
|
|
8
9
|
import type { RefreshReason } from '../store.js'
|
|
9
10
|
import { getDurationMsFromSpan } from '../utils/otel.js'
|
|
10
11
|
import type { GetAtomResult, LiveQuery, QueryContext, ReactivityGraph } from './base-class.js'
|
|
11
12
|
import { LiveStoreQueryBase, makeGetAtomResult } from './base-class.js'
|
|
12
13
|
|
|
13
|
-
export type MapRows<TResult, TRaw = any> =
|
|
14
|
-
| ((rows: ReadonlyArray<TRaw>) => TResult)
|
|
15
|
-
| Schema.Schema<TResult, ReadonlyArray<TRaw>, unknown>
|
|
16
|
-
|
|
17
14
|
/**
|
|
18
15
|
* NOTE `querySQL` is only supposed to read data. Don't use it to insert/update/delete data but use mutations instead.
|
|
19
16
|
*/
|
|
20
|
-
export const querySQL = <
|
|
17
|
+
export const querySQL = <TResultSchema, TResult = TResultSchema>(
|
|
21
18
|
query: string | ((get: GetAtomResult) => string),
|
|
22
|
-
options
|
|
23
|
-
|
|
19
|
+
options: {
|
|
20
|
+
schema: Schema.Schema<TResultSchema, ReadonlyArray<any>>
|
|
21
|
+
map?: (rows: TResultSchema) => TResult
|
|
24
22
|
/**
|
|
25
23
|
* Can be provided explicitly to slightly speed up initial query performance
|
|
26
24
|
*
|
|
@@ -32,21 +30,23 @@ export const querySQL = <TResult, TRaw = any>(
|
|
|
32
30
|
reactivityGraph?: ReactivityGraph
|
|
33
31
|
},
|
|
34
32
|
): LiveQuery<TResult, QueryInfoNone> =>
|
|
35
|
-
new LiveStoreSQLQuery<TResult, QueryInfoNone>({
|
|
33
|
+
new LiveStoreSQLQuery<TResultSchema, TResult, QueryInfoNone>({
|
|
36
34
|
label: options?.label,
|
|
37
35
|
genQueryString: query,
|
|
38
36
|
queriedTables: options?.queriedTables,
|
|
39
37
|
bindValues: options?.bindValues,
|
|
40
38
|
reactivityGraph: options?.reactivityGraph,
|
|
41
39
|
map: options?.map,
|
|
40
|
+
schema: options.schema,
|
|
42
41
|
queryInfo: { _tag: 'None' },
|
|
43
42
|
})
|
|
44
43
|
|
|
45
44
|
/* An object encapsulating a reactive SQL query */
|
|
46
|
-
export class LiveStoreSQLQuery<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
export class LiveStoreSQLQuery<
|
|
46
|
+
TResultSchema,
|
|
47
|
+
TResult = TResultSchema,
|
|
48
|
+
TQueryInfo extends QueryInfo = QueryInfoNone,
|
|
49
|
+
> extends LiveStoreQueryBase<TResult, TQueryInfo> {
|
|
50
50
|
_tag: 'sql' = 'sql'
|
|
51
51
|
|
|
52
52
|
/** A reactive thunk representing the query text */
|
|
@@ -62,7 +62,8 @@ export class LiveStoreSQLQuery<TResult, TQueryInfo extends QueryInfo = QueryInfo
|
|
|
62
62
|
/** Currently only used by `rowQuery` for lazy table migrations and eager default row insertion */
|
|
63
63
|
private execBeforeFirstRun
|
|
64
64
|
|
|
65
|
-
private
|
|
65
|
+
private mapResult: (rows: TResultSchema) => TResult
|
|
66
|
+
private schema: Schema.Schema<TResultSchema, ReadonlyArray<any>>
|
|
66
67
|
|
|
67
68
|
queryInfo: TQueryInfo
|
|
68
69
|
|
|
@@ -70,8 +71,9 @@ export class LiveStoreSQLQuery<TResult, TQueryInfo extends QueryInfo = QueryInfo
|
|
|
70
71
|
genQueryString,
|
|
71
72
|
queriedTables,
|
|
72
73
|
bindValues,
|
|
73
|
-
label
|
|
74
|
+
label = genQueryString.toString(),
|
|
74
75
|
reactivityGraph,
|
|
76
|
+
schema,
|
|
75
77
|
map,
|
|
76
78
|
execBeforeFirstRun,
|
|
77
79
|
queryInfo,
|
|
@@ -81,51 +83,20 @@ export class LiveStoreSQLQuery<TResult, TQueryInfo extends QueryInfo = QueryInfo
|
|
|
81
83
|
queriedTables?: Set<string>
|
|
82
84
|
bindValues?: Bindable
|
|
83
85
|
reactivityGraph?: ReactivityGraph
|
|
84
|
-
|
|
86
|
+
schema: Schema.Schema<TResultSchema, ReadonlyArray<any>>
|
|
87
|
+
map?: (rows: TResultSchema) => TResult
|
|
85
88
|
execBeforeFirstRun?: (ctx: QueryContext) => void
|
|
86
89
|
queryInfo?: TQueryInfo
|
|
87
90
|
}) {
|
|
88
91
|
super()
|
|
89
92
|
|
|
90
|
-
const label = label_ ?? genQueryString.toString()
|
|
91
93
|
this.label = `sql(${label})`
|
|
92
94
|
this.reactivityGraph = reactivityGraph ?? globalReactivityGraph
|
|
93
95
|
this.execBeforeFirstRun = execBeforeFirstRun
|
|
94
96
|
this.queryInfo = queryInfo ?? ({ _tag: 'None' } as TQueryInfo)
|
|
95
|
-
this.mapRows =
|
|
96
|
-
map === undefined
|
|
97
|
-
? (rows: any) => rows as TResult
|
|
98
|
-
: Schema.isSchema(map)
|
|
99
|
-
? (rows: any, opts: { sqlString: string }) => {
|
|
100
|
-
const parseResult = Schema.decodeEither(map as Schema.Schema<TResult, ReadonlyArray<any>>)(rows)
|
|
101
|
-
if (parseResult._tag === 'Left') {
|
|
102
|
-
const parseErrorStr = TreeFormatter.formatErrorSync(parseResult.left)
|
|
103
|
-
const expectedSchemaStr = String(map.ast)
|
|
104
|
-
const bindValuesStr = bindValues === undefined ? '' : `\nBind values: ${JSON.stringify(bindValues)}`
|
|
105
|
-
|
|
106
|
-
console.error(
|
|
107
|
-
`\
|
|
108
|
-
Error parsing SQL query result.
|
|
109
|
-
|
|
110
|
-
Query: ${opts.sqlString}\
|
|
111
|
-
${bindValuesStr}
|
|
112
|
-
|
|
113
|
-
Expected schema: ${expectedSchemaStr}
|
|
114
97
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
Result:`,
|
|
118
|
-
rows,
|
|
119
|
-
)
|
|
120
|
-
// console.error(`Error parsing SQL query result: ${TreeFormatter.formatErrorSync(parseResult.left)}`)
|
|
121
|
-
return shouldNeverHappen(`Error parsing SQL query result: ${parseResult.left}`)
|
|
122
|
-
} else {
|
|
123
|
-
return parseResult.right as TResult
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
: typeof map === 'function'
|
|
127
|
-
? map
|
|
128
|
-
: shouldNeverHappen(`Invalid map function ${map}`)
|
|
98
|
+
this.schema = schema
|
|
99
|
+
this.mapResult = map === undefined ? (rows: any) => rows as TResult : map
|
|
129
100
|
|
|
130
101
|
let queryString$OrQueryString: string | Thunk<string, QueryContext, RefreshReason>
|
|
131
102
|
if (typeof genQueryString === 'function') {
|
|
@@ -137,7 +108,11 @@ Result:`,
|
|
|
137
108
|
setDebugInfo({ _tag: 'js', label: `${label}:queryString`, query: queryString, durationMs })
|
|
138
109
|
return queryString
|
|
139
110
|
},
|
|
140
|
-
{
|
|
111
|
+
{
|
|
112
|
+
label: `${label}:queryString`,
|
|
113
|
+
meta: { liveStoreThunkType: 'sqlQueryString' },
|
|
114
|
+
equal: (a, b) => a === b,
|
|
115
|
+
},
|
|
141
116
|
)
|
|
142
117
|
|
|
143
118
|
this.queryString$ = queryString$OrQueryString
|
|
@@ -149,6 +124,15 @@ Result:`,
|
|
|
149
124
|
|
|
150
125
|
const queriedTablesRef = { current: queriedTables }
|
|
151
126
|
|
|
127
|
+
const schemaEqual = SchemaEquivalence.make(schema)
|
|
128
|
+
// TODO also support derived equality for `map` (probably will depend on having an easy way to transform a schema without an `encode` step)
|
|
129
|
+
// This would mean dropping the `map` option
|
|
130
|
+
const equal =
|
|
131
|
+
map === undefined
|
|
132
|
+
? (a: TResult, b: TResult) =>
|
|
133
|
+
a === NOT_REFRESHED_YET || b === NOT_REFRESHED_YET ? false : schemaEqual(a as any, b as any)
|
|
134
|
+
: undefined
|
|
135
|
+
|
|
152
136
|
const results$ = this.reactivityGraph.makeThunk<TResult>(
|
|
153
137
|
(get, setDebugInfo, { store, otelTracer, rootOtelContext }, otelContext) =>
|
|
154
138
|
otelTracer.startActiveSpan(
|
|
@@ -189,7 +173,31 @@ Result:`,
|
|
|
189
173
|
|
|
190
174
|
span.setAttribute('sql.rowsCount', rawResults.length)
|
|
191
175
|
|
|
192
|
-
const
|
|
176
|
+
const parsedResult = Schema.decodeEither(this.schema)(rawResults)
|
|
177
|
+
|
|
178
|
+
if (parsedResult._tag === 'Left') {
|
|
179
|
+
const parseErrorStr = TreeFormatter.formatErrorSync(parsedResult.left)
|
|
180
|
+
const expectedSchemaStr = String(this.schema.ast)
|
|
181
|
+
const bindValuesStr = bindValues === undefined ? '' : `\nBind values: ${JSON.stringify(bindValues)}`
|
|
182
|
+
|
|
183
|
+
console.error(
|
|
184
|
+
`\
|
|
185
|
+
Error parsing SQL query result.
|
|
186
|
+
|
|
187
|
+
Query: ${sqlString}\
|
|
188
|
+
${bindValuesStr}
|
|
189
|
+
|
|
190
|
+
Expected schema: ${expectedSchemaStr}
|
|
191
|
+
|
|
192
|
+
Error: ${parseErrorStr}
|
|
193
|
+
|
|
194
|
+
Result:`,
|
|
195
|
+
rawResults,
|
|
196
|
+
)
|
|
197
|
+
return shouldNeverHappen(`Error parsing SQL query result: ${parsedResult.left}`)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const result = this.mapResult(parsedResult.right)
|
|
193
201
|
|
|
194
202
|
span.end()
|
|
195
203
|
|
|
@@ -202,45 +210,12 @@ Result:`,
|
|
|
202
210
|
return result
|
|
203
211
|
},
|
|
204
212
|
),
|
|
205
|
-
{ label: queryLabel },
|
|
213
|
+
{ label: queryLabel, equal },
|
|
206
214
|
)
|
|
207
215
|
|
|
208
216
|
this.results$ = results$
|
|
209
217
|
}
|
|
210
218
|
|
|
211
|
-
/**
|
|
212
|
-
* Returns a new reactive query that contains the result of
|
|
213
|
-
* running an arbitrary JS computation on the results of this SQL query.
|
|
214
|
-
*/
|
|
215
|
-
// pipe = <U>(fn: (result: Result, get: GetAtomResult) => U): LiveStoreJSQuery<U> =>
|
|
216
|
-
// new LiveStoreJSQuery({
|
|
217
|
-
// fn: (get) => {
|
|
218
|
-
// const results = get(this.results$!)
|
|
219
|
-
// return fn(results, get)
|
|
220
|
-
// },
|
|
221
|
-
// label: `${this.label}:js`,
|
|
222
|
-
// onDestroy: () => this.destroy(),
|
|
223
|
-
// reactivityGraph: this.reactivityGraph,
|
|
224
|
-
// queryInfo: undefined,
|
|
225
|
-
// })
|
|
226
|
-
|
|
227
|
-
/** Returns a reactive query */
|
|
228
|
-
// getFirstRow = (args?: { defaultValue?: Result }) =>
|
|
229
|
-
// new LiveStoreJSQuery({
|
|
230
|
-
// fn: (get) => {
|
|
231
|
-
// const results = get(this.results$!)
|
|
232
|
-
// if (results.length === 0 && args?.defaultValue === undefined) {
|
|
233
|
-
// // const queryLabel = this._tag === 'sql' ? this.queryString$!.computeResult(otelContext) : this.label
|
|
234
|
-
// const queryLabel = this.label
|
|
235
|
-
// return shouldNeverHappen(`Expected query ${queryLabel} to return at least one result`)
|
|
236
|
-
// }
|
|
237
|
-
// return results[0] ?? args!.defaultValue!
|
|
238
|
-
// },
|
|
239
|
-
// label: `${this.label}:first`,
|
|
240
|
-
// onDestroy: () => this.destroy(),
|
|
241
|
-
// reactivityGraph: this.reactivityGraph,
|
|
242
|
-
// })
|
|
243
|
-
|
|
244
219
|
destroy = () => {
|
|
245
220
|
if (this.queryString$ !== undefined) {
|
|
246
221
|
this.reactivityGraph.destroyNode(this.queryString$)
|