@livestore/livestore 0.0.55-dev.3 → 0.0.55
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.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/package.json +5 -5
- 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/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
|
@@ -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$)
|
package/src/row-query.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { sql } from '@livestore/common'
|
|
|
3
3
|
import { DbSchema } from '@livestore/common/schema'
|
|
4
4
|
import type { GetValForKey } from '@livestore/utils'
|
|
5
5
|
import { shouldNeverHappen } from '@livestore/utils'
|
|
6
|
-
import { Schema
|
|
6
|
+
import { Schema } from '@livestore/utils/effect'
|
|
7
7
|
import type * as otel from '@opentelemetry/api'
|
|
8
8
|
import type { SqliteDsl } from 'effect-db-schema'
|
|
9
9
|
|
|
@@ -12,14 +12,16 @@ import { computed } from './reactiveQueries/js.js'
|
|
|
12
12
|
import { LiveStoreSQLQuery } from './reactiveQueries/sql.js'
|
|
13
13
|
import type { Store } from './store.js'
|
|
14
14
|
|
|
15
|
-
export type RowQueryOptions = {
|
|
15
|
+
export type RowQueryOptions<TTableDef extends DbSchema.TableDef, TResult = RowResult<TTableDef>> = {
|
|
16
16
|
otelContext?: otel.Context
|
|
17
17
|
skipInsertDefaultRow?: boolean
|
|
18
18
|
reactivityGraph?: ReactivityGraph
|
|
19
|
+
map?: (result: RowResult<TTableDef>) => TResult
|
|
20
|
+
label?: string
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export type RowQueryOptionsDefaulValues<TTableDef extends DbSchema.TableDef> = {
|
|
22
|
-
defaultValues
|
|
24
|
+
defaultValues?: Partial<RowResult<TTableDef>>
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export type MakeRowQuery = {
|
|
@@ -29,9 +31,10 @@ export type MakeRowQuery = {
|
|
|
29
31
|
boolean,
|
|
30
32
|
DbSchema.TableOptions & { isSingleton: true }
|
|
31
33
|
>,
|
|
34
|
+
TResult = RowResult<TTableDef>,
|
|
32
35
|
>(
|
|
33
36
|
table: TTableDef,
|
|
34
|
-
options?: RowQueryOptions,
|
|
37
|
+
options?: RowQueryOptions<TTableDef, TResult>,
|
|
35
38
|
): LiveQuery<RowResult<TTableDef>, QueryInfoRow<TTableDef>>
|
|
36
39
|
<
|
|
37
40
|
TTableDef extends DbSchema.TableDef<
|
|
@@ -39,19 +42,20 @@ export type MakeRowQuery = {
|
|
|
39
42
|
boolean,
|
|
40
43
|
DbSchema.TableOptions & { isSingleton: false }
|
|
41
44
|
>,
|
|
45
|
+
TResult = RowResult<TTableDef>,
|
|
42
46
|
>(
|
|
43
47
|
table: TTableDef,
|
|
44
48
|
// TODO adjust so it works with arbitrary primary keys or unique constraints
|
|
45
49
|
id: string,
|
|
46
|
-
options?: RowQueryOptions & RowQueryOptionsDefaulValues<TTableDef>,
|
|
47
|
-
): LiveQuery<
|
|
50
|
+
options?: RowQueryOptions<TTableDef, TResult> & RowQueryOptionsDefaulValues<TTableDef>,
|
|
51
|
+
): LiveQuery<TResult, QueryInfoRow<TTableDef>>
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
// TODO also allow other where clauses and multiple rows
|
|
51
55
|
export const rowQuery: MakeRowQuery = <TTableDef extends DbSchema.TableDef>(
|
|
52
56
|
table: TTableDef,
|
|
53
|
-
idOrOptions?: string | RowQueryOptions,
|
|
54
|
-
options_?: RowQueryOptions & RowQueryOptionsDefaulValues<TTableDef>,
|
|
57
|
+
idOrOptions?: string | RowQueryOptions<TTableDef, any>,
|
|
58
|
+
options_?: RowQueryOptions<TTableDef, any> & RowQueryOptionsDefaulValues<TTableDef>,
|
|
55
59
|
) => {
|
|
56
60
|
const id = typeof idOrOptions === 'string' ? idOrOptions : undefined
|
|
57
61
|
const options = typeof idOrOptions === 'string' ? options_ : idOrOptions
|
|
@@ -70,8 +74,10 @@ export const rowQuery: MakeRowQuery = <TTableDef extends DbSchema.TableDef>(
|
|
|
70
74
|
const whereClause = id === undefined ? '' : `where id = '${id}'`
|
|
71
75
|
const queryStr = sql`select * from ${tableName} ${whereClause} limit 1`
|
|
72
76
|
|
|
77
|
+
const rowSchema = table.isSingleColumn === true ? table.schema.pipe(Schema.pluck('value' as any)) : table.schema
|
|
78
|
+
|
|
73
79
|
return new LiveStoreSQLQuery({
|
|
74
|
-
label: `rowQuery:query:${tableSchema.name}${id === undefined ? '' : `:${id}`}`,
|
|
80
|
+
label: options?.label ?? `rowQuery:query:${tableSchema.name}${id === undefined ? '' : `:${id}`}`,
|
|
75
81
|
genQueryString: queryStr,
|
|
76
82
|
queriedTables: new Set([tableName]),
|
|
77
83
|
reactivityGraph: options?.reactivityGraph,
|
|
@@ -83,18 +89,8 @@ export const rowQuery: MakeRowQuery = <TTableDef extends DbSchema.TableDef>(
|
|
|
83
89
|
id,
|
|
84
90
|
skipInsertDefaultRow: options?.skipInsertDefaultRow,
|
|
85
91
|
}),
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const parseResult = Schema.decodeEither(table.schema)(results[0]!)
|
|
90
|
-
|
|
91
|
-
if (parseResult._tag === 'Left') {
|
|
92
|
-
console.error('decode error', TreeFormatter.formatErrorSync(parseResult.left), 'results', results)
|
|
93
|
-
return shouldNeverHappen(`Error decoding query result for ${queryStr}`)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return table.isSingleColumn === true ? parseResult.right.value : parseResult.right
|
|
97
|
-
},
|
|
92
|
+
schema: rowSchema.pipe(Schema.Array, Schema.headOrElse()),
|
|
93
|
+
map: options?.map,
|
|
98
94
|
queryInfo: { _tag: 'Row', table, id: id ?? 'singleton' },
|
|
99
95
|
})
|
|
100
96
|
}
|