@livestore/livestore 0.0.37 → 0.0.39-dev.2
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 +12 -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 +50 -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 +6 -0
- package/dist/schema/parse-utils.d.ts.map +1 -0
- package/dist/schema/parse-utils.js +59 -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 +18 -4
- package/src/reactiveQueries/graphql.ts +16 -14
- package/src/reactiveQueries/js.ts +36 -15
- package/src/reactiveQueries/sql.ts +77 -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 } 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,32 @@ 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
|
+
: typeof map === 'function'
|
|
98
|
+
? map
|
|
99
|
+
: (rows: any) => Schema.parseSync(map)(rows)
|
|
68
100
|
|
|
69
101
|
// TODO don't even create a thunk if query string is static
|
|
70
102
|
const queryString$ = this.dbGraph.makeThunk(
|
|
@@ -88,7 +120,7 @@ export class LiveStoreSQLQuery<Row> extends LiveStoreQueryBase<ReadonlyArray<Row
|
|
|
88
120
|
|
|
89
121
|
const queriedTablesRef = { current: queriedTables }
|
|
90
122
|
|
|
91
|
-
const results$ = this.dbGraph.makeThunk<
|
|
123
|
+
const results$ = this.dbGraph.makeThunk<Result>(
|
|
92
124
|
(get, setDebugInfo, { store, otelTracer, rootOtelContext }, otelContext) =>
|
|
93
125
|
otelTracer.startActiveSpan(
|
|
94
126
|
'sql:...', // NOTE span name will be overridden further down
|
|
@@ -97,6 +129,11 @@ export class LiveStoreSQLQuery<Row> extends LiveStoreQueryBase<ReadonlyArray<Row
|
|
|
97
129
|
(span) => {
|
|
98
130
|
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
99
131
|
|
|
132
|
+
if (this.execBeforeFirstRun !== undefined) {
|
|
133
|
+
this.execBeforeFirstRun({ store, otelTracer, rootOtelContext })
|
|
134
|
+
this.execBeforeFirstRun = undefined
|
|
135
|
+
}
|
|
136
|
+
|
|
100
137
|
const sqlString = get(queryString$, otelContext)
|
|
101
138
|
|
|
102
139
|
if (queriedTablesRef.current === undefined) {
|
|
@@ -112,13 +149,15 @@ export class LiveStoreSQLQuery<Row> extends LiveStoreQueryBase<ReadonlyArray<Row
|
|
|
112
149
|
span.setAttribute('sql.query', sqlString)
|
|
113
150
|
span.updateName(`sql:${sqlString.slice(0, 50)}`)
|
|
114
151
|
|
|
115
|
-
const
|
|
152
|
+
const rawResults = store.inMemoryDB.select<any>(sqlString, {
|
|
116
153
|
queriedTables,
|
|
117
154
|
bindValues: bindValues ? prepareBindValues(bindValues, sqlString) : undefined,
|
|
118
155
|
otelContext,
|
|
119
156
|
})
|
|
120
157
|
|
|
121
|
-
span.setAttribute('sql.rowsCount',
|
|
158
|
+
span.setAttribute('sql.rowsCount', rawResults.length)
|
|
159
|
+
|
|
160
|
+
const result = this.mapRows(rawResults)
|
|
122
161
|
|
|
123
162
|
span.end()
|
|
124
163
|
|
|
@@ -126,7 +165,7 @@ export class LiveStoreSQLQuery<Row> extends LiveStoreQueryBase<ReadonlyArray<Row
|
|
|
126
165
|
|
|
127
166
|
setDebugInfo({ _tag: 'sql', label, query: sqlString, durationMs })
|
|
128
167
|
|
|
129
|
-
return
|
|
168
|
+
return result
|
|
130
169
|
},
|
|
131
170
|
),
|
|
132
171
|
{ label: queryLabel },
|
|
@@ -139,33 +178,34 @@ export class LiveStoreSQLQuery<Row> extends LiveStoreQueryBase<ReadonlyArray<Row
|
|
|
139
178
|
* Returns a new reactive query that contains the result of
|
|
140
179
|
* running an arbitrary JS computation on the results of this SQL query.
|
|
141
180
|
*/
|
|
142
|
-
pipe = <U>(fn: (result:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
181
|
+
// pipe = <U>(fn: (result: Result, get: GetAtomResult) => U): LiveStoreJSQuery<U> =>
|
|
182
|
+
// new LiveStoreJSQuery({
|
|
183
|
+
// fn: (get) => {
|
|
184
|
+
// const results = get(this.results$!)
|
|
185
|
+
// return fn(results, get)
|
|
186
|
+
// },
|
|
187
|
+
// label: `${this.label}:js`,
|
|
188
|
+
// onDestroy: () => this.destroy(),
|
|
189
|
+
// dbGraph: this.dbGraph,
|
|
190
|
+
// updatePathDesc: undefined,
|
|
191
|
+
// })
|
|
152
192
|
|
|
153
193
|
/** Returns a reactive query */
|
|
154
|
-
getFirstRow = (args?: { defaultValue?:
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
194
|
+
// getFirstRow = (args?: { defaultValue?: Result }) =>
|
|
195
|
+
// new LiveStoreJSQuery({
|
|
196
|
+
// fn: (get) => {
|
|
197
|
+
// const results = get(this.results$!)
|
|
198
|
+
// if (results.length === 0 && args?.defaultValue === undefined) {
|
|
199
|
+
// // const queryLabel = this._tag === 'sql' ? this.queryString$!.computeResult(otelContext) : this.label
|
|
200
|
+
// const queryLabel = this.label
|
|
201
|
+
// return shouldNeverHappen(`Expected query ${queryLabel} to return at least one result`)
|
|
202
|
+
// }
|
|
203
|
+
// return results[0] ?? args!.defaultValue!
|
|
204
|
+
// },
|
|
205
|
+
// label: `${this.label}:first`,
|
|
206
|
+
// onDestroy: () => this.destroy(),
|
|
207
|
+
// dbGraph: this.dbGraph,
|
|
208
|
+
// })
|
|
169
209
|
|
|
170
210
|
destroy = () => {
|
|
171
211
|
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<ReadonlyArray<From>, ReadonlyArray<To>>) =>
|
|
9
|
+
// Schema.transform(
|
|
10
|
+
// schema,
|
|
11
|
+
// Schema.xxx(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
|
+
}
|