@livestore/common 0.0.47 → 0.0.48-dev.1
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 +52 -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 +13 -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 +20 -0
- package/dist/schema-management/migrations.d.ts.map +1 -0
- package/dist/{migrations.js → schema-management/migrations.js} +49 -35
- package/dist/schema-management/migrations.js.map +1 -0
- package/dist/schema-management/validate-mutation-defs.d.ts +6 -0
- package/dist/schema-management/validate-mutation-defs.d.ts.map +1 -0
- package/dist/schema-management/validate-mutation-defs.js +37 -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 +127 -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 +40 -0
- package/src/{migrations.ts → schema-management/migrations.ts} +81 -56
- package/src/schema-management/validate-mutation-defs.ts +60 -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
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { appConfig, todos } from './__tests__/fixture.js'
|
|
4
|
+
import type { MutationEvent } from './schema/mutations.js'
|
|
5
|
+
|
|
6
|
+
describe('derived mutations', () => {
|
|
7
|
+
test('todos', () => {
|
|
8
|
+
expect(patchId(todos.insert({ id: 't1', completed: true, text: 'Task 1' }))).toMatchInlineSnapshot(`
|
|
9
|
+
{
|
|
10
|
+
"args": {
|
|
11
|
+
"completed": true,
|
|
12
|
+
"id": "t1",
|
|
13
|
+
"text": "Task 1",
|
|
14
|
+
},
|
|
15
|
+
"id": "00000000-0000-0000-0000-000000000000",
|
|
16
|
+
"mutation": "_Derived_Create_todos",
|
|
17
|
+
}
|
|
18
|
+
`)
|
|
19
|
+
|
|
20
|
+
expect(patchId(todos.update({ where: { id: 't1' }, values: { text: 'Task 1 - fixed' } }))).toMatchInlineSnapshot(`
|
|
21
|
+
{
|
|
22
|
+
"args": {
|
|
23
|
+
"values": {
|
|
24
|
+
"text": "Task 1 - fixed",
|
|
25
|
+
},
|
|
26
|
+
"where": {
|
|
27
|
+
"id": "t1",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
"id": "00000000-0000-0000-0000-000000000000",
|
|
31
|
+
"mutation": "_Derived_Update_todos",
|
|
32
|
+
}
|
|
33
|
+
`)
|
|
34
|
+
|
|
35
|
+
expect(patchId(todos.delete({ where: { id: 't1' } }))).toMatchInlineSnapshot(`
|
|
36
|
+
{
|
|
37
|
+
"args": {
|
|
38
|
+
"where": {
|
|
39
|
+
"id": "t1",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
"id": "00000000-0000-0000-0000-000000000000",
|
|
43
|
+
"mutation": "_Derived_Delete_todos",
|
|
44
|
+
}
|
|
45
|
+
`)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('app_config', () => {
|
|
49
|
+
expect(patchId(appConfig.insert())).toMatchInlineSnapshot(`
|
|
50
|
+
{
|
|
51
|
+
"args": {
|
|
52
|
+
"id": "singleton",
|
|
53
|
+
"value": {
|
|
54
|
+
"value": undefined,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
"id": "00000000-0000-0000-0000-000000000000",
|
|
58
|
+
"mutation": "_Derived_Create_app_config",
|
|
59
|
+
}
|
|
60
|
+
`)
|
|
61
|
+
|
|
62
|
+
expect(patchId(appConfig.insert({ fontSize: 12, theme: 'dark' }))).toMatchInlineSnapshot(`
|
|
63
|
+
{
|
|
64
|
+
"args": {
|
|
65
|
+
"id": "singleton",
|
|
66
|
+
"value": {
|
|
67
|
+
"value": {
|
|
68
|
+
"fontSize": 12,
|
|
69
|
+
"theme": "dark",
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
"id": "00000000-0000-0000-0000-000000000000",
|
|
74
|
+
"mutation": "_Derived_Create_app_config",
|
|
75
|
+
}
|
|
76
|
+
`)
|
|
77
|
+
|
|
78
|
+
expect(patchId(appConfig.update({ fontSize: 13 }))).toMatchInlineSnapshot(`
|
|
79
|
+
{
|
|
80
|
+
"args": {
|
|
81
|
+
"values": {
|
|
82
|
+
"value": {
|
|
83
|
+
"fontSize": 13,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
"where": {
|
|
87
|
+
"id": "singleton",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
"id": "00000000-0000-0000-0000-000000000000",
|
|
91
|
+
"mutation": "_Derived_Update_app_config",
|
|
92
|
+
}
|
|
93
|
+
`)
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const patchId = (muationEvent: MutationEvent.Any) => {
|
|
98
|
+
const id = `00000000-0000-0000-0000-000000000000`
|
|
99
|
+
return { ...muationEvent, id }
|
|
100
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { GetValForKey } from '@livestore/utils'
|
|
2
|
+
import { ReadonlyRecord, Schema } from '@livestore/utils/effect'
|
|
3
|
+
import type { SqliteDsl } from 'effect-db-schema'
|
|
4
|
+
|
|
5
|
+
import type { MutationEvent } from './schema/mutations.js'
|
|
6
|
+
import { defineMutation } from './schema/mutations.js'
|
|
7
|
+
import type { TableOptions } from './schema/table-def.js'
|
|
8
|
+
import * as DbSchema from './schema/table-def.js'
|
|
9
|
+
import { deleteRows, insertRow, updateRows } from './sql-queries/sql-queries.js'
|
|
10
|
+
|
|
11
|
+
export const makeDerivedMutationDefsForTable = <TTableDef extends DbSchema.TableDef>(table: TTableDef) => ({
|
|
12
|
+
insert: deriveCreateMutationDef(table),
|
|
13
|
+
update: deriveUpdateMutationDef(table),
|
|
14
|
+
delete: deriveDeleteMutationDef(table),
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
export const deriveCreateMutationDef = <TTableDef extends DbSchema.TableDef>(table: TTableDef) => {
|
|
18
|
+
const tableName = table.sqliteDef.name
|
|
19
|
+
|
|
20
|
+
const [optionalFields, requiredColumns] = ReadonlyRecord.partition(
|
|
21
|
+
(table.sqliteDef as DbSchema.DefaultSqliteTableDef).columns,
|
|
22
|
+
(col) => col.nullable === false && col.default._tag === 'None',
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
const insertSchema = Schema.Struct(ReadonlyRecord.map(requiredColumns, (col) => col.schema)).pipe(
|
|
26
|
+
Schema.extend(Schema.partial(Schema.Struct(ReadonlyRecord.map(optionalFields, (col) => col.schema)))),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
return defineMutation(`_Derived_Create_${tableName}`, insertSchema, ({ id, ...explicitDefaultValues }) => {
|
|
30
|
+
const defaultValues = DbSchema.getDefaultValuesDecoded(table, explicitDefaultValues)
|
|
31
|
+
|
|
32
|
+
const [sql, bindValues] = insertRow({
|
|
33
|
+
tableName: table.sqliteDef.name,
|
|
34
|
+
columns: table.sqliteDef.columns,
|
|
35
|
+
values: { ...defaultValues, id },
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
return { sql, bindValues, writeTables: new Set([tableName]) }
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const deriveUpdateMutationDef = <TTableDef extends DbSchema.TableDef>(table: TTableDef) => {
|
|
43
|
+
const tableName = table.sqliteDef.name
|
|
44
|
+
|
|
45
|
+
return defineMutation(
|
|
46
|
+
`_Derived_Update_${tableName}`,
|
|
47
|
+
Schema.Struct({
|
|
48
|
+
where: Schema.partial(table.schema),
|
|
49
|
+
values: Schema.partial(table.schema),
|
|
50
|
+
}),
|
|
51
|
+
({ where, values }) => {
|
|
52
|
+
const [sql, bindValues] = updateRows({
|
|
53
|
+
tableName: table.sqliteDef.name,
|
|
54
|
+
columns: table.sqliteDef.columns,
|
|
55
|
+
where,
|
|
56
|
+
updateValues: values,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
return { sql, bindValues, writeTables: new Set([tableName]) }
|
|
60
|
+
},
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const deriveDeleteMutationDef = <TTableDef extends DbSchema.TableDef>(table: TTableDef) => {
|
|
65
|
+
const tableName = table.sqliteDef.name
|
|
66
|
+
|
|
67
|
+
return defineMutation(
|
|
68
|
+
`_Derived_Delete_${tableName}`,
|
|
69
|
+
Schema.Struct({
|
|
70
|
+
where: Schema.partial(table.schema),
|
|
71
|
+
}),
|
|
72
|
+
({ where }) => {
|
|
73
|
+
const [sql, bindValues] = deleteRows({
|
|
74
|
+
tableName: table.sqliteDef.name,
|
|
75
|
+
columns: table.sqliteDef.columns,
|
|
76
|
+
where,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
return { sql, bindValues, writeTables: new Set([tableName]) }
|
|
80
|
+
},
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Convenience helper functions on top of the derived mutation definitions.
|
|
86
|
+
*/
|
|
87
|
+
export type DerivedMutationHelperFns<TColumns extends SqliteDsl.ConstraintColumns, TOptions extends TableOptions> = {
|
|
88
|
+
insert: DerivedMutationHelperFns.InsertMutationFn<TColumns, TOptions>
|
|
89
|
+
update: DerivedMutationHelperFns.UpdateMutationFn<TColumns, TOptions>
|
|
90
|
+
delete: DerivedMutationHelperFns.DeleteMutationFn<TColumns, TOptions>
|
|
91
|
+
// TODO also consider adding upsert and deep json mutations (like lenses)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export namespace DerivedMutationHelperFns {
|
|
95
|
+
export type InsertMutationFn<
|
|
96
|
+
TColumns extends SqliteDsl.ConstraintColumns,
|
|
97
|
+
TOptions extends TableOptions,
|
|
98
|
+
> = SqliteDsl.AnyIfConstained<
|
|
99
|
+
TColumns,
|
|
100
|
+
UseShortcut<TOptions> extends true
|
|
101
|
+
? (values?: GetValForKey<SqliteDsl.FromColumns.InsertRowDecoded<TColumns>, 'value'>) => MutationEvent.Any
|
|
102
|
+
: (values: SqliteDsl.FromColumns.InsertRowDecoded<TColumns>) => MutationEvent.Any
|
|
103
|
+
>
|
|
104
|
+
|
|
105
|
+
export type UpdateMutationFn<
|
|
106
|
+
TColumns extends SqliteDsl.ConstraintColumns,
|
|
107
|
+
TOptions extends TableOptions,
|
|
108
|
+
> = SqliteDsl.AnyIfConstained<
|
|
109
|
+
TColumns,
|
|
110
|
+
UseShortcut<TOptions> extends true
|
|
111
|
+
? (values: Partial<GetValForKey<SqliteDsl.FromColumns.RowDecoded<TColumns>, 'value'>>) => MutationEvent.Any
|
|
112
|
+
: (args: {
|
|
113
|
+
where: Partial<SqliteDsl.FromColumns.RowDecoded<TColumns>>
|
|
114
|
+
values: Partial<SqliteDsl.FromColumns.RowDecoded<TColumns>>
|
|
115
|
+
}) => MutationEvent.Any
|
|
116
|
+
>
|
|
117
|
+
|
|
118
|
+
export type DeleteMutationFn<TColumns extends SqliteDsl.ConstraintColumns, _TOptions extends TableOptions> = (args: {
|
|
119
|
+
where: Partial<SqliteDsl.FromColumns.RowDecoded<TColumns>>
|
|
120
|
+
}) => MutationEvent.Any
|
|
121
|
+
|
|
122
|
+
type UseShortcut<TOptions extends TableOptions> = TOptions['isSingleColumn'] extends true
|
|
123
|
+
? TOptions['isSingleton'] extends true
|
|
124
|
+
? true
|
|
125
|
+
: false
|
|
126
|
+
: false
|
|
127
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
export * from './schema/system-tables.js'
|
|
1
2
|
export * from './util.js'
|
|
2
|
-
export * from './
|
|
3
|
-
export * from './migrations.js'
|
|
3
|
+
export * from './adapter-types.js'
|
|
4
|
+
export * from './schema-management/migrations.js'
|
|
4
5
|
export * from './mutation.js'
|
|
5
6
|
export * from './init-singleton-tables.js'
|
|
6
7
|
export * from './rehydrate-from-mutationlog.js'
|
|
8
|
+
export * from './query-info.js'
|
|
9
|
+
export * from './derived-mutations.js'
|
|
10
|
+
export * from './sync/index.js'
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { InMemoryDatabase } from './adapter-types.js'
|
|
2
2
|
import type { LiveStoreSchema } from './schema/index.js'
|
|
3
3
|
import { DbSchema } from './schema/index.js'
|
|
4
4
|
import { prepareBindValues, sql } from './util.js'
|
|
5
5
|
|
|
6
|
-
export const initializeSingletonTables = (schema: LiveStoreSchema, db:
|
|
6
|
+
export const initializeSingletonTables = (schema: LiveStoreSchema, db: InMemoryDatabase) => {
|
|
7
7
|
for (const [, tableDef] of schema.tables) {
|
|
8
8
|
if (tableDef.options.isSingleton) {
|
|
9
9
|
const defaultValues = DbSchema.getDefaultValuesEncoded(tableDef, undefined)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { Schema } from '@livestore/utils/effect'
|
|
2
|
+
|
|
3
|
+
import type { DbSchema } from './schema/index.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Semantic information about a query with supported cases being:
|
|
7
|
+
* - a whole row
|
|
8
|
+
* - a single column value
|
|
9
|
+
* - a sub value in a JSON column
|
|
10
|
+
*/
|
|
11
|
+
export type QueryInfo<TTableDef extends DbSchema.TableDef = DbSchema.TableDef> =
|
|
12
|
+
| QueryInfoNone
|
|
13
|
+
| QueryInfoRow<TTableDef>
|
|
14
|
+
| QueryInfoColJsonValue<TTableDef, GetJsonColumn<TTableDef>>
|
|
15
|
+
| QueryInfoCol<TTableDef, keyof TTableDef['sqliteDef']['columns']>
|
|
16
|
+
|
|
17
|
+
export type QueryInfoNone = {
|
|
18
|
+
_tag: 'None'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type QueryInfoRow<TTableDef extends DbSchema.TableDef> = {
|
|
22
|
+
_tag: 'Row'
|
|
23
|
+
table: TTableDef
|
|
24
|
+
id: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type QueryInfoCol<
|
|
28
|
+
TTableDef extends DbSchema.TableDef,
|
|
29
|
+
TColName extends keyof TTableDef['sqliteDef']['columns'],
|
|
30
|
+
> = {
|
|
31
|
+
_tag: 'Col'
|
|
32
|
+
table: TTableDef
|
|
33
|
+
id: string
|
|
34
|
+
column: TColName
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type QueryInfoColJsonValue<TTableDef extends DbSchema.TableDef, TColName extends GetJsonColumn<TTableDef>> = {
|
|
38
|
+
_tag: 'ColJsonValue'
|
|
39
|
+
table: TTableDef
|
|
40
|
+
id: string
|
|
41
|
+
column: TColName
|
|
42
|
+
/**
|
|
43
|
+
* example: `$.tabs[3].items[2]` (`$` referring to the column value)
|
|
44
|
+
*/
|
|
45
|
+
jsonPath: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type GetJsonColumn<TTableDef extends DbSchema.TableDef> = keyof {
|
|
49
|
+
[ColName in keyof TTableDef['sqliteDef']['columns'] as TTableDef['sqliteDef']['columns'][ColName]['columnType'] extends 'text'
|
|
50
|
+
? ColName
|
|
51
|
+
: never]: {}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type UpdateValueForPath<TQueryInfo extends QueryInfo> = TQueryInfo extends { _tag: 'Row' }
|
|
55
|
+
? Partial<DbSchema.FromTable.RowDecodedAll<TQueryInfo['table']>>
|
|
56
|
+
: TQueryInfo extends { _tag: 'Col' }
|
|
57
|
+
? Schema.Schema.Type<TQueryInfo['table']['sqliteDef']['columns'][TQueryInfo['column']]['schema']>
|
|
58
|
+
: TQueryInfo extends { _tag: 'ColJsonValue' }
|
|
59
|
+
? { TODO: true }
|
|
60
|
+
: never
|
|
61
|
+
|
|
62
|
+
// export const mutationForQueryInfo = <const TQueryInfo extends QueryInfo>(
|
|
63
|
+
// queryInfo: TQueryInfo,
|
|
64
|
+
// value: UpdateValueForPath<TQueryInfo>,
|
|
65
|
+
// ): RawSqlMutationEvent => {
|
|
66
|
+
// if (queryInfo._tag === 'ColJsonValue' || queryInfo._tag === 'None') {
|
|
67
|
+
// return notYetImplemented('TODO')
|
|
68
|
+
// }
|
|
69
|
+
|
|
70
|
+
// const sqliteTableDef = queryInfo.table.sqliteDef
|
|
71
|
+
// const id = queryInfo.id
|
|
72
|
+
|
|
73
|
+
// const { columnNames, bindValues } = (() => {
|
|
74
|
+
// if (queryInfo._tag === 'Row') {
|
|
75
|
+
// const columnNames = Object.keys(value)
|
|
76
|
+
|
|
77
|
+
// const partialStructSchema = queryInfo.table.schema.pipe(Schema.pick(...columnNames))
|
|
78
|
+
|
|
79
|
+
// // const columnNames = Object.keys(value)
|
|
80
|
+
// const encodedBindValues = Schema.encodeEither(partialStructSchema)(value)
|
|
81
|
+
// if (encodedBindValues._tag === 'Left') {
|
|
82
|
+
// return shouldNeverHappen(encodedBindValues.left.toString())
|
|
83
|
+
// } else {
|
|
84
|
+
// return { columnNames, bindValues: encodedBindValues.right }
|
|
85
|
+
// }
|
|
86
|
+
// } else if (queryInfo._tag === 'Col') {
|
|
87
|
+
// const columnName = queryInfo.column
|
|
88
|
+
// const columnSchema =
|
|
89
|
+
// sqliteTableDef.columns[columnName]?.schema ?? shouldNeverHappen(`Column ${columnName} not found`)
|
|
90
|
+
// const bindValues = { [columnName]: Schema.encodeSync(columnSchema)(value) }
|
|
91
|
+
// return { columnNames: [columnName], bindValues }
|
|
92
|
+
// } else {
|
|
93
|
+
// return shouldNeverHappen()
|
|
94
|
+
// }
|
|
95
|
+
// })()
|
|
96
|
+
|
|
97
|
+
// const updateClause = columnNames.map((columnName) => `${columnName} = $${columnName}`).join(', ')
|
|
98
|
+
|
|
99
|
+
// const whereClause = `where id = '${id}'`
|
|
100
|
+
// const sql = `UPDATE ${sqliteTableDef.name} SET ${updateClause} ${whereClause}`
|
|
101
|
+
// const writeTables = new Set<string>([queryInfo.table.sqliteDef.name])
|
|
102
|
+
|
|
103
|
+
// return rawSqlMutation({ sql, bindValues, writeTables })
|
|
104
|
+
// }
|
|
@@ -1,45 +1,52 @@
|
|
|
1
1
|
import { shouldNeverHappen } from '@livestore/utils'
|
|
2
2
|
import { Schema } from '@livestore/utils/effect'
|
|
3
3
|
|
|
4
|
-
import type {
|
|
4
|
+
import type { InMemoryDatabase, MigrationOptionsFromMutationLog } from './adapter-types.js'
|
|
5
5
|
import { getExecArgsFromMutation } from './mutation.js'
|
|
6
|
-
import type { LiveStoreSchema } from './schema/index.js'
|
|
6
|
+
import type { LiveStoreSchema, MutationLogMetaRow } from './schema/index.js'
|
|
7
|
+
import { MUTATION_LOG_META_TABLE } from './schema/index.js'
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
id: string
|
|
10
|
-
mutation: string
|
|
11
|
-
args_json: string
|
|
12
|
-
schema_hash: number
|
|
13
|
-
created_at: string
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const rehydrateFromMutationLog = ({
|
|
9
|
+
export const rehydrateFromMutationLog = async ({
|
|
17
10
|
logDb,
|
|
18
11
|
db,
|
|
19
12
|
schema,
|
|
13
|
+
migrationOptions,
|
|
20
14
|
}: {
|
|
21
|
-
logDb:
|
|
22
|
-
db:
|
|
15
|
+
logDb: InMemoryDatabase
|
|
16
|
+
db: InMemoryDatabase
|
|
23
17
|
schema: LiveStoreSchema
|
|
18
|
+
migrationOptions: MigrationOptionsFromMutationLog
|
|
24
19
|
}) => {
|
|
25
20
|
try {
|
|
26
|
-
|
|
27
|
-
const
|
|
21
|
+
// TODO possibly implement this in a streaming fashion
|
|
22
|
+
const stmt = logDb.prepare(`SELECT * FROM ${MUTATION_LOG_META_TABLE} ORDER BY id ASC`)
|
|
23
|
+
const results = stmt.select<MutationLogMetaRow>(undefined)
|
|
28
24
|
|
|
29
25
|
performance.mark('livestore:hydrate-from-mutationlog:start')
|
|
30
26
|
|
|
31
27
|
for (const row of results) {
|
|
32
28
|
const mutationDef = schema.mutations.get(row.mutation) ?? shouldNeverHappen(`Unknown mutation ${row.mutation}`)
|
|
33
29
|
|
|
34
|
-
if (
|
|
35
|
-
|
|
30
|
+
if (migrationOptions.excludeMutations?.has(row.mutation) === true) continue
|
|
31
|
+
|
|
32
|
+
if (Schema.hash(mutationDef.schema) !== row.schemaHash) {
|
|
33
|
+
console.warn(`Schema hash mismatch for mutation ${row.mutation}. Trying to apply mutation anyway.`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const argsDecodedEither = Schema.decodeUnknownEither(Schema.parseJson(mutationDef.schema))(row.argsJson)
|
|
37
|
+
if (argsDecodedEither._tag === 'Left') {
|
|
38
|
+
return shouldNeverHappen(`\
|
|
39
|
+
There was an error decoding the persisted mutation event args for mutation "${row.mutation}".
|
|
40
|
+
This likely means the schema has changed in an incompatible way.
|
|
41
|
+
|
|
42
|
+
Error: ${argsDecodedEither.left}
|
|
43
|
+
`)
|
|
36
44
|
}
|
|
37
45
|
|
|
38
|
-
const argsDecoded = Schema.decodeUnknownSync(Schema.parseJson(mutationDef.schema))(row.args_json)
|
|
39
46
|
const mutationEventDecoded = {
|
|
40
47
|
id: row.id,
|
|
41
48
|
mutation: row.mutation,
|
|
42
|
-
args:
|
|
49
|
+
args: argsDecodedEither.right,
|
|
43
50
|
}
|
|
44
51
|
// const argsEncoded = JSON.parse(row.args_json)
|
|
45
52
|
// const mutationSqlRes =
|
|
@@ -53,7 +60,14 @@ export const rehydrateFromMutationLog = ({
|
|
|
53
60
|
|
|
54
61
|
for (const { statementSql, bindValues } of execArgsArr) {
|
|
55
62
|
try {
|
|
56
|
-
db.execute(statementSql, bindValues)
|
|
63
|
+
const getRowsChanged = db.execute(statementSql, bindValues)
|
|
64
|
+
if (
|
|
65
|
+
import.meta.env.DEV &&
|
|
66
|
+
getRowsChanged() === 0 &&
|
|
67
|
+
migrationOptions.logging?.excludeAffectedRows?.(statementSql) !== true
|
|
68
|
+
) {
|
|
69
|
+
console.warn(`Mutation "${mutationDef.name}" did not affect any rows:`, statementSql, bindValues)
|
|
70
|
+
}
|
|
57
71
|
// console.log(`Re-executed mutation ${mutationSql}`, bindValues)
|
|
58
72
|
} catch (e) {
|
|
59
73
|
console.error(`Error executing migration for mutation ${statementSql}`, bindValues, e)
|
package/src/schema/index.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { isReadonlyArray } from '@livestore/utils'
|
|
1
|
+
import { isReadonlyArray, shouldNeverHappen } from '@livestore/utils'
|
|
2
2
|
import type { ReadonlyArray } from '@livestore/utils/effect'
|
|
3
3
|
import { SqliteAst, type SqliteDsl } from 'effect-db-schema'
|
|
4
4
|
|
|
5
|
+
import type { MigrationOptions } from '../adapter-types.js'
|
|
6
|
+
import { makeDerivedMutationDefsForTable } from '../derived-mutations.js'
|
|
5
7
|
import {
|
|
6
8
|
type MutationDef,
|
|
7
9
|
type MutationDefMap,
|
|
@@ -28,6 +30,10 @@ export type LiveStoreSchema<
|
|
|
28
30
|
|
|
29
31
|
readonly tables: Map<string, TableDef>
|
|
30
32
|
readonly mutations: MutationDefMap
|
|
33
|
+
/** Compound hash of all table defs etc */
|
|
34
|
+
readonly hash: number
|
|
35
|
+
|
|
36
|
+
migrationOptions: MigrationOptions
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
export type InputSchema = {
|
|
@@ -37,20 +43,22 @@ export type InputSchema = {
|
|
|
37
43
|
|
|
38
44
|
export const makeSchema = <TInputSchema extends InputSchema>(
|
|
39
45
|
/** Note when using the object-notation for tables/mutations, the object keys are ignored and not used as table/mutation names */
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
> => {
|
|
45
|
-
const inputTables: ReadonlyArray<TableDef> = Array.isArray(
|
|
46
|
-
?
|
|
47
|
-
:
|
|
48
|
-
Object.values(schema.tables)
|
|
46
|
+
inputSchema: TInputSchema & {
|
|
47
|
+
/** "hard-reset" is currently the default strategy */
|
|
48
|
+
migrations?: MigrationOptions<FromInputSchema.DeriveSchema<TInputSchema>>
|
|
49
|
+
},
|
|
50
|
+
): FromInputSchema.DeriveSchema<TInputSchema> => {
|
|
51
|
+
const inputTables: ReadonlyArray<TableDef> = Array.isArray(inputSchema.tables)
|
|
52
|
+
? inputSchema.tables
|
|
53
|
+
: Object.values(inputSchema.tables)
|
|
49
54
|
|
|
50
55
|
const tables = new Map<string, TableDef>()
|
|
51
56
|
|
|
52
57
|
for (const tableDef of inputTables) {
|
|
53
58
|
// TODO validate tables (e.g. index names are unique)
|
|
59
|
+
if (tables.has(tableDef.sqliteDef.ast.name)) {
|
|
60
|
+
shouldNeverHappen(`Duplicate table name: ${tableDef.sqliteDef.ast.name}. Please use unique names for tables.`)
|
|
61
|
+
}
|
|
54
62
|
tables.set(tableDef.sqliteDef.ast.name, tableDef)
|
|
55
63
|
}
|
|
56
64
|
|
|
@@ -60,47 +68,67 @@ export const makeSchema = <TInputSchema extends InputSchema>(
|
|
|
60
68
|
|
|
61
69
|
const mutations: MutationDefMap = new Map()
|
|
62
70
|
|
|
63
|
-
if (isReadonlyArray(
|
|
64
|
-
for (const mutation of
|
|
71
|
+
if (isReadonlyArray(inputSchema.mutations)) {
|
|
72
|
+
for (const mutation of inputSchema.mutations) {
|
|
65
73
|
mutations.set(mutation.name, mutation)
|
|
66
74
|
}
|
|
67
75
|
} else {
|
|
68
|
-
for (const
|
|
69
|
-
mutations.
|
|
76
|
+
for (const mutation of Object.values(inputSchema.mutations ?? {})) {
|
|
77
|
+
if (mutations.has(mutation.name)) {
|
|
78
|
+
shouldNeverHappen(`Duplicate mutation name: ${mutation.name}. Please use unique names for mutations.`)
|
|
79
|
+
}
|
|
80
|
+
mutations.set(mutation.name, mutation)
|
|
70
81
|
}
|
|
71
82
|
}
|
|
72
83
|
|
|
73
|
-
mutations.set(
|
|
84
|
+
mutations.set(rawSqlMutation.name, rawSqlMutation)
|
|
85
|
+
|
|
86
|
+
for (const tableDef of tables.values()) {
|
|
87
|
+
if (tableDef.options.deriveMutations) {
|
|
88
|
+
const derivedMutationDefs = makeDerivedMutationDefsForTable(tableDef)
|
|
89
|
+
mutations.set(derivedMutationDefs.insert.name, derivedMutationDefs.insert)
|
|
90
|
+
mutations.set(derivedMutationDefs.update.name, derivedMutationDefs.update)
|
|
91
|
+
mutations.set(derivedMutationDefs.delete.name, derivedMutationDefs.delete)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const hash = SqliteAst.hash({
|
|
96
|
+
_tag: 'dbSchema',
|
|
97
|
+
tables: [...tables.values()].map((_) => _.sqliteDef.ast),
|
|
98
|
+
})
|
|
74
99
|
|
|
75
100
|
return {
|
|
76
101
|
_DbSchemaType: Symbol('livestore.DbSchemaType') as any,
|
|
77
102
|
_MutationDefMapType: Symbol('livestore.MutationDefMapType') as any,
|
|
78
103
|
tables,
|
|
79
104
|
mutations,
|
|
105
|
+
migrationOptions: inputSchema.migrations ?? { strategy: 'hard-reset' },
|
|
106
|
+
hash,
|
|
80
107
|
} satisfies LiveStoreSchema
|
|
81
108
|
}
|
|
82
109
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
110
|
+
namespace FromInputSchema {
|
|
111
|
+
export type DeriveSchema<TInputSchema extends InputSchema> = LiveStoreSchema<
|
|
112
|
+
DbSchemaFromInputSchemaTables<TInputSchema['tables']>,
|
|
113
|
+
MutationDefRecordFromInputSchemaMutations<TInputSchema['mutations']>
|
|
114
|
+
>
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* In case of ...
|
|
118
|
+
* - array: we use the table name of each array item (= table definition) as the object key
|
|
119
|
+
* - object: we discard the keys of the input object and use the table name of each object value (= table definition) as the new object key
|
|
120
|
+
*/
|
|
121
|
+
type DbSchemaFromInputSchemaTables<TTables extends InputSchema['tables']> =
|
|
122
|
+
TTables extends ReadonlyArray<TableDef>
|
|
123
|
+
? { [K in TTables[number] as K['sqliteDef']['name']]: K['sqliteDef'] }
|
|
124
|
+
: TTables extends Record<string, TableDef>
|
|
125
|
+
? { [K in keyof TTables as TTables[K]['sqliteDef']['name']]: TTables[K]['sqliteDef'] }
|
|
126
|
+
: never
|
|
127
|
+
|
|
128
|
+
type MutationDefRecordFromInputSchemaMutations<TMutations extends InputSchema['mutations']> =
|
|
129
|
+
TMutations extends ReadonlyArray<MutationDef.Any>
|
|
130
|
+
? { [K in TMutations[number] as K['name']]: K } & { 'livestore.RawSql': RawSqlMutation }
|
|
131
|
+
: TMutations extends { [name: string]: MutationDef.Any }
|
|
132
|
+
? { [K in keyof TMutations as TMutations[K]['name']]: TMutations[K] } & { 'livestore.RawSql': RawSqlMutation }
|
|
133
|
+
: never
|
|
134
|
+
}
|
package/src/schema/mutations.ts
CHANGED
|
@@ -77,10 +77,10 @@ export const makeMutationDefRecord = <TInputRecord extends Record<string, Mutati
|
|
|
77
77
|
|
|
78
78
|
export const rawSqlMutation = defineMutation(
|
|
79
79
|
'livestore.RawSql',
|
|
80
|
-
Schema.
|
|
81
|
-
sql: Schema.
|
|
82
|
-
bindValues: Schema.optional(Schema.
|
|
83
|
-
writeTables: Schema.optional(Schema.
|
|
80
|
+
Schema.Struct({
|
|
81
|
+
sql: Schema.String,
|
|
82
|
+
bindValues: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
|
|
83
|
+
writeTables: Schema.optional(Schema.ReadonlySet(Schema.String)),
|
|
84
84
|
}),
|
|
85
85
|
({ sql, bindValues, writeTables }) => ({ sql, bindValues: bindValues ?? {}, writeTables }),
|
|
86
86
|
)
|
|
@@ -94,8 +94,15 @@ export type MutationEvent<TMutationsDef extends MutationDef.Any> = {
|
|
|
94
94
|
id: string
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
export type MutationEventEncoded<TMutationsDef extends MutationDef.Any> = {
|
|
98
|
+
mutation: TMutationsDef['name']
|
|
99
|
+
args: Schema.Schema.Encoded<TMutationsDef['schema']>
|
|
100
|
+
id: string
|
|
101
|
+
}
|
|
102
|
+
|
|
97
103
|
export namespace MutationEvent {
|
|
98
104
|
export type Any = MutationEvent<MutationDef.Any>
|
|
105
|
+
export type AnyEncoded = MutationEventEncoded<MutationDef.Any>
|
|
99
106
|
|
|
100
107
|
export type ForSchema<TSchema extends LiveStoreSchema> = {
|
|
101
108
|
[K in keyof TSchema['_MutationDefMapType']]: MutationEvent<TSchema['_MutationDefMapType'][K]>
|
|
@@ -122,12 +129,24 @@ export type MutationEventSchema<TMutationsDefRecord extends MutationDefRecord> =
|
|
|
122
129
|
export const makeMutationEventSchema = <TMutationsDefRecord extends MutationDefRecord>(
|
|
123
130
|
mutationDefRecord: TMutationsDefRecord,
|
|
124
131
|
): MutationEventSchema<TMutationsDefRecord> =>
|
|
125
|
-
Schema.
|
|
132
|
+
Schema.Union(
|
|
126
133
|
...Object.values(mutationDefRecord).map((def) =>
|
|
127
|
-
Schema.
|
|
128
|
-
mutation: Schema.
|
|
134
|
+
Schema.Struct({
|
|
135
|
+
mutation: Schema.Literal(def.name),
|
|
129
136
|
args: def.schema,
|
|
130
|
-
id: Schema.
|
|
137
|
+
id: Schema.String,
|
|
131
138
|
}),
|
|
132
139
|
),
|
|
133
|
-
) as any
|
|
140
|
+
).annotations({ title: 'MutationEventSchema' }) as any
|
|
141
|
+
|
|
142
|
+
export const mutationEventSchemaDecodedAny = Schema.Struct({
|
|
143
|
+
mutation: Schema.String,
|
|
144
|
+
args: Schema.Any,
|
|
145
|
+
id: Schema.String,
|
|
146
|
+
}).annotations({ title: 'MutationEventSchema.DecodedAny' })
|
|
147
|
+
|
|
148
|
+
export const mutationEventSchemaEncodedAny = Schema.Struct({
|
|
149
|
+
mutation: Schema.String,
|
|
150
|
+
args: Schema.Any,
|
|
151
|
+
id: Schema.String,
|
|
152
|
+
}).annotations({ title: 'MutationEventSchema.EncodedAny' })
|