@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.
Files changed (39) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/__tests__/react/fixture.d.ts +0 -5
  3. package/dist/__tests__/react/fixture.d.ts.map +1 -1
  4. package/dist/__tests__/react/fixture.js +1 -2
  5. package/dist/__tests__/react/fixture.js.map +1 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/react/LiveStoreProvider.test.js +3 -2
  10. package/dist/react/LiveStoreProvider.test.js.map +1 -1
  11. package/dist/react/useQuery.test.js +11 -4
  12. package/dist/react/useQuery.test.js.map +1 -1
  13. package/dist/react/useRow.test.js +13 -5
  14. package/dist/react/useRow.test.js.map +1 -1
  15. package/dist/react/useTemporaryQuery.test.js +5 -2
  16. package/dist/react/useTemporaryQuery.test.js.map +1 -1
  17. package/dist/reactiveQueries/graphql.d.ts.map +1 -1
  18. package/dist/reactiveQueries/graphql.js.map +1 -1
  19. package/dist/reactiveQueries/sql.d.ts +9 -12
  20. package/dist/reactiveQueries/sql.d.ts.map +1 -1
  21. package/dist/reactiveQueries/sql.js +39 -69
  22. package/dist/reactiveQueries/sql.js.map +1 -1
  23. package/dist/reactiveQueries/sql.test.js +9 -5
  24. package/dist/reactiveQueries/sql.test.js.map +1 -1
  25. package/dist/row-query.d.ts +6 -4
  26. package/dist/row-query.d.ts.map +1 -1
  27. package/dist/row-query.js +5 -12
  28. package/dist/row-query.js.map +1 -1
  29. package/package.json +5 -5
  30. package/src/__tests__/react/fixture.tsx +1 -3
  31. package/src/index.ts +1 -1
  32. package/src/react/LiveStoreProvider.test.tsx +3 -2
  33. package/src/react/useQuery.test.tsx +11 -4
  34. package/src/react/useRow.test.tsx +13 -9
  35. package/src/react/useTemporaryQuery.test.tsx +5 -2
  36. package/src/reactiveQueries/graphql.ts +5 -1
  37. package/src/reactiveQueries/sql.test.ts +9 -5
  38. package/src/reactiveQueries/sql.ts +61 -86
  39. 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 = <TResult, TRaw = any>(
17
+ export const querySQL = <TResultSchema, TResult = TResultSchema>(
21
18
  query: string | ((get: GetAtomResult) => string),
22
- options?: {
23
- map?: MapRows<TResult, TRaw>
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<TResult, TQueryInfo extends QueryInfo = QueryInfoNone> extends LiveStoreQueryBase<
47
- TResult,
48
- TQueryInfo
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 mapRows
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: 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
- map?: MapRows<TResult>
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
- Error: ${parseErrorStr}
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
- { label: `${label}:queryString`, meta: { liveStoreThunkType: 'sqlQueryString' } },
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 result = this.mapRows(rawResults, { sqlString })
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, TreeFormatter } from '@livestore/utils/effect'
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: Partial<RowResult<TTableDef>>
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<RowResult<TTableDef>, QueryInfoRow<TTableDef>>
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
- map: (results): RowResult<TTableDef> => {
87
- if (results.length === 0) return shouldNeverHappen(`No results for query ${queryStr}`)
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
  }