@livestore/common 0.0.47-dev.0 → 0.0.48-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/fixture.d.ts +68 -0
- package/dist/__tests__/fixture.d.ts.map +1 -0
- package/dist/__tests__/fixture.js +16 -0
- package/dist/__tests__/fixture.js.map +1 -0
- package/dist/adapter-types.d.ts +86 -0
- package/dist/adapter-types.d.ts.map +1 -0
- package/dist/adapter-types.js +2 -0
- package/dist/adapter-types.js.map +1 -0
- package/dist/derived-mutations.d.ts +107 -0
- package/dist/derived-mutations.d.ts.map +1 -0
- package/dist/derived-mutations.js +51 -0
- package/dist/derived-mutations.js.map +1 -0
- package/dist/derived-mutations.test.d.ts +2 -0
- package/dist/derived-mutations.test.d.ts.map +1 -0
- package/dist/derived-mutations.test.js +92 -0
- package/dist/derived-mutations.test.js.map +1 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/init-singleton-tables.d.ts +2 -2
- package/dist/init-singleton-tables.d.ts.map +1 -1
- package/dist/init-singleton-tables.js.map +1 -1
- package/dist/mutation.d.ts.map +1 -1
- package/dist/query-info.d.ts +47 -0
- package/dist/query-info.d.ts.map +1 -0
- package/dist/query-info.js +38 -0
- package/dist/query-info.js.map +1 -0
- package/dist/rehydrate-from-mutationlog.d.ts +6 -5
- package/dist/rehydrate-from-mutationlog.d.ts.map +1 -1
- package/dist/rehydrate-from-mutationlog.js +24 -7
- package/dist/rehydrate-from-mutationlog.js.map +1 -1
- package/dist/schema/index.d.ts +33 -23
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +31 -15
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/mutations.d.ts +16 -0
- package/dist/schema/mutations.d.ts.map +1 -1
- package/dist/schema/mutations.js +18 -8
- package/dist/schema/mutations.js.map +1 -1
- package/dist/schema/parse-utils.d.ts +14 -4
- package/dist/schema/parse-utils.d.ts.map +1 -1
- package/dist/schema/parse-utils.js +3 -3
- package/dist/schema/parse-utils.js.map +1 -1
- package/dist/schema/system-tables.d.ts +239 -67
- package/dist/schema/system-tables.d.ts.map +1 -1
- package/dist/schema/system-tables.js +24 -3
- package/dist/schema/system-tables.js.map +1 -1
- package/dist/schema/table-def.d.ts +53 -10
- package/dist/schema/table-def.d.ts.map +1 -1
- package/dist/schema/table-def.js +27 -12
- package/dist/schema/table-def.js.map +1 -1
- package/dist/schema-management/common.d.ts +5 -0
- package/dist/schema-management/common.d.ts.map +1 -0
- package/dist/schema-management/common.js +22 -0
- package/dist/schema-management/common.js.map +1 -0
- package/dist/schema-management/migrations.d.ts +18 -0
- package/dist/schema-management/migrations.d.ts.map +1 -0
- package/dist/{migrations.js → schema-management/migrations.js} +29 -36
- package/dist/schema-management/migrations.js.map +1 -0
- package/dist/schema-management/validate-mutation-defs.d.ts +16 -0
- package/dist/schema-management/validate-mutation-defs.d.ts.map +1 -0
- package/dist/schema-management/validate-mutation-defs.js +63 -0
- package/dist/schema-management/validate-mutation-defs.js.map +1 -0
- package/dist/sql-queries/sql-queries.d.ts +4 -4
- package/dist/sql-queries/sql-queries.d.ts.map +1 -1
- package/dist/sql-queries/sql-queries.js +7 -8
- package/dist/sql-queries/sql-queries.js.map +1 -1
- package/dist/sql-queries/sql-query-builder.d.ts +1 -1
- package/dist/sql-queries/sql-query-builder.d.ts.map +1 -1
- package/dist/sync/index.d.ts +2 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +2 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/sync.d.ts +25 -0
- package/dist/sync/sync.d.ts.map +1 -0
- package/dist/sync/sync.js +8 -0
- package/dist/sync/sync.js.map +1 -0
- package/package.json +6 -6
- package/src/__tests__/fixture.ts +23 -0
- package/src/adapter-types.ts +104 -0
- package/src/ambient.d.ts +3 -0
- package/src/derived-mutations.test.ts +100 -0
- package/src/derived-mutations.ts +126 -0
- package/src/index.ts +6 -2
- package/src/init-singleton-tables.ts +2 -2
- package/src/query-info.ts +104 -0
- package/src/rehydrate-from-mutationlog.ts +34 -20
- package/src/schema/index.ts +67 -39
- package/src/schema/mutations.ts +28 -9
- package/src/schema/parse-utils.ts +3 -3
- package/src/schema/system-tables.ts +44 -3
- package/src/schema/table-def.ts +64 -18
- package/src/schema-management/common.ts +29 -0
- package/src/{migrations.ts → schema-management/migrations.ts} +44 -57
- package/src/schema-management/validate-mutation-defs.ts +108 -0
- package/src/sql-queries/sql-queries.ts +8 -9
- package/src/sync/index.ts +1 -0
- package/src/sync/sync.ts +14 -0
- package/dist/database.d.ts +0 -50
- package/dist/database.d.ts.map +0 -1
- package/dist/database.js +0 -2
- package/dist/database.js.map +0 -1
- package/dist/migrations.d.ts +0 -16
- package/dist/migrations.d.ts.map +0 -1
- package/dist/migrations.js.map +0 -1
- package/src/database.ts +0 -60
|
@@ -8,7 +8,7 @@ import { type FromColumns, type FromTable, getDefaultValuesDecoded, type TableDe
|
|
|
8
8
|
export const many = <TTableDef extends TableDef>(
|
|
9
9
|
table: TTableDef,
|
|
10
10
|
): ((rawRows: ReadonlyArray<any>) => ReadonlyArray<FromTable.RowDecoded<TTableDef>>) => {
|
|
11
|
-
return Schema.decodeSync(Schema.
|
|
11
|
+
return Schema.decodeSync(Schema.Array(table.schema)) as TODO
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export const first =
|
|
@@ -17,7 +17,7 @@ export const first =
|
|
|
17
17
|
fallback?: FromColumns.InsertRowDecoded<TTableDef['sqliteDef']['columns']>,
|
|
18
18
|
) =>
|
|
19
19
|
(rawRows: ReadonlyArray<any>) => {
|
|
20
|
-
const rows = Schema.decodeSync(Schema.
|
|
20
|
+
const rows = Schema.decodeSync(Schema.Array(table.schema))(rawRows)
|
|
21
21
|
|
|
22
22
|
if (rows.length === 0) {
|
|
23
23
|
const schemaDefaultValues = getDefaultValuesDecoded(table)
|
|
@@ -31,7 +31,7 @@ export const first =
|
|
|
31
31
|
if (defaultValuesResult._tag === 'Right') {
|
|
32
32
|
return defaultValuesResult.right
|
|
33
33
|
} else {
|
|
34
|
-
console.error('decode error', TreeFormatter.
|
|
34
|
+
console.error('decode error', TreeFormatter.formatErrorSync(defaultValuesResult.left))
|
|
35
35
|
return shouldNeverHappen(
|
|
36
36
|
`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.`,
|
|
37
37
|
)
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
import { Schema } from '@livestore/utils/effect'
|
|
2
2
|
import { type SqliteAst as __SqliteAst, SqliteDsl } from 'effect-db-schema'
|
|
3
3
|
|
|
4
4
|
import type { FromTable } from './table-def.js'
|
|
5
5
|
import { table } from './table-def.js'
|
|
6
6
|
|
|
7
|
+
/// App DB
|
|
8
|
+
|
|
7
9
|
export const SCHEMA_META_TABLE = '__livestore_schema'
|
|
8
10
|
|
|
9
|
-
const schemaMetaTable = table(
|
|
11
|
+
export const schemaMetaTable = table(
|
|
10
12
|
SCHEMA_META_TABLE,
|
|
11
13
|
{
|
|
12
14
|
tableName: SqliteDsl.text({ primaryKey: true }),
|
|
@@ -19,4 +21,43 @@ const schemaMetaTable = table(
|
|
|
19
21
|
|
|
20
22
|
export type SchemaMetaRow = FromTable.RowDecoded<typeof schemaMetaTable>
|
|
21
23
|
|
|
22
|
-
export const
|
|
24
|
+
export const SCHEMA_MUTATIONS_META_TABLE = '__livestore_schema_mutations'
|
|
25
|
+
|
|
26
|
+
export const schemaMutationsMetaTable = table(
|
|
27
|
+
SCHEMA_MUTATIONS_META_TABLE,
|
|
28
|
+
{
|
|
29
|
+
mutationName: SqliteDsl.text({ primaryKey: true }),
|
|
30
|
+
schemaHash: SqliteDsl.integer({ nullable: false }),
|
|
31
|
+
/** ISO date format */
|
|
32
|
+
updatedAt: SqliteDsl.text({ nullable: false }),
|
|
33
|
+
},
|
|
34
|
+
{ disableAutomaticIdColumn: true },
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
export type SchemaMutationsMetaRow = FromTable.RowDecoded<typeof schemaMutationsMetaTable>
|
|
38
|
+
|
|
39
|
+
export const systemTables = [schemaMetaTable, schemaMutationsMetaTable]
|
|
40
|
+
|
|
41
|
+
/// Mutation log DB
|
|
42
|
+
|
|
43
|
+
export const SyncStatus = Schema.Literal('synced', 'pending', 'error')
|
|
44
|
+
export type SyncStatus = typeof SyncStatus.Type
|
|
45
|
+
|
|
46
|
+
export const MUTATION_LOG_META_TABLE = 'mutation_log'
|
|
47
|
+
|
|
48
|
+
export const mutationLogMetaTable = table(
|
|
49
|
+
MUTATION_LOG_META_TABLE,
|
|
50
|
+
{
|
|
51
|
+
// TODO add parent ids (see https://vlcn.io/blog/crdt-substrate)
|
|
52
|
+
id: SqliteDsl.text({ primaryKey: true }),
|
|
53
|
+
mutation: SqliteDsl.text({ nullable: false }),
|
|
54
|
+
argsJson: SqliteDsl.text({ nullable: false, schema: Schema.parseJson(Schema.Any) }),
|
|
55
|
+
schemaHash: SqliteDsl.integer({ nullable: false }),
|
|
56
|
+
/** ISO date format */
|
|
57
|
+
createdAt: SqliteDsl.text({ nullable: false }),
|
|
58
|
+
syncStatus: SqliteDsl.text({ schema: SyncStatus }),
|
|
59
|
+
},
|
|
60
|
+
{ disableAutomaticIdColumn: true },
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
export type MutationLogMetaRow = FromTable.RowDecoded<typeof mutationLogMetaTable>
|
package/src/schema/table-def.ts
CHANGED
|
@@ -3,6 +3,9 @@ import { pipe, ReadonlyRecord, Schema } from '@livestore/utils/effect'
|
|
|
3
3
|
import type { Nullable, PrettifyFlat } from 'effect-db-schema'
|
|
4
4
|
import { SqliteDsl } from 'effect-db-schema'
|
|
5
5
|
|
|
6
|
+
import type { DerivedMutationHelperFns } from '../derived-mutations.js'
|
|
7
|
+
import { makeDerivedMutationDefsForTable } from '../derived-mutations.js'
|
|
8
|
+
|
|
6
9
|
export const { blob, boolean, column, datetime, integer, isColumnDefinition, json, real, text } = SqliteDsl
|
|
7
10
|
|
|
8
11
|
export { type SqliteDsl } from 'effect-db-schema'
|
|
@@ -53,26 +56,42 @@ export type TableDef<
|
|
|
53
56
|
>,
|
|
54
57
|
> = {
|
|
55
58
|
sqliteDef: TSqliteDef
|
|
59
|
+
// TODO move this into options (for now it's duplicated)
|
|
56
60
|
isSingleColumn: TIsSingleColumn
|
|
57
61
|
options: TOptions
|
|
58
62
|
schema: TSchema
|
|
59
|
-
}
|
|
63
|
+
} & (TOptions['deriveMutations'] extends true ? DerivedMutationHelperFns<TSqliteDef['columns'], TOptions> : {})
|
|
60
64
|
|
|
61
|
-
export type TableOptionsInput = Partial<TableOptions & { indexes: SqliteDsl.Index[] }>
|
|
65
|
+
export type TableOptionsInput = Partial<Omit<TableOptions, 'isSingleColumn'> & { indexes: SqliteDsl.Index[] }>
|
|
62
66
|
|
|
63
67
|
export type TableOptions = {
|
|
64
68
|
/**
|
|
65
69
|
* Setting this to true will have the following consequences:
|
|
66
70
|
* - An `id` column will be added with `primaryKey: true` and `"singleton"` as default value and only allowed value
|
|
67
|
-
* - LiveStore will automatically create the singleton row when
|
|
71
|
+
* - LiveStore will automatically create the singleton row when booting up
|
|
68
72
|
* - LiveStore will fail if there is already a column defined with `primaryKey: true`
|
|
69
73
|
*
|
|
70
74
|
* @default false
|
|
71
75
|
*/
|
|
72
76
|
isSingleton: boolean
|
|
73
|
-
// TODO
|
|
77
|
+
// TODO remove
|
|
74
78
|
dynamicRegistration: boolean
|
|
75
79
|
disableAutomaticIdColumn: boolean
|
|
80
|
+
/**
|
|
81
|
+
* Setting this to true will automatically derive insert, update and delete mutations for this table. Example:
|
|
82
|
+
*
|
|
83
|
+
* ```ts
|
|
84
|
+
* const todos = table('todos', { ... }, { deriveMutations: true })
|
|
85
|
+
* todos.insert({ id: '1', text: 'Hello' })
|
|
86
|
+
* ```
|
|
87
|
+
*
|
|
88
|
+
* This is also a prerequisite for using the `useRow`, `useAtom` and `rowQuery` APIs.
|
|
89
|
+
*
|
|
90
|
+
* Important: When using this option, make sure you're following the "Rules of mutations" for the table schema.
|
|
91
|
+
*/
|
|
92
|
+
deriveMutations: boolean
|
|
93
|
+
/** Derived based on whether the table definition has one or more columns (besides the `id` column) */
|
|
94
|
+
isSingleColumn: boolean
|
|
76
95
|
}
|
|
77
96
|
|
|
78
97
|
export const table = <
|
|
@@ -82,17 +101,19 @@ export const table = <
|
|
|
82
101
|
>(
|
|
83
102
|
name: TName,
|
|
84
103
|
columnOrColumns: TColumns,
|
|
85
|
-
// type?: TStateType,
|
|
86
104
|
options?: TOptionsInput,
|
|
87
105
|
): TableDef<
|
|
88
106
|
SqliteDsl.TableDefinition<
|
|
89
107
|
TName,
|
|
90
108
|
PrettifyFlat<
|
|
91
|
-
WithId<
|
|
109
|
+
WithId<
|
|
110
|
+
TColumns extends SqliteDsl.Columns ? TColumns : { value: TColumns },
|
|
111
|
+
WithDefaults<TOptionsInput, SqliteDsl.IsSingleColumn<TColumns>>
|
|
112
|
+
>
|
|
92
113
|
>
|
|
93
114
|
>,
|
|
94
|
-
|
|
95
|
-
WithDefaults<TOptionsInput
|
|
115
|
+
SqliteDsl.IsSingleColumn<TColumns>,
|
|
116
|
+
WithDefaults<TOptionsInput, SqliteDsl.IsSingleColumn<TColumns>>
|
|
96
117
|
> => {
|
|
97
118
|
const tablePath = name
|
|
98
119
|
|
|
@@ -100,6 +121,8 @@ export const table = <
|
|
|
100
121
|
isSingleton: options?.isSingleton ?? false,
|
|
101
122
|
dynamicRegistration: options?.dynamicRegistration ?? false,
|
|
102
123
|
disableAutomaticIdColumn: options?.disableAutomaticIdColumn ?? false,
|
|
124
|
+
deriveMutations: options?.deriveMutations ?? false,
|
|
125
|
+
isSingleColumn: SqliteDsl.isColumnDefinition(columnOrColumns) === true,
|
|
103
126
|
}
|
|
104
127
|
|
|
105
128
|
const columns = (
|
|
@@ -114,7 +137,7 @@ export const table = <
|
|
|
114
137
|
}
|
|
115
138
|
} else if (columns.id === undefined && ReadonlyRecord.some(columns, (_) => _.primaryKey === true) === false) {
|
|
116
139
|
if (options_.isSingleton) {
|
|
117
|
-
columns.id = SqliteDsl.text({ schema: Schema.
|
|
140
|
+
columns.id = SqliteDsl.text({ schema: Schema.Literal('singleton'), primaryKey: true, default: 'singleton' })
|
|
118
141
|
} else {
|
|
119
142
|
columns.id = SqliteDsl.text({ primaryKey: true })
|
|
120
143
|
}
|
|
@@ -122,6 +145,7 @@ export const table = <
|
|
|
122
145
|
|
|
123
146
|
const sqliteDef = SqliteDsl.table(tablePath, columns, options?.indexes ?? [])
|
|
124
147
|
|
|
148
|
+
// TODO also enforce this on the type level
|
|
125
149
|
if (options_.isSingleton) {
|
|
126
150
|
for (const column of sqliteDef.ast.columns) {
|
|
127
151
|
if (column.nullable === false && column.default._tag === 'None') {
|
|
@@ -137,18 +161,38 @@ export const table = <
|
|
|
137
161
|
const schema = SqliteDsl.structSchemaForTable(sqliteDef)
|
|
138
162
|
const tableDef = { sqliteDef, isSingleColumn, options: options_, schema } satisfies TableDef
|
|
139
163
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
164
|
+
if (tableHasDerivedMutations(tableDef)) {
|
|
165
|
+
const derivedMutationDefs = makeDerivedMutationDefsForTable(tableDef)
|
|
166
|
+
|
|
167
|
+
tableDef.insert = (valuesOrValue: any) => {
|
|
168
|
+
if (isSingleColumn && options_.isSingleton) {
|
|
169
|
+
return derivedMutationDefs.insert({ id: 'singleton', value: { value: valuesOrValue } })
|
|
170
|
+
} else {
|
|
171
|
+
return derivedMutationDefs.insert(valuesOrValue as any)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
tableDef.update = (argsOrValues: any) => {
|
|
176
|
+
if (isSingleColumn && options_.isSingleton) {
|
|
177
|
+
return derivedMutationDefs.update({ where: { id: 'singleton' }, values: { value: argsOrValues } as any })
|
|
178
|
+
} else {
|
|
179
|
+
return derivedMutationDefs.update(argsOrValues as any)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
tableDef.delete = (args: any) => derivedMutationDefs.delete(args)
|
|
184
|
+
}
|
|
148
185
|
|
|
149
186
|
return tableDef as any
|
|
150
187
|
}
|
|
151
188
|
|
|
189
|
+
export const tableHasDerivedMutations = <TTableDef extends TableDef>(
|
|
190
|
+
tableDef: TTableDef,
|
|
191
|
+
): tableDef is TTableDef & {
|
|
192
|
+
options: { deriveMutations: true }
|
|
193
|
+
} & DerivedMutationHelperFns<TTableDef['sqliteDef']['columns'], TTableDef['options']> =>
|
|
194
|
+
tableDef.options.deriveMutations === true
|
|
195
|
+
|
|
152
196
|
export const tableIsSingleton = <TTableDef extends TableDef>(
|
|
153
197
|
tableDef: TTableDef,
|
|
154
198
|
): tableDef is TTableDef & { options: { isSingleton: true } } => tableDef.options.isSingleton === true
|
|
@@ -210,10 +254,12 @@ type WithId<TColumns extends SqliteDsl.Columns, TOptions extends TableOptions> =
|
|
|
210
254
|
id: SqliteDsl.ColumnDefinition<string, string>
|
|
211
255
|
})
|
|
212
256
|
|
|
213
|
-
type WithDefaults<TOptionsInput extends TableOptionsInput> = {
|
|
257
|
+
type WithDefaults<TOptionsInput extends TableOptionsInput, TIsSingleColumn extends boolean> = {
|
|
214
258
|
isSingleton: TOptionsInput['isSingleton'] extends true ? true : false
|
|
215
259
|
dynamicRegistration: TOptionsInput['dynamicRegistration'] extends true ? true : false
|
|
216
260
|
disableAutomaticIdColumn: TOptionsInput['disableAutomaticIdColumn'] extends true ? true : false
|
|
261
|
+
deriveMutations: TOptionsInput['deriveMutations'] extends true ? true : false
|
|
262
|
+
isSingleColumn: TIsSingleColumn
|
|
217
263
|
}
|
|
218
264
|
|
|
219
265
|
export namespace FromTable {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { InMemoryDatabase } from '../adapter-types.js'
|
|
2
|
+
import type { ParamsObject } from '../util.js'
|
|
3
|
+
import { prepareBindValues } from '../util.js'
|
|
4
|
+
|
|
5
|
+
// TODO bring back statement caching
|
|
6
|
+
// will require proper scope-aware cleanup etc (for testing and apps with multiple LiveStore instances)
|
|
7
|
+
// const cachedStmts = new Map<string, PreparedStatement>()
|
|
8
|
+
|
|
9
|
+
export const dbExecute = (db: InMemoryDatabase, queryStr: string, bindValues?: ParamsObject) => {
|
|
10
|
+
// let stmt = cachedStmts.get(queryStr)
|
|
11
|
+
// if (!stmt) {
|
|
12
|
+
const stmt = db.prepare(queryStr)
|
|
13
|
+
// cachedStmts.set(queryStr, stmt)
|
|
14
|
+
// }
|
|
15
|
+
|
|
16
|
+
const preparedBindValues = bindValues ? prepareBindValues(bindValues, queryStr) : undefined
|
|
17
|
+
|
|
18
|
+
stmt.execute(preparedBindValues)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const dbSelect = <T>(db: InMemoryDatabase, queryStr: string, bindValues?: ParamsObject) => {
|
|
22
|
+
// let stmt = cachedStmts.get(queryStr)
|
|
23
|
+
// if (!stmt) {
|
|
24
|
+
const stmt = db.prepare(queryStr)
|
|
25
|
+
// cachedStmts.set(queryStr, stmt)
|
|
26
|
+
// }
|
|
27
|
+
|
|
28
|
+
return stmt.select<T>(bindValues ? prepareBindValues(bindValues, queryStr) : undefined)
|
|
29
|
+
}
|
|
@@ -1,58 +1,36 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { memoizeByStringifyArgs } from '@livestore/utils'
|
|
2
2
|
import { Schema as EffectSchema } from '@livestore/utils/effect'
|
|
3
|
-
import
|
|
3
|
+
import * as otel from '@opentelemetry/api'
|
|
4
4
|
import { SqliteAst, SqliteDsl } from 'effect-db-schema'
|
|
5
5
|
|
|
6
|
-
import type {
|
|
7
|
-
import type { LiveStoreSchema } from '
|
|
8
|
-
import type { SchemaMetaRow } from '
|
|
9
|
-
import { SCHEMA_META_TABLE, systemTables } from '
|
|
10
|
-
import
|
|
11
|
-
import {
|
|
6
|
+
import type { InMemoryDatabase } from '../adapter-types.js'
|
|
7
|
+
import type { LiveStoreSchema } from '../schema/index.js'
|
|
8
|
+
import type { SchemaMetaRow } from '../schema/system-tables.js'
|
|
9
|
+
import { SCHEMA_META_TABLE, schemaMetaTable, systemTables } from '../schema/system-tables.js'
|
|
10
|
+
import { sql } from '../util.js'
|
|
11
|
+
import { dbExecute, dbSelect } from './common.js'
|
|
12
|
+
import { makeSchemaManager, validateSchema } from './validate-mutation-defs.js'
|
|
12
13
|
|
|
13
|
-
const getMemoizedTimestamp =
|
|
14
|
-
|
|
15
|
-
// TODO bring back statement caching
|
|
16
|
-
// will require proper scope-aware cleanup etc (for testing and apps with multiple LiveStore instances)
|
|
17
|
-
// const cachedStmts = new Map<string, PreparedStatement>()
|
|
18
|
-
|
|
19
|
-
const dbExecute = (db: MainDatabase, queryStr: string, bindValues?: ParamsObject) => {
|
|
20
|
-
// let stmt = cachedStmts.get(queryStr)
|
|
21
|
-
// if (!stmt) {
|
|
22
|
-
const stmt = db.prepare(queryStr)
|
|
23
|
-
// cachedStmts.set(queryStr, stmt)
|
|
24
|
-
// }
|
|
25
|
-
|
|
26
|
-
const preparedBindValues = bindValues ? prepareBindValues(bindValues, queryStr) : undefined
|
|
27
|
-
|
|
28
|
-
stmt.execute(preparedBindValues)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const dbSelect = <T>(db: MainDatabase, queryStr: string, bindValues?: ParamsObject) => {
|
|
32
|
-
// let stmt = cachedStmts.get(queryStr)
|
|
33
|
-
// if (!stmt) {
|
|
34
|
-
const stmt = db.prepare(queryStr)
|
|
35
|
-
// cachedStmts.set(queryStr, stmt)
|
|
36
|
-
// }
|
|
37
|
-
|
|
38
|
-
return stmt.select<T>(bindValues ? prepareBindValues(bindValues, queryStr) : undefined)
|
|
39
|
-
}
|
|
14
|
+
const getMemoizedTimestamp = memoizeByStringifyArgs(() => new Date().toISOString())
|
|
40
15
|
|
|
41
16
|
// TODO more graceful DB migration (e.g. backup DB before destructive migrations)
|
|
42
17
|
export const migrateDb = ({
|
|
43
18
|
db,
|
|
44
|
-
otelContext,
|
|
19
|
+
otelContext = otel.context.active(),
|
|
45
20
|
schema,
|
|
46
21
|
}: {
|
|
47
|
-
db:
|
|
48
|
-
otelContext
|
|
22
|
+
db: InMemoryDatabase
|
|
23
|
+
otelContext?: otel.Context
|
|
49
24
|
schema: LiveStoreSchema
|
|
50
25
|
}) => {
|
|
51
|
-
|
|
26
|
+
migrateTable({
|
|
52
27
|
db,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
28
|
+
otelContext,
|
|
29
|
+
tableAst: schemaMetaTable.sqliteDef.ast,
|
|
30
|
+
behaviour: 'create-if-not-exists',
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
validateSchema(schema, makeSchemaManager(db))
|
|
56
34
|
|
|
57
35
|
const schemaMetaRows = dbSelect<SchemaMetaRow>(db, sql`SELECT * FROM ${SCHEMA_META_TABLE}`)
|
|
58
36
|
|
|
@@ -72,7 +50,6 @@ export const migrateDb = ({
|
|
|
72
50
|
const dbSchemaHash = dbSchemaHashByTable[tableName]
|
|
73
51
|
const schemaHash = SqliteAst.hash(tableAst)
|
|
74
52
|
|
|
75
|
-
// @ts-expect-error TODO fix typing
|
|
76
53
|
const skipMigrations = import.meta.env.VITE_LIVESTORE_SKIP_MIGRATIONS !== undefined
|
|
77
54
|
|
|
78
55
|
if (schemaHash !== dbSchemaHash && skipMigrations === false) {
|
|
@@ -80,7 +57,7 @@ export const migrateDb = ({
|
|
|
80
57
|
`Schema hash mismatch for table '${tableName}' (DB: ${dbSchemaHash}, expected: ${schemaHash}), migrating table...`,
|
|
81
58
|
)
|
|
82
59
|
|
|
83
|
-
migrateTable({ db, tableAst, otelContext, schemaHash })
|
|
60
|
+
migrateTable({ db, tableAst, otelContext, schemaHash, behaviour: 'drop-and-recreate' })
|
|
84
61
|
}
|
|
85
62
|
}
|
|
86
63
|
}
|
|
@@ -89,35 +66,45 @@ export const migrateTable = ({
|
|
|
89
66
|
db,
|
|
90
67
|
tableAst,
|
|
91
68
|
// otelContext,
|
|
92
|
-
schemaHash,
|
|
69
|
+
schemaHash = SqliteAst.hash(tableAst),
|
|
70
|
+
behaviour,
|
|
71
|
+
skipMetaTable = false,
|
|
93
72
|
}: {
|
|
94
|
-
db:
|
|
73
|
+
db: InMemoryDatabase
|
|
95
74
|
tableAst: SqliteAst.Table
|
|
96
|
-
otelContext
|
|
97
|
-
schemaHash
|
|
75
|
+
otelContext?: otel.Context
|
|
76
|
+
schemaHash?: number
|
|
77
|
+
behaviour: 'drop-and-recreate' | 'create-if-not-exists'
|
|
78
|
+
skipMetaTable?: boolean
|
|
98
79
|
}) => {
|
|
99
80
|
console.log(`Migrating table '${tableAst.name}'...`)
|
|
100
81
|
const tableName = tableAst.name
|
|
101
82
|
const columnSpec = makeColumnSpec(tableAst)
|
|
102
83
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
84
|
+
if (behaviour === 'drop-and-recreate') {
|
|
85
|
+
// TODO need to possibly handle cascading deletes due to foreign keys
|
|
86
|
+
dbExecute(db, sql`drop table if exists ${tableName}`)
|
|
87
|
+
dbExecute(db, sql`create table if not exists ${tableName} (${columnSpec}) strict`)
|
|
88
|
+
} else if (behaviour === 'create-if-not-exists') {
|
|
89
|
+
dbExecute(db, sql`create table if not exists ${tableName} (${columnSpec}) strict`)
|
|
90
|
+
}
|
|
106
91
|
|
|
107
92
|
for (const index of tableAst.indexes) {
|
|
108
93
|
dbExecute(db, createIndexFromDefinition(tableName, index))
|
|
109
94
|
}
|
|
110
95
|
|
|
111
|
-
|
|
96
|
+
if (skipMetaTable !== true) {
|
|
97
|
+
const updatedAt = getMemoizedTimestamp()
|
|
112
98
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
99
|
+
dbExecute(
|
|
100
|
+
db,
|
|
101
|
+
sql`
|
|
116
102
|
INSERT INTO ${SCHEMA_META_TABLE} (tableName, schemaHash, updatedAt) VALUES ($tableName, $schemaHash, $updatedAt)
|
|
117
103
|
ON CONFLICT (tableName) DO UPDATE SET schemaHash = $schemaHash, updatedAt = $updatedAt;
|
|
118
104
|
`,
|
|
119
|
-
|
|
120
|
-
|
|
105
|
+
{ tableName, schemaHash, updatedAt },
|
|
106
|
+
)
|
|
107
|
+
}
|
|
121
108
|
}
|
|
122
109
|
|
|
123
110
|
const createIndexFromDefinition = (tableName: string, index: SqliteAst.Index) => {
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { shouldNeverHappen } from '@livestore/utils'
|
|
2
|
+
import { Schema } from '@livestore/utils/effect'
|
|
3
|
+
import * as otel from '@opentelemetry/api'
|
|
4
|
+
|
|
5
|
+
import type { InMemoryDatabase } from '../adapter-types.js'
|
|
6
|
+
import type { LiveStoreSchema } from '../schema/index.js'
|
|
7
|
+
import type { MutationDef } from '../schema/mutations.js'
|
|
8
|
+
import type { SchemaMutationsMetaRow } from '../schema/system-tables.js'
|
|
9
|
+
import { SCHEMA_MUTATIONS_META_TABLE, schemaMutationsMetaTable } from '../schema/system-tables.js'
|
|
10
|
+
import { sql } from '../util.js'
|
|
11
|
+
import { dbExecute, dbSelect } from './common.js'
|
|
12
|
+
import { migrateTable } from './migrations.js'
|
|
13
|
+
|
|
14
|
+
export const validateSchema = (schema: LiveStoreSchema, schemaManager: SchemaManager) => {
|
|
15
|
+
// Validate mutation definitions
|
|
16
|
+
const registeredMutationDefInfos = schemaManager.getMutationDefInfos()
|
|
17
|
+
|
|
18
|
+
const missingMutationDefs = registeredMutationDefInfos.filter(
|
|
19
|
+
(registeredMutationDefInfo) => !schema.mutations.has(registeredMutationDefInfo.mutationName),
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if (missingMutationDefs.length > 0) {
|
|
23
|
+
shouldNeverHappen(
|
|
24
|
+
`Missing mutation definitions: ${missingMutationDefs.map((info) => info.mutationName).join(', ')}`,
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (const [, mutationDef] of schema.mutations) {
|
|
29
|
+
const registeredMutationDefInfo = registeredMutationDefInfos.find((info) => info.mutationName === mutationDef.name)
|
|
30
|
+
|
|
31
|
+
validateMutationDef(mutationDef, schemaManager, registeredMutationDefInfo)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Validate table schemas
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const validateMutationDef = (
|
|
38
|
+
mutationDef: MutationDef.Any,
|
|
39
|
+
schemaManager: SchemaManager,
|
|
40
|
+
registeredMutationDefInfo: MutationDefInfo | undefined,
|
|
41
|
+
) => {
|
|
42
|
+
const schemaHash = Schema.hash(mutationDef.schema)
|
|
43
|
+
|
|
44
|
+
if (registeredMutationDefInfo === undefined) {
|
|
45
|
+
schemaManager.setMutationDefInfo({
|
|
46
|
+
schemaHash,
|
|
47
|
+
mutationName: mutationDef.name,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (schemaHash === registeredMutationDefInfo.schemaHash) return
|
|
54
|
+
|
|
55
|
+
// TODO bring back some form of schema compatibility check (see https://github.com/livestorejs/livestore/issues/69)
|
|
56
|
+
// const newSchemaIsCompatibleWithOldSchema = Schema.isSubType(jsonSchemaDefFromMgmtStore, mutationDef.schema)
|
|
57
|
+
|
|
58
|
+
// if (!newSchemaIsCompatibleWithOldSchema) {
|
|
59
|
+
// shouldNeverHappen(`Schema for mutation ${mutationDef.name} has changed in an incompatible way`)
|
|
60
|
+
// }
|
|
61
|
+
|
|
62
|
+
schemaManager.setMutationDefInfo({
|
|
63
|
+
schemaHash,
|
|
64
|
+
mutationName: mutationDef.name,
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface SchemaManager {
|
|
69
|
+
getMutationDefInfos: () => ReadonlyArray<MutationDefInfo>
|
|
70
|
+
|
|
71
|
+
setMutationDefInfo: (mutationDefInfo: MutationDefInfo) => void
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type MutationDefInfo = {
|
|
75
|
+
mutationName: string
|
|
76
|
+
schemaHash: number
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const makeSchemaManager = (db: InMemoryDatabase): SchemaManager => {
|
|
80
|
+
migrateTable({
|
|
81
|
+
db,
|
|
82
|
+
otelContext: otel.context.active(),
|
|
83
|
+
tableAst: schemaMutationsMetaTable.sqliteDef.ast,
|
|
84
|
+
behaviour: 'create-if-not-exists',
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
getMutationDefInfos: () => {
|
|
89
|
+
const schemaMutationsMetaRows = dbSelect<SchemaMutationsMetaRow>(
|
|
90
|
+
db,
|
|
91
|
+
sql`SELECT * FROM ${SCHEMA_MUTATIONS_META_TABLE}`,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return schemaMutationsMetaRows
|
|
95
|
+
},
|
|
96
|
+
setMutationDefInfo: (info) => {
|
|
97
|
+
dbExecute(
|
|
98
|
+
db,
|
|
99
|
+
sql`INSERT OR REPLACE INTO ${SCHEMA_MUTATIONS_META_TABLE} (mutationName, schemaHash, updatedAt) VALUES ($mutationName, $schemaHash, $updatedAt)`,
|
|
100
|
+
{
|
|
101
|
+
mutationName: info.mutationName,
|
|
102
|
+
schemaHash: info.schemaHash,
|
|
103
|
+
updatedAt: new Date().toISOString(),
|
|
104
|
+
},
|
|
105
|
+
)
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { shouldNeverHappen } from '@livestore/utils'
|
|
1
2
|
import { pipe, ReadonlyArray, Schema, TreeFormatter } from '@livestore/utils/effect'
|
|
2
3
|
import type { SqliteDsl } from 'effect-db-schema'
|
|
3
4
|
|
|
@@ -55,7 +56,7 @@ export const insertRow = <TColumns extends SqliteDsl.Columns>({
|
|
|
55
56
|
tableName: string
|
|
56
57
|
columns: TColumns
|
|
57
58
|
values: ClientTypes.DecodedValuesForColumns<TColumns>
|
|
58
|
-
options
|
|
59
|
+
options?: { orReplace: boolean }
|
|
59
60
|
}): [string, BindValues] => {
|
|
60
61
|
const keysStr = Object.keys(values).join(', ')
|
|
61
62
|
const valuesStr = Object.keys(values)
|
|
@@ -63,7 +64,7 @@ export const insertRow = <TColumns extends SqliteDsl.Columns>({
|
|
|
63
64
|
.join(', ')
|
|
64
65
|
|
|
65
66
|
return [
|
|
66
|
-
sql`INSERT ${options.orReplace ? 'OR REPLACE' : ''}
|
|
67
|
+
sql`INSERT ${options.orReplace ? 'OR REPLACE ' : ''}INTO ${tableName} (${keysStr}) VALUES (${valuesStr})`,
|
|
67
68
|
makeBindValues({ columns, values }),
|
|
68
69
|
]
|
|
69
70
|
}
|
|
@@ -263,7 +264,7 @@ export const makeBindValues = <TColumns extends SqliteDsl.Columns, TKeys extends
|
|
|
263
264
|
if (columnDef.nullable === true && (value === null || value === undefined)) return null
|
|
264
265
|
const res = Schema.encodeEither(columnDef.schema)(value)
|
|
265
266
|
if (res._tag === 'Left') {
|
|
266
|
-
const parseErrorStr = TreeFormatter.
|
|
267
|
+
const parseErrorStr = TreeFormatter.formatErrorSync(res.left)
|
|
267
268
|
const expectedSchemaStr = String(columnDef.schema.ast)
|
|
268
269
|
|
|
269
270
|
console.error(
|
|
@@ -292,26 +293,24 @@ Value:`,
|
|
|
292
293
|
// NOTE null/undefined values are handled via explicit SQL syntax and don't need to be provided as bind values
|
|
293
294
|
.filter(([, value]) => skipNil !== true || (value !== null && value !== undefined))
|
|
294
295
|
.flatMap(([columnName, value]: [string, any]) => {
|
|
296
|
+
const codec = codecMap[columnName] ?? shouldNeverHappen(`No codec found for column "${columnName}"`)
|
|
295
297
|
// remap complex where-values with `op`
|
|
296
298
|
if (typeof value === 'object' && value !== null && 'op' in value) {
|
|
297
299
|
switch (value.op) {
|
|
298
300
|
case 'in': {
|
|
299
|
-
return value.val.map((value: any, i: number) => [
|
|
300
|
-
`${variablePrefix}${columnName}_${i}`,
|
|
301
|
-
codecMap[columnName]!(value),
|
|
302
|
-
])
|
|
301
|
+
return value.val.map((value: any, i: number) => [`${variablePrefix}${columnName}_${i}`, codec(value)])
|
|
303
302
|
}
|
|
304
303
|
case '=':
|
|
305
304
|
case '>':
|
|
306
305
|
case '<': {
|
|
307
|
-
return [[`${variablePrefix}${columnName}`,
|
|
306
|
+
return [[`${variablePrefix}${columnName}`, codec(value.val)]]
|
|
308
307
|
}
|
|
309
308
|
default: {
|
|
310
309
|
throw new Error(`Unknown op: ${value.op}`)
|
|
311
310
|
}
|
|
312
311
|
}
|
|
313
312
|
} else {
|
|
314
|
-
return [[`${variablePrefix}${columnName}`,
|
|
313
|
+
return [[`${variablePrefix}${columnName}`, codec(value)]]
|
|
315
314
|
}
|
|
316
315
|
}),
|
|
317
316
|
Object.fromEntries,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './sync.js'
|
package/src/sync/sync.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Effect, Schema, type Stream, type SubscriptionRef } from '@livestore/utils/effect'
|
|
2
|
+
|
|
3
|
+
import type { MutationEvent } from '../schema/mutations.js'
|
|
4
|
+
|
|
5
|
+
export type SyncImpl = {
|
|
6
|
+
pull: (cursor: string | undefined) => Stream.Stream<MutationEvent.AnyEncoded, IsOfflineError | InvalidPullError>
|
|
7
|
+
push: (mutationEvent: MutationEvent.AnyEncoded) => Effect.Effect<void, IsOfflineError | InvalidPushError>
|
|
8
|
+
pushes: Stream.Stream<MutationEvent.AnyEncoded>
|
|
9
|
+
isConnected: SubscriptionRef.SubscriptionRef<boolean>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class IsOfflineError extends Schema.TaggedError<IsOfflineError>()('IsOfflineError', {}) {}
|
|
13
|
+
export class InvalidPushError extends Schema.TaggedError<InvalidPushError>()('InvalidPushError', {}) {}
|
|
14
|
+
export class InvalidPullError extends Schema.TaggedError<InvalidPullError>()('InvalidPullError', {}) {}
|