@livestore/livestore 0.0.38 → 0.0.39-dev.3
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/README.md +3 -4
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/react/fixture.d.ts +97 -3
- package/dist/__tests__/react/fixture.d.ts.map +1 -1
- package/dist/__tests__/react/fixture.js +10 -7
- package/dist/__tests__/react/fixture.js.map +1 -1
- package/dist/__tests__/react/useQuery.test.js +12 -23
- package/dist/__tests__/react/useQuery.test.js.map +1 -1
- package/dist/__tests__/react/useRow.test.js +4 -4
- package/dist/__tests__/react/useRow.test.js.map +1 -1
- package/dist/__tests__/reactiveQueries/sql.test.js +32 -35
- package/dist/__tests__/reactiveQueries/sql.test.js.map +1 -1
- package/dist/effect/LiveStore.d.ts +3 -2
- package/dist/effect/LiveStore.d.ts.map +1 -1
- package/dist/effect/LiveStore.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/migrations.js +2 -2
- package/dist/migrations.js.map +1 -1
- package/dist/mutations.d.ts +3 -1
- package/dist/mutations.d.ts.map +1 -1
- package/dist/mutations.js +2 -2
- package/dist/mutations.js.map +1 -1
- package/dist/react/LiveStoreContext.d.ts +2 -2
- package/dist/react/LiveStoreContext.d.ts.map +1 -1
- package/dist/react/LiveStoreContext.js.map +1 -1
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/useAtom.d.ts +5 -0
- package/dist/react/useAtom.d.ts.map +1 -0
- package/dist/react/useAtom.js +16 -0
- package/dist/react/useAtom.js.map +1 -0
- package/dist/react/useQuery.d.ts +3 -3
- package/dist/react/useQuery.d.ts.map +1 -1
- package/dist/react/useQuery.js.map +1 -1
- package/dist/react/useRow.d.ts +5 -5
- package/dist/react/useRow.d.ts.map +1 -1
- package/dist/react/useRow.js +16 -30
- package/dist/react/useRow.js.map +1 -1
- package/dist/react/useTemporaryQuery.d.ts +3 -3
- package/dist/react/useTemporaryQuery.d.ts.map +1 -1
- package/dist/react/useTemporaryQuery.js.map +1 -1
- package/dist/reactiveQueries/base-class.d.ts +13 -4
- package/dist/reactiveQueries/base-class.d.ts.map +1 -1
- package/dist/reactiveQueries/base-class.js +1 -0
- package/dist/reactiveQueries/base-class.js.map +1 -1
- package/dist/reactiveQueries/graphql.d.ts +5 -5
- package/dist/reactiveQueries/graphql.d.ts.map +1 -1
- package/dist/reactiveQueries/graphql.js +11 -10
- package/dist/reactiveQueries/graphql.js.map +1 -1
- package/dist/reactiveQueries/js.d.ts +10 -7
- package/dist/reactiveQueries/js.d.ts.map +1 -1
- package/dist/reactiveQueries/js.js +19 -11
- package/dist/reactiveQueries/js.js.map +1 -1
- package/dist/reactiveQueries/sql.d.ts +21 -15
- package/dist/reactiveQueries/sql.d.ts.map +1 -1
- package/dist/reactiveQueries/sql.js +61 -28
- package/dist/reactiveQueries/sql.js.map +1 -1
- package/dist/row-query.d.ts +22 -21
- package/dist/row-query.d.ts.map +1 -1
- package/dist/row-query.js +62 -47
- package/dist/row-query.js.map +1 -1
- package/dist/schema/index.d.ts +3 -2
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +3 -2
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/parse-utils.d.ts +9 -0
- package/dist/schema/parse-utils.d.ts.map +1 -0
- package/dist/schema/parse-utils.js +47 -0
- package/dist/schema/parse-utils.js.map +1 -0
- package/dist/schema/system-tables.d.ts +24 -8
- package/dist/schema/system-tables.d.ts.map +1 -1
- package/dist/schema/table-def.d.ts +32 -7
- package/dist/schema/table-def.d.ts.map +1 -1
- package/dist/schema/table-def.js +18 -6
- package/dist/schema/table-def.js.map +1 -1
- package/dist/store.d.ts +4 -8
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +7 -8
- package/dist/store.js.map +1 -1
- package/dist/update-path.d.ts +52 -0
- package/dist/update-path.d.ts.map +1 -0
- package/dist/update-path.js +33 -0
- package/dist/update-path.js.map +1 -0
- package/dist/utils/util.d.ts +1 -0
- package/dist/utils/util.d.ts.map +1 -1
- package/dist/utils/util.js.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/react/fixture.tsx +13 -7
- package/src/__tests__/react/useQuery.test.tsx +12 -29
- package/src/__tests__/react/useRow.test.tsx +5 -7
- package/src/__tests__/reactiveQueries/sql.test.ts +33 -35
- package/src/effect/LiveStore.ts +3 -2
- package/src/index.ts +6 -6
- package/src/migrations.ts +2 -2
- package/src/mutations.ts +8 -3
- package/src/react/LiveStoreContext.ts +3 -2
- package/src/react/index.ts +1 -0
- package/src/react/useAtom.ts +25 -0
- package/src/react/useQuery.ts +7 -7
- package/src/react/useRow.ts +27 -47
- package/src/react/useTemporaryQuery.ts +4 -6
- package/src/reactiveQueries/base-class.ts +20 -4
- package/src/reactiveQueries/graphql.ts +16 -14
- package/src/reactiveQueries/js.ts +36 -15
- package/src/reactiveQueries/sql.ts +87 -37
- package/src/row-query.ts +155 -113
- package/src/schema/index.ts +5 -4
- package/src/schema/parse-utils.ts +84 -0
- package/src/schema/table-def.ts +80 -12
- package/src/store.ts +14 -29
- package/src/update-path.ts +102 -0
- package/src/utils/util.ts +2 -0
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
import { shouldNeverHappen } from '@livestore/utils'
|
|
2
|
+
import { Schema, TreeFormatter } from '@livestore/utils/effect'
|
|
2
3
|
import * as otel from '@opentelemetry/api'
|
|
3
4
|
|
|
4
5
|
import { globalDbGraph } from '../global-state.js'
|
|
5
6
|
import type { Thunk } from '../reactive.js'
|
|
6
7
|
import type { RefreshReason } from '../store.js'
|
|
8
|
+
import type { UpdatePathDesc, UpdatePathDescNone } from '../update-path.js'
|
|
7
9
|
import { getDurationMsFromSpan } from '../utils/otel.js'
|
|
8
10
|
import type { Bindable } from '../utils/util.js'
|
|
9
11
|
import { prepareBindValues } from '../utils/util.js'
|
|
10
|
-
import type { DbContext, DbGraph, GetAtomResult } from './base-class.js'
|
|
12
|
+
import type { DbContext, DbGraph, GetAtomResult, LiveQuery } from './base-class.js'
|
|
11
13
|
import { LiveStoreQueryBase, makeGetAtomResult } from './base-class.js'
|
|
12
|
-
import { LiveStoreJSQuery } from './js.js'
|
|
13
14
|
|
|
14
|
-
export
|
|
15
|
+
export type MapRows<TResult, TRaw = any> =
|
|
16
|
+
| ((rows: ReadonlyArray<TRaw>) => TResult)
|
|
17
|
+
| Schema.Schema<ReadonlyArray<TRaw>, TResult>
|
|
18
|
+
|
|
19
|
+
export const querySQL = <Result, TRaw = any>(
|
|
15
20
|
query: string | ((get: GetAtomResult) => string),
|
|
16
21
|
options?: {
|
|
22
|
+
map?: MapRows<Result, TRaw>
|
|
17
23
|
/**
|
|
18
24
|
* Can be provided explicitly to slightly speed up initial query performance
|
|
19
25
|
*
|
|
@@ -24,28 +30,40 @@ export const querySQL = <Row>(
|
|
|
24
30
|
label?: string
|
|
25
31
|
dbGraph?: DbGraph
|
|
26
32
|
},
|
|
27
|
-
) =>
|
|
28
|
-
new LiveStoreSQLQuery<
|
|
33
|
+
): LiveQuery<Result, UpdatePathDescNone> =>
|
|
34
|
+
new LiveStoreSQLQuery<Result, UpdatePathDescNone>({
|
|
29
35
|
label: options?.label,
|
|
30
36
|
genQueryString: query,
|
|
31
37
|
queriedTables: options?.queriedTables,
|
|
32
38
|
bindValues: options?.bindValues,
|
|
33
39
|
dbGraph: options?.dbGraph,
|
|
40
|
+
map: options?.map,
|
|
41
|
+
updatePathDesc: { _tag: 'None' },
|
|
34
42
|
})
|
|
35
43
|
|
|
36
44
|
/* An object encapsulating a reactive SQL query */
|
|
37
|
-
export class LiveStoreSQLQuery<
|
|
45
|
+
export class LiveStoreSQLQuery<
|
|
46
|
+
Result,
|
|
47
|
+
TUpdatePath extends UpdatePathDesc = UpdatePathDescNone,
|
|
48
|
+
> extends LiveStoreQueryBase<Result, TUpdatePath> {
|
|
38
49
|
_tag: 'sql' = 'sql'
|
|
39
50
|
|
|
40
51
|
/** A reactive thunk representing the query text */
|
|
41
52
|
queryString$: Thunk<string, DbContext, RefreshReason>
|
|
42
53
|
|
|
43
54
|
/** A reactive thunk representing the query results */
|
|
44
|
-
results$: Thunk<
|
|
55
|
+
results$: Thunk<Result, DbContext, RefreshReason>
|
|
45
56
|
|
|
46
57
|
label: string
|
|
47
58
|
|
|
48
|
-
protected dbGraph
|
|
59
|
+
protected dbGraph
|
|
60
|
+
|
|
61
|
+
/** Currently only used by `rowQuery` for lazy table migrations and eager default row insertion */
|
|
62
|
+
private execBeforeFirstRun
|
|
63
|
+
|
|
64
|
+
private mapRows
|
|
65
|
+
|
|
66
|
+
updatePathDesc: TUpdatePath
|
|
49
67
|
|
|
50
68
|
constructor({
|
|
51
69
|
genQueryString,
|
|
@@ -53,18 +71,42 @@ export class LiveStoreSQLQuery<Row> extends LiveStoreQueryBase<ReadonlyArray<Row
|
|
|
53
71
|
bindValues,
|
|
54
72
|
label: label_,
|
|
55
73
|
dbGraph,
|
|
74
|
+
map,
|
|
75
|
+
execBeforeFirstRun,
|
|
76
|
+
updatePathDesc,
|
|
56
77
|
}: {
|
|
57
78
|
label?: string
|
|
58
79
|
genQueryString: string | ((get: GetAtomResult) => string)
|
|
59
80
|
queriedTables?: Set<string>
|
|
60
81
|
bindValues?: Bindable
|
|
61
82
|
dbGraph?: DbGraph
|
|
83
|
+
map?: MapRows<Result>
|
|
84
|
+
execBeforeFirstRun?: (ctx: DbContext) => void
|
|
85
|
+
updatePathDesc?: TUpdatePath
|
|
62
86
|
}) {
|
|
63
87
|
super()
|
|
64
88
|
|
|
65
89
|
const label = label_ ?? genQueryString.toString()
|
|
66
90
|
this.label = `sql(${label})`
|
|
67
91
|
this.dbGraph = dbGraph ?? globalDbGraph
|
|
92
|
+
this.execBeforeFirstRun = execBeforeFirstRun
|
|
93
|
+
this.updatePathDesc = updatePathDesc ?? ({ _tag: 'None' } as TUpdatePath)
|
|
94
|
+
this.mapRows =
|
|
95
|
+
map === undefined
|
|
96
|
+
? (rows: any) => rows as Result
|
|
97
|
+
: Schema.isSchema(map)
|
|
98
|
+
? (rows: any) => {
|
|
99
|
+
const parseResult = Schema.parseEither(map)(rows)
|
|
100
|
+
if (parseResult._tag === 'Left') {
|
|
101
|
+
console.error(`Error parsing SQL query result: ${TreeFormatter.formatError(parseResult.left)}`)
|
|
102
|
+
return shouldNeverHappen(`Error parsing SQL query result: ${parseResult.left}`)
|
|
103
|
+
} else {
|
|
104
|
+
return parseResult.right as Result
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
: typeof map === 'function'
|
|
108
|
+
? map
|
|
109
|
+
: shouldNeverHappen(`Invalid map function ${map}`)
|
|
68
110
|
|
|
69
111
|
// TODO don't even create a thunk if query string is static
|
|
70
112
|
const queryString$ = this.dbGraph.makeThunk(
|
|
@@ -88,7 +130,7 @@ export class LiveStoreSQLQuery<Row> extends LiveStoreQueryBase<ReadonlyArray<Row
|
|
|
88
130
|
|
|
89
131
|
const queriedTablesRef = { current: queriedTables }
|
|
90
132
|
|
|
91
|
-
const results$ = this.dbGraph.makeThunk<
|
|
133
|
+
const results$ = this.dbGraph.makeThunk<Result>(
|
|
92
134
|
(get, setDebugInfo, { store, otelTracer, rootOtelContext }, otelContext) =>
|
|
93
135
|
otelTracer.startActiveSpan(
|
|
94
136
|
'sql:...', // NOTE span name will be overridden further down
|
|
@@ -97,6 +139,11 @@ export class LiveStoreSQLQuery<Row> extends LiveStoreQueryBase<ReadonlyArray<Row
|
|
|
97
139
|
(span) => {
|
|
98
140
|
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
99
141
|
|
|
142
|
+
if (this.execBeforeFirstRun !== undefined) {
|
|
143
|
+
this.execBeforeFirstRun({ store, otelTracer, rootOtelContext })
|
|
144
|
+
this.execBeforeFirstRun = undefined
|
|
145
|
+
}
|
|
146
|
+
|
|
100
147
|
const sqlString = get(queryString$, otelContext)
|
|
101
148
|
|
|
102
149
|
if (queriedTablesRef.current === undefined) {
|
|
@@ -112,13 +159,15 @@ export class LiveStoreSQLQuery<Row> extends LiveStoreQueryBase<ReadonlyArray<Row
|
|
|
112
159
|
span.setAttribute('sql.query', sqlString)
|
|
113
160
|
span.updateName(`sql:${sqlString.slice(0, 50)}`)
|
|
114
161
|
|
|
115
|
-
const
|
|
162
|
+
const rawResults = store.inMemoryDB.select<any>(sqlString, {
|
|
116
163
|
queriedTables,
|
|
117
164
|
bindValues: bindValues ? prepareBindValues(bindValues, sqlString) : undefined,
|
|
118
165
|
otelContext,
|
|
119
166
|
})
|
|
120
167
|
|
|
121
|
-
span.setAttribute('sql.rowsCount',
|
|
168
|
+
span.setAttribute('sql.rowsCount', rawResults.length)
|
|
169
|
+
|
|
170
|
+
const result = this.mapRows(rawResults)
|
|
122
171
|
|
|
123
172
|
span.end()
|
|
124
173
|
|
|
@@ -126,7 +175,7 @@ export class LiveStoreSQLQuery<Row> extends LiveStoreQueryBase<ReadonlyArray<Row
|
|
|
126
175
|
|
|
127
176
|
setDebugInfo({ _tag: 'sql', label, query: sqlString, durationMs })
|
|
128
177
|
|
|
129
|
-
return
|
|
178
|
+
return result
|
|
130
179
|
},
|
|
131
180
|
),
|
|
132
181
|
{ label: queryLabel },
|
|
@@ -139,33 +188,34 @@ export class LiveStoreSQLQuery<Row> extends LiveStoreQueryBase<ReadonlyArray<Row
|
|
|
139
188
|
* Returns a new reactive query that contains the result of
|
|
140
189
|
* running an arbitrary JS computation on the results of this SQL query.
|
|
141
190
|
*/
|
|
142
|
-
pipe = <U>(fn: (result:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
191
|
+
// pipe = <U>(fn: (result: Result, get: GetAtomResult) => U): LiveStoreJSQuery<U> =>
|
|
192
|
+
// new LiveStoreJSQuery({
|
|
193
|
+
// fn: (get) => {
|
|
194
|
+
// const results = get(this.results$!)
|
|
195
|
+
// return fn(results, get)
|
|
196
|
+
// },
|
|
197
|
+
// label: `${this.label}:js`,
|
|
198
|
+
// onDestroy: () => this.destroy(),
|
|
199
|
+
// dbGraph: this.dbGraph,
|
|
200
|
+
// updatePathDesc: undefined,
|
|
201
|
+
// })
|
|
152
202
|
|
|
153
203
|
/** Returns a reactive query */
|
|
154
|
-
getFirstRow = (args?: { defaultValue?:
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
204
|
+
// getFirstRow = (args?: { defaultValue?: Result }) =>
|
|
205
|
+
// new LiveStoreJSQuery({
|
|
206
|
+
// fn: (get) => {
|
|
207
|
+
// const results = get(this.results$!)
|
|
208
|
+
// if (results.length === 0 && args?.defaultValue === undefined) {
|
|
209
|
+
// // const queryLabel = this._tag === 'sql' ? this.queryString$!.computeResult(otelContext) : this.label
|
|
210
|
+
// const queryLabel = this.label
|
|
211
|
+
// return shouldNeverHappen(`Expected query ${queryLabel} to return at least one result`)
|
|
212
|
+
// }
|
|
213
|
+
// return results[0] ?? args!.defaultValue!
|
|
214
|
+
// },
|
|
215
|
+
// label: `${this.label}:first`,
|
|
216
|
+
// onDestroy: () => this.destroy(),
|
|
217
|
+
// dbGraph: this.dbGraph,
|
|
218
|
+
// })
|
|
169
219
|
|
|
170
220
|
destroy = () => {
|
|
171
221
|
this.dbGraph.destroyNode(this.queryString$)
|
package/src/row-query.ts
CHANGED
|
@@ -3,167 +3,209 @@ import { pipe, ReadonlyRecord, Schema, TreeFormatter } from '@livestore/utils/ef
|
|
|
3
3
|
import type * as otel from '@opentelemetry/api'
|
|
4
4
|
import { SqliteAst, SqliteDsl } from 'effect-db-schema'
|
|
5
5
|
|
|
6
|
+
import { computed } from './index.js'
|
|
6
7
|
import type { InMemoryDatabase } from './inMemoryDatabase.js'
|
|
7
8
|
import { migrateTable } from './migrations.js'
|
|
8
9
|
import type { Ref } from './reactive.js'
|
|
9
|
-
import type { DbContext, DbGraph } from './reactiveQueries/base-class.js'
|
|
10
|
-
import type { LiveStoreJSQuery } from './reactiveQueries/js.js'
|
|
10
|
+
import type { DbContext, DbGraph, GetResult, LiveQuery, LiveQueryAny } from './reactiveQueries/base-class.js'
|
|
11
|
+
// import type { LiveStoreJSQuery } from './reactiveQueries/js.js'
|
|
11
12
|
import { LiveStoreSQLQuery } from './reactiveQueries/sql.js'
|
|
12
13
|
import { SCHEMA_META_TABLE } from './schema/index.js'
|
|
13
|
-
import
|
|
14
|
-
|
|
14
|
+
import {
|
|
15
|
+
type DefaultSqliteTableDef,
|
|
16
|
+
getDefaultValuesEncoded,
|
|
17
|
+
type TableDef,
|
|
18
|
+
type TableOptions,
|
|
19
|
+
} from './schema/table-def.js'
|
|
20
|
+
import type { RefreshReason } from './store.js'
|
|
21
|
+
import type { UpdatePathDesc, UpdatePathDescCol, UpdatePathDescNone, UpdatePathDescRow } from './update-path.js'
|
|
22
|
+
import type { GetValForKey } from './utils/util.js'
|
|
15
23
|
import { prepareBindValues, sql } from './utils/util.js'
|
|
16
24
|
|
|
17
|
-
export type
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
25
|
+
export type RowQueryOptions = {
|
|
26
|
+
otelContext?: otel.Context
|
|
27
|
+
skipInsertDefaultRow?: boolean
|
|
28
|
+
dbGraph?: DbGraph
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type RowQueryOptionsDefaulValues<TTableDef extends TableDef> = {
|
|
32
|
+
defaultValues: Partial<RowResult<TTableDef>>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type MakeRowQuery = {
|
|
36
|
+
<TTableDef extends TableDef<DefaultSqliteTableDef, boolean, TableOptions & { isSingleton: true }>>(
|
|
37
|
+
table: TTableDef,
|
|
38
|
+
options?: RowQueryOptions,
|
|
39
|
+
): LiveQuery<RowResult<TTableDef>, UpdatePathDescRow<TTableDef>>
|
|
40
|
+
<TTableDef extends TableDef<DefaultSqliteTableDef, boolean, TableOptions & { isSingleton: false }>>(
|
|
41
|
+
table: TTableDef,
|
|
42
|
+
// TODO adjust so it works with arbitrary primary keys or unique constraints
|
|
43
|
+
id: string,
|
|
44
|
+
options?: RowQueryOptions & RowQueryOptionsDefaulValues<TTableDef>,
|
|
45
|
+
): LiveQuery<RowResult<TTableDef>, UpdatePathDescRow<TTableDef>>
|
|
46
|
+
}
|
|
35
47
|
|
|
36
48
|
// TODO also allow other where clauses and multiple rows
|
|
37
|
-
export const rowQuery = <TTableDef extends TableDef>(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const id
|
|
49
|
+
export const rowQuery: MakeRowQuery = <TTableDef extends TableDef>(
|
|
50
|
+
table: TTableDef,
|
|
51
|
+
idOrOptions?: string | RowQueryOptions,
|
|
52
|
+
options_?: RowQueryOptions & RowQueryOptionsDefaulValues<TTableDef>,
|
|
53
|
+
) => {
|
|
54
|
+
const id = typeof idOrOptions === 'string' ? idOrOptions : undefined
|
|
55
|
+
const options = typeof idOrOptions === 'string' ? options_ : idOrOptions
|
|
56
|
+
const defaultValues: Partial<RowResult<TTableDef>> | undefined = (options as any)?.defaultValues ?? {}
|
|
43
57
|
|
|
44
58
|
// Validate query args
|
|
45
59
|
if (table.options.isSingleton === true && id !== undefined) {
|
|
46
|
-
shouldNeverHappen(`Cannot query state table ${table.
|
|
60
|
+
shouldNeverHappen(`Cannot query state table ${table.sqliteDef.name} with id "${id}" as it is a singleton`)
|
|
47
61
|
} else if (table.options.isSingleton !== true && id === undefined) {
|
|
48
|
-
shouldNeverHappen(`Cannot query state table ${table.
|
|
62
|
+
shouldNeverHappen(`Cannot query state table ${table.sqliteDef.name} without id`)
|
|
49
63
|
}
|
|
50
64
|
|
|
51
|
-
const stateSchema = table.
|
|
65
|
+
const stateSchema = table.sqliteDef
|
|
52
66
|
const componentTableName = stateSchema.name
|
|
53
67
|
|
|
54
|
-
type TComponentState = SqliteDsl.FromColumns.RowDecoded<TTableDef['schema']['columns']>
|
|
55
|
-
|
|
56
|
-
// TODO find a better solution for this
|
|
57
|
-
if (store.tableRefs[componentTableName] === undefined) {
|
|
58
|
-
const schemaHash = SqliteAst.hash(stateSchema.ast)
|
|
59
|
-
const res = store.inMemoryDB.select<{ schemaHash: number }>(
|
|
60
|
-
sql`SELECT schemaHash FROM ${SCHEMA_META_TABLE} WHERE tableName = '${componentTableName}'`,
|
|
61
|
-
)
|
|
62
|
-
if (res.length === 0 || res[0]!.schemaHash !== schemaHash) {
|
|
63
|
-
migrateTable({
|
|
64
|
-
db: store._proxyDb,
|
|
65
|
-
tableAst: stateSchema.ast,
|
|
66
|
-
otelContext,
|
|
67
|
-
schemaHash,
|
|
68
|
-
})
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const label = `tableRef:${componentTableName}`
|
|
72
|
-
|
|
73
|
-
const existingTableRefFromGraph = Array.from(store.graph.atoms.values()).find(
|
|
74
|
-
(_) => _._tag === 'ref' && _.label === label,
|
|
75
|
-
) as Ref<null, DbContext, RefreshReason> | undefined
|
|
76
|
-
|
|
77
|
-
store.tableRefs[componentTableName] =
|
|
78
|
-
existingTableRefFromGraph ??
|
|
79
|
-
store.graph.makeRef(null, {
|
|
80
|
-
equal: () => false,
|
|
81
|
-
label,
|
|
82
|
-
meta: { liveStoreRefType: 'table' },
|
|
83
|
-
})
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (skipInsertDefaultRow !== true) {
|
|
87
|
-
// TODO find a way to only do this if necessary
|
|
88
|
-
insertRowWithDefaultValuesOrIgnore({
|
|
89
|
-
db: store._proxyDb,
|
|
90
|
-
id: id ?? 'singleton',
|
|
91
|
-
stateSchema,
|
|
92
|
-
otelContext,
|
|
93
|
-
defaultValues,
|
|
94
|
-
})
|
|
95
|
-
}
|
|
96
|
-
|
|
97
68
|
const whereClause = id === undefined ? '' : `where id = '${id}'`
|
|
98
69
|
const queryStr = sql`select * from ${componentTableName} ${whereClause} limit 1`
|
|
99
70
|
|
|
100
71
|
return new LiveStoreSQLQuery({
|
|
101
|
-
label: `
|
|
72
|
+
label: `rowQuery:query:${stateSchema.name}${id === undefined ? '' : `:${id}`}`,
|
|
102
73
|
genQueryString: queryStr,
|
|
103
74
|
queriedTables: new Set([componentTableName]),
|
|
104
|
-
dbGraph,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
75
|
+
dbGraph: options?.dbGraph,
|
|
76
|
+
execBeforeFirstRun: makeExecBeforeFirstRun({
|
|
77
|
+
otelContext: options?.otelContext,
|
|
78
|
+
table,
|
|
79
|
+
componentTableName,
|
|
80
|
+
defaultValues,
|
|
81
|
+
id,
|
|
82
|
+
skipInsertDefaultRow: options?.skipInsertDefaultRow,
|
|
83
|
+
}),
|
|
84
|
+
map: (results): RowResult<TTableDef> => {
|
|
85
|
+
if (results.length === 0) return shouldNeverHappen(`No results for query ${queryStr}`)
|
|
86
|
+
|
|
87
|
+
const componentStateEffectSchema = SqliteDsl.structSchemaForTable(stateSchema)
|
|
88
|
+
const parseResult = Schema.parseEither(componentStateEffectSchema)(results[0]!)
|
|
89
|
+
|
|
90
|
+
if (parseResult._tag === 'Left') {
|
|
91
|
+
console.error('decode error', TreeFormatter.formatError(parseResult.left), 'results', results)
|
|
92
|
+
return shouldNeverHappen(`Error decoding query result for ${queryStr}`)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return table.isSingleColumn === true ? parseResult.right.value : parseResult.right
|
|
96
|
+
},
|
|
97
|
+
updatePathDesc: { _tag: 'Row', table, id: id ?? 'singleton' },
|
|
98
|
+
})
|
|
118
99
|
}
|
|
119
100
|
|
|
120
|
-
type GetValForKey<T, K> = K extends keyof T ? T[K] : never
|
|
121
|
-
|
|
122
101
|
export type RowResult<TTableDef extends TableDef> = TTableDef['isSingleColumn'] extends true
|
|
123
|
-
? GetValForKey<SqliteDsl.FromColumns.RowDecoded<TTableDef['
|
|
124
|
-
: SqliteDsl.FromColumns.RowDecoded<TTableDef['
|
|
102
|
+
? GetValForKey<SqliteDsl.FromColumns.RowDecoded<TTableDef['sqliteDef']['columns']>, 'value'>
|
|
103
|
+
: SqliteDsl.FromColumns.RowDecoded<TTableDef['sqliteDef']['columns']>
|
|
125
104
|
|
|
126
105
|
export type RowResultEncoded<TTableDef extends TableDef> = TTableDef['isSingleColumn'] extends true
|
|
127
|
-
? GetValForKey<SqliteDsl.FromColumns.RowEncoded<TTableDef['
|
|
128
|
-
: SqliteDsl.FromColumns.RowEncoded<TTableDef['
|
|
129
|
-
|
|
130
|
-
export
|
|
131
|
-
|
|
132
|
-
|
|
106
|
+
? GetValForKey<SqliteDsl.FromColumns.RowEncoded<TTableDef['sqliteDef']['columns']>, 'value'>
|
|
107
|
+
: SqliteDsl.FromColumns.RowEncoded<TTableDef['sqliteDef']['columns']>
|
|
108
|
+
|
|
109
|
+
export const deriveColQuery: {
|
|
110
|
+
<TQuery extends LiveQuery<any, UpdatePathDescNone>, TCol extends keyof TQuery['result!'] & string>(
|
|
111
|
+
query$: TQuery,
|
|
112
|
+
colName: TCol,
|
|
113
|
+
): LiveQuery<TQuery['result!'][TCol], UpdatePathDescNone>
|
|
114
|
+
<TQuery extends LiveQuery<any, UpdatePathDescRow<any>>, TCol extends keyof TQuery['result!'] & string>(
|
|
115
|
+
query$: TQuery,
|
|
116
|
+
colName: TCol,
|
|
117
|
+
): LiveQuery<TQuery['result!'][TCol], UpdatePathDescCol<TQuery['updatePathDesc']['table'], TCol>>
|
|
118
|
+
} = (query$: LiveQueryAny, colName: string) => {
|
|
119
|
+
return computed((get) => get(query$)[colName], {
|
|
120
|
+
label: `deriveColQuery:${query$.label}:${colName}`,
|
|
121
|
+
updatePathDesc:
|
|
122
|
+
query$.updatePathDesc._tag === 'Row'
|
|
123
|
+
? { _tag: 'Col', table: query$.updatePathDesc.table, column: colName, id: query$.updatePathDesc.id }
|
|
124
|
+
: undefined,
|
|
125
|
+
}) as any
|
|
126
|
+
}
|
|
133
127
|
|
|
134
128
|
const insertRowWithDefaultValuesOrIgnore = ({
|
|
135
129
|
db,
|
|
136
130
|
id,
|
|
137
|
-
|
|
131
|
+
table,
|
|
138
132
|
otelContext,
|
|
139
133
|
defaultValues: explicitDefaultValues,
|
|
140
134
|
}: {
|
|
141
135
|
db: InMemoryDatabase
|
|
142
136
|
id: string
|
|
143
|
-
|
|
137
|
+
table: TableDef
|
|
144
138
|
otelContext: otel.Context
|
|
145
139
|
defaultValues: Partial<RowResult<TableDef>> | undefined
|
|
146
140
|
}) => {
|
|
147
|
-
const columnNames = Object.keys(
|
|
141
|
+
const columnNames = Object.keys(table.sqliteDef.columns)
|
|
148
142
|
const columnValues = columnNames.map((name) => `$${name}`).join(', ')
|
|
149
143
|
|
|
150
|
-
const tableName =
|
|
144
|
+
const tableName = table.sqliteDef.name
|
|
151
145
|
const insertQuery = sql`insert into ${tableName} (${columnNames.join(
|
|
152
146
|
', ',
|
|
153
147
|
)}) select ${columnValues} where not exists(select 1 from ${tableName} where id = '${id}')`
|
|
154
148
|
|
|
155
149
|
const defaultValues = pipe(
|
|
156
|
-
|
|
157
|
-
ReadonlyRecord.filter((_, key) => key !== 'id'),
|
|
158
|
-
ReadonlyRecord.map((column, columnName) =>
|
|
159
|
-
column.default._tag === 'None'
|
|
160
|
-
? column.nullable === true
|
|
161
|
-
? null
|
|
162
|
-
: shouldNeverHappen(`Column ${columnName} has no default value and is not nullable`)
|
|
163
|
-
: Schema.encodeSync(column.schema)(column.default.value),
|
|
164
|
-
),
|
|
150
|
+
getDefaultValuesEncoded(table),
|
|
165
151
|
ReadonlyRecord.map((val, columnName) => explicitDefaultValues?.[columnName] ?? val),
|
|
166
152
|
)
|
|
167
153
|
|
|
168
|
-
|
|
154
|
+
db.execute(insertQuery, prepareBindValues({ ...defaultValues, id }, insertQuery), [tableName], { otelContext })
|
|
169
155
|
}
|
|
156
|
+
|
|
157
|
+
const makeExecBeforeFirstRun =
|
|
158
|
+
({
|
|
159
|
+
id,
|
|
160
|
+
defaultValues,
|
|
161
|
+
skipInsertDefaultRow,
|
|
162
|
+
otelContext: otelContext_,
|
|
163
|
+
table,
|
|
164
|
+
componentTableName,
|
|
165
|
+
}: {
|
|
166
|
+
id?: string
|
|
167
|
+
defaultValues?: any
|
|
168
|
+
skipInsertDefaultRow?: boolean
|
|
169
|
+
otelContext?: otel.Context
|
|
170
|
+
componentTableName: string
|
|
171
|
+
table: TableDef
|
|
172
|
+
}) =>
|
|
173
|
+
({ store }: DbContext) => {
|
|
174
|
+
const otelContext = otelContext_ ?? store.otel.queriesSpanContext
|
|
175
|
+
|
|
176
|
+
// TODO find a better solution for this
|
|
177
|
+
if (store.tableRefs[componentTableName] === undefined) {
|
|
178
|
+
const schemaHash = SqliteAst.hash(table.sqliteDef.ast)
|
|
179
|
+
const res = store.inMemoryDB.select<{ schemaHash: number }>(
|
|
180
|
+
sql`SELECT schemaHash FROM ${SCHEMA_META_TABLE} WHERE tableName = '${componentTableName}'`,
|
|
181
|
+
)
|
|
182
|
+
if (res.length === 0 || res[0]!.schemaHash !== schemaHash) {
|
|
183
|
+
migrateTable({
|
|
184
|
+
db: store._proxyDb,
|
|
185
|
+
tableAst: table.sqliteDef.ast,
|
|
186
|
+
otelContext,
|
|
187
|
+
schemaHash,
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const label = `tableRef:${componentTableName}`
|
|
192
|
+
|
|
193
|
+
// TODO find a better implementation for this
|
|
194
|
+
const existingTableRefFromGraph = Array.from(store.graph.atoms.values()).find(
|
|
195
|
+
(_) => _._tag === 'ref' && _.label === label,
|
|
196
|
+
) as Ref<null, DbContext, RefreshReason> | undefined
|
|
197
|
+
|
|
198
|
+
store.tableRefs[componentTableName] = existingTableRefFromGraph ?? store.makeTableRef(componentTableName)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (skipInsertDefaultRow !== true) {
|
|
202
|
+
// TODO find a way to only do this if necessary
|
|
203
|
+
insertRowWithDefaultValuesOrIgnore({
|
|
204
|
+
db: store._proxyDb,
|
|
205
|
+
id: id ?? 'singleton',
|
|
206
|
+
table,
|
|
207
|
+
otelContext,
|
|
208
|
+
defaultValues,
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
}
|
package/src/schema/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { TableDef } from './table-def.js'
|
|
|
7
7
|
export * from './action.js'
|
|
8
8
|
export * from './system-tables.js'
|
|
9
9
|
export * as DbSchema from './table-def.js'
|
|
10
|
+
export * as ParseUtils from './parse-utils.js'
|
|
10
11
|
|
|
11
12
|
// export { SqliteDsl as DbSchema } from 'effect-db-schema'
|
|
12
13
|
|
|
@@ -36,11 +37,11 @@ export const makeSchema = <TInputSchema extends InputSchema>(
|
|
|
36
37
|
|
|
37
38
|
for (const tableDef of inputTables) {
|
|
38
39
|
// TODO validate tables (e.g. index names are unique)
|
|
39
|
-
tables.set(tableDef.
|
|
40
|
+
tables.set(tableDef.sqliteDef.ast.name, tableDef)
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
for (const tableDef of systemTables) {
|
|
43
|
-
tables.set(tableDef.
|
|
44
|
+
tables.set(tableDef.sqliteDef.name, tableDef)
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
return {
|
|
@@ -57,7 +58,7 @@ export const makeSchema = <TInputSchema extends InputSchema>(
|
|
|
57
58
|
*/
|
|
58
59
|
export type DbSchemaFromInputSchemaTables<TTables extends InputSchema['tables']> =
|
|
59
60
|
TTables extends ReadonlyArray<TableDef>
|
|
60
|
-
? { [K in TTables[number] as K['
|
|
61
|
+
? { [K in TTables[number] as K['sqliteDef']['name']]: K['sqliteDef'] }
|
|
61
62
|
: TTables extends Record<string, TableDef>
|
|
62
|
-
? { [K in keyof TTables as TTables[K]['
|
|
63
|
+
? { [K in keyof TTables as TTables[K]['sqliteDef']['name']]: TTables[K]['sqliteDef'] }
|
|
63
64
|
: never
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { shouldNeverHappen } from '@livestore/utils'
|
|
2
|
+
import type { ReadonlyArray } from '@livestore/utils/effect'
|
|
3
|
+
import { pipe, ReadonlyRecord, Schema, TreeFormatter } from '@livestore/utils/effect'
|
|
4
|
+
import { SqliteDsl as __SqliteDsl } from 'effect-db-schema'
|
|
5
|
+
|
|
6
|
+
import { type FromColumns, type FromTable, getDefaultValuesDecoded, type TableDef } from './table-def.js'
|
|
7
|
+
|
|
8
|
+
export const headUnsafe = <From, To>(schema: Schema.Schema<From, To>) =>
|
|
9
|
+
Schema.transform(
|
|
10
|
+
Schema.array(schema),
|
|
11
|
+
Schema.to(schema),
|
|
12
|
+
(rows) => rows[0]!,
|
|
13
|
+
(row) => [row],
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
// export const head = <From, To>(schema: Schema.Schema<From, To>) =>
|
|
17
|
+
// Schema.transform(
|
|
18
|
+
// Schema.array(schema),
|
|
19
|
+
// Schema.optionFromSelf(Schema.to(schema)),
|
|
20
|
+
// (rows) => Option.fromNullable(rows[0]),
|
|
21
|
+
// (row) => (row._tag === 'None' ? [] : [row.value]),
|
|
22
|
+
// )
|
|
23
|
+
|
|
24
|
+
export const headOr = <From, To>(schema: Schema.Schema<From, To>, fallback: To) =>
|
|
25
|
+
Schema.transform(
|
|
26
|
+
Schema.array(schema),
|
|
27
|
+
Schema.to(schema),
|
|
28
|
+
(rows) => rows[0] ?? fallback,
|
|
29
|
+
(row) => [row],
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
// export const pluck = <From extends {}, To, K extends keyof From & keyof To & string>(
|
|
33
|
+
// schema: Schema.Schema<From, To>,
|
|
34
|
+
// prop: K,
|
|
35
|
+
// ): Schema.Schema<From, To[K]> => {
|
|
36
|
+
// const toSchema = Schema.make(SchemaAST.getPropertySignatures(schema.ast).find((s) => s.name === prop)!.type) as any
|
|
37
|
+
// return Schema.transform(
|
|
38
|
+
// schema,
|
|
39
|
+
// toSchema,
|
|
40
|
+
// (row) => (row as any)[prop],
|
|
41
|
+
// (val) => ({ [prop]: val }) as any,
|
|
42
|
+
// )
|
|
43
|
+
// }
|
|
44
|
+
|
|
45
|
+
// export const schemaFor = <TTableDef extends TableDef>(
|
|
46
|
+
// table: TTableDef,
|
|
47
|
+
// ): Schema.Schema<FromTable.RowEncoded<TTableDef>, FromTable.RowDecoded<TTableDef>> =>
|
|
48
|
+
// SqliteDsl.structSchemaForTable(table.sqliteDef) as any
|
|
49
|
+
|
|
50
|
+
export const many = <TTableDef extends TableDef>(
|
|
51
|
+
table: TTableDef,
|
|
52
|
+
): ((rawRows: ReadonlyArray<any>) => ReadonlyArray<FromTable.RowDecoded<TTableDef>>) => {
|
|
53
|
+
return Schema.parseSync(Schema.array(table.schema)) as TODO
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const first =
|
|
57
|
+
<TTableDef extends TableDef>(
|
|
58
|
+
table: TTableDef,
|
|
59
|
+
fallback?: FromColumns.InsertRowDecoded<TTableDef['sqliteDef']['columns']>,
|
|
60
|
+
) =>
|
|
61
|
+
(rawRows: ReadonlyArray<any>) => {
|
|
62
|
+
const rows = Schema.parseSync(Schema.array(table.schema))(rawRows)
|
|
63
|
+
|
|
64
|
+
if (rows.length === 0) {
|
|
65
|
+
const schemaDefaultValues = getDefaultValuesDecoded(table)
|
|
66
|
+
|
|
67
|
+
const defaultValuesResult = pipe(
|
|
68
|
+
table.sqliteDef.columns,
|
|
69
|
+
ReadonlyRecord.map((_column, columnName) => (fallback as any)?.[columnName] ?? schemaDefaultValues[columnName]),
|
|
70
|
+
Schema.validateEither(table.schema),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if (defaultValuesResult._tag === 'Right') {
|
|
74
|
+
return defaultValuesResult.right
|
|
75
|
+
} else {
|
|
76
|
+
console.error('decode error', TreeFormatter.formatError(defaultValuesResult.left))
|
|
77
|
+
return shouldNeverHappen(
|
|
78
|
+
`Expected query (for table ${table.sqliteDef.name}) to return at least one result but found none. Also can't fallback to default values as some were not provided.`,
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return rows[0]!
|
|
84
|
+
}
|