@livestore/livestore 0.4.0-dev.21 → 0.4.0-dev.23
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 +0 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/QueryCache.js +1 -1
- package/dist/QueryCache.js.map +1 -1
- package/dist/SqliteDbWrapper.d.ts +5 -5
- package/dist/SqliteDbWrapper.d.ts.map +1 -1
- package/dist/SqliteDbWrapper.js +8 -8
- package/dist/SqliteDbWrapper.js.map +1 -1
- package/dist/SqliteDbWrapper.test.js +2 -2
- package/dist/SqliteDbWrapper.test.js.map +1 -1
- package/dist/effect/LiveStore.d.ts +130 -2
- package/dist/effect/LiveStore.d.ts.map +1 -1
- package/dist/effect/LiveStore.js +185 -6
- package/dist/effect/LiveStore.js.map +1 -1
- package/dist/effect/LiveStore.test.d.ts +2 -0
- package/dist/effect/LiveStore.test.d.ts.map +1 -0
- package/dist/effect/LiveStore.test.js +42 -0
- package/dist/effect/LiveStore.test.js.map +1 -0
- package/dist/effect/mod.d.ts +1 -1
- package/dist/effect/mod.d.ts.map +1 -1
- package/dist/effect/mod.js +3 -1
- package/dist/effect/mod.js.map +1 -1
- package/dist/live-queries/base-class.d.ts +3 -3
- package/dist/live-queries/base-class.d.ts.map +1 -1
- package/dist/live-queries/base-class.js +2 -2
- package/dist/live-queries/base-class.js.map +1 -1
- package/dist/live-queries/client-document-get-query.d.ts +1 -1
- package/dist/live-queries/client-document-get-query.d.ts.map +1 -1
- package/dist/live-queries/client-document-get-query.js +1 -1
- package/dist/live-queries/client-document-get-query.js.map +1 -1
- package/dist/live-queries/computed.d.ts.map +1 -1
- package/dist/live-queries/computed.js +2 -2
- package/dist/live-queries/computed.js.map +1 -1
- package/dist/live-queries/db-query.js +14 -14
- package/dist/live-queries/db-query.js.map +1 -1
- package/dist/live-queries/db-query.test.js +2 -2
- package/dist/live-queries/db-query.test.js.map +1 -1
- package/dist/live-queries/signal.test.js +2 -2
- package/dist/live-queries/signal.test.js.map +1 -1
- package/dist/mod.d.ts +2 -1
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +1 -0
- package/dist/mod.js.map +1 -1
- package/dist/reactive.d.ts +9 -9
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +9 -26
- package/dist/reactive.js.map +1 -1
- package/dist/reactive.test.js +2 -2
- package/dist/reactive.test.js.map +1 -1
- package/dist/store/StoreRegistry.d.ts +215 -0
- package/dist/store/StoreRegistry.d.ts.map +1 -0
- package/dist/store/StoreRegistry.js +267 -0
- package/dist/store/StoreRegistry.js.map +1 -0
- package/dist/store/StoreRegistry.test.d.ts +2 -0
- package/dist/store/StoreRegistry.test.d.ts.map +1 -0
- package/dist/store/StoreRegistry.test.js +381 -0
- package/dist/store/StoreRegistry.test.js.map +1 -0
- package/dist/store/create-store.d.ts +56 -6
- package/dist/store/create-store.d.ts.map +1 -1
- package/dist/store/create-store.js +32 -7
- package/dist/store/create-store.js.map +1 -1
- package/dist/store/devtools.d.ts +1 -1
- package/dist/store/devtools.d.ts.map +1 -1
- package/dist/store/devtools.js +16 -3
- package/dist/store/devtools.js.map +1 -1
- package/dist/store/store-eventstream.test.js +2 -2
- package/dist/store/store-eventstream.test.js.map +1 -1
- package/dist/store/store-types.d.ts +59 -9
- package/dist/store/store-types.d.ts.map +1 -1
- package/dist/store/store-types.js.map +1 -1
- package/dist/store/store-types.test.js +1 -1
- package/dist/store/store-types.test.js.map +1 -1
- package/dist/store/store.d.ts +102 -6
- package/dist/store/store.d.ts.map +1 -1
- package/dist/store/store.js +148 -47
- package/dist/store/store.js.map +1 -1
- package/dist/utils/dev.js.map +1 -1
- package/dist/utils/stack-info.js +2 -2
- package/dist/utils/stack-info.js.map +1 -1
- package/dist/utils/tests/fixture.d.ts +1 -1
- package/dist/utils/tests/fixture.d.ts.map +1 -1
- package/dist/utils/tests/fixture.js.map +1 -1
- package/dist/utils/tests/otel.d.ts.map +1 -1
- package/dist/utils/tests/otel.js +5 -5
- package/dist/utils/tests/otel.js.map +1 -1
- package/package.json +59 -18
- package/src/QueryCache.ts +1 -1
- package/src/SqliteDbWrapper.test.ts +4 -2
- package/src/SqliteDbWrapper.ts +12 -11
- package/src/ambient.d.ts +0 -7
- package/src/effect/LiveStore.test.ts +61 -0
- package/src/effect/LiveStore.ts +381 -8
- package/src/effect/mod.ts +13 -1
- package/src/live-queries/__snapshots__/db-query.test.ts.snap +336 -231
- package/src/live-queries/base-class.ts +7 -6
- package/src/live-queries/client-document-get-query.ts +4 -2
- package/src/live-queries/computed.ts +3 -2
- package/src/live-queries/db-query.test.ts +3 -2
- package/src/live-queries/db-query.ts +15 -15
- package/src/live-queries/signal.test.ts +3 -2
- package/src/mod.ts +2 -0
- package/src/reactive.test.ts +3 -2
- package/src/reactive.ts +22 -23
- package/src/store/StoreRegistry.test.ts +540 -0
- package/src/store/StoreRegistry.ts +418 -0
- package/src/store/create-store.ts +76 -15
- package/src/store/devtools.ts +20 -6
- package/src/store/store-eventstream.test.ts +4 -2
- package/src/store/store-types.test.ts +3 -1
- package/src/store/store-types.ts +64 -13
- package/src/store/store.ts +197 -60
- package/src/utils/dev.ts +2 -2
- package/src/utils/stack-info.ts +2 -2
- package/src/utils/tests/fixture.ts +2 -1
- package/src/utils/tests/otel.ts +8 -7
- package/docs/api/index.md +0 -3
- package/docs/building-with-livestore/complex-ui-state/index.md +0 -5
- package/docs/building-with-livestore/crud/index.md +0 -5
- package/docs/building-with-livestore/data-modeling/index.md +0 -1
- package/docs/building-with-livestore/debugging/index.md +0 -17
- package/docs/building-with-livestore/devtools/index.md +0 -79
- package/docs/building-with-livestore/events/index.md +0 -355
- package/docs/building-with-livestore/examples/ai-agent/index.md +0 -5
- package/docs/building-with-livestore/examples/index.md +0 -30
- package/docs/building-with-livestore/examples/todo-workspaces/index.md +0 -891
- package/docs/building-with-livestore/examples/turnbased-game/index.md +0 -7
- package/docs/building-with-livestore/opentelemetry/index.md +0 -208
- package/docs/building-with-livestore/production-checklist/index.md +0 -5
- package/docs/building-with-livestore/reactivity-system/index.md +0 -202
- package/docs/building-with-livestore/rules-for-ai-agents/index.md +0 -9
- package/docs/building-with-livestore/state/materializers/index.md +0 -300
- package/docs/building-with-livestore/state/sql-queries/index.md +0 -72
- package/docs/building-with-livestore/state/sqlite/index.md +0 -45
- package/docs/building-with-livestore/state/sqlite-schema/index.md +0 -306
- package/docs/building-with-livestore/state/sqlite-schema-effect/index.md +0 -300
- package/docs/building-with-livestore/store/index.md +0 -281
- package/docs/building-with-livestore/syncing/index.md +0 -136
- package/docs/building-with-livestore/tools/cli/index.md +0 -177
- package/docs/building-with-livestore/tools/mcp/index.md +0 -187
- package/docs/examples/cloudflare-adapter/index.md +0 -44
- package/docs/examples/expo-adapter/index.md +0 -44
- package/docs/examples/index.md +0 -55
- package/docs/examples/node-adapter/index.md +0 -44
- package/docs/examples/web-adapter/index.md +0 -52
- package/docs/framework-integrations/custom-elements/index.md +0 -142
- package/docs/framework-integrations/react-integration/index.md +0 -918
- package/docs/framework-integrations/solid-integration/index.md +0 -293
- package/docs/framework-integrations/svelte-integration/index.md +0 -42
- package/docs/framework-integrations/vue-integration/index.md +0 -294
- package/docs/getting-started/expo/index.md +0 -736
- package/docs/getting-started/node/index.md +0 -115
- package/docs/getting-started/react-web/index.md +0 -573
- package/docs/getting-started/solid/index.md +0 -3
- package/docs/getting-started/vue/index.md +0 -471
- package/docs/index.md +0 -209
- package/docs/llms.txt +0 -147
- package/docs/misc/CODE_OF_CONDUCT/index.md +0 -133
- package/docs/misc/FAQ/index.md +0 -37
- package/docs/misc/community/index.md +0 -88
- package/docs/misc/credits/index.md +0 -14
- package/docs/misc/design-partners/index.md +0 -13
- package/docs/misc/package-management/index.md +0 -21
- package/docs/misc/performance/index.md +0 -25
- package/docs/misc/resources/index.md +0 -46
- package/docs/misc/state-of-the-project/index.md +0 -37
- package/docs/misc/troubleshooting/index.md +0 -82
- package/docs/overview/concepts/index.md +0 -78
- package/docs/overview/how-livestore-works/index.md +0 -56
- package/docs/overview/introduction/index.md +0 -5
- package/docs/overview/technology-comparison/index.md +0 -40
- package/docs/overview/when-livestore/index.md +0 -81
- package/docs/overview/why-livestore/index.md +0 -5
- package/docs/patterns/ai/index.md +0 -15
- package/docs/patterns/anonymous-user-transition/index.md +0 -10
- package/docs/patterns/app-evolution/index.md +0 -72
- package/docs/patterns/auth/index.md +0 -226
- package/docs/patterns/effect/index.md +0 -1495
- package/docs/patterns/encryption/index.md +0 -6
- package/docs/patterns/external-data/index.md +0 -5
- package/docs/patterns/file-management/index.md +0 -11
- package/docs/patterns/file-structure/index.md +0 -14
- package/docs/patterns/list-ordering/index.md +0 -369
- package/docs/patterns/offline/index.md +0 -32
- package/docs/patterns/orm/index.md +0 -18
- package/docs/patterns/presence/index.md +0 -11
- package/docs/patterns/rich-text-editing/index.md +0 -11
- package/docs/patterns/server-side-clients/index.md +0 -97
- package/docs/patterns/side-effects/index.md +0 -11
- package/docs/patterns/state-machines/index.md +0 -11
- package/docs/patterns/storybook/index.md +0 -192
- package/docs/patterns/undo-redo/index.md +0 -9
- package/docs/patterns/version-control/index.md +0 -8
- package/docs/platform-adapters/cloudflare-durable-object-adapter/index.md +0 -453
- package/docs/platform-adapters/electron-adapter/index.md +0 -15
- package/docs/platform-adapters/expo-adapter/index.md +0 -245
- package/docs/platform-adapters/node-adapter/index.md +0 -160
- package/docs/platform-adapters/tauri-adapter/index.md +0 -15
- package/docs/platform-adapters/web-adapter/index.md +0 -218
- package/docs/sustainable-open-source/contributing/docs/index.md +0 -94
- package/docs/sustainable-open-source/contributing/info/index.md +0 -63
- package/docs/sustainable-open-source/contributing/monorepo/index.md +0 -195
- package/docs/sustainable-open-source/sponsoring/index.md +0 -104
- package/docs/sync-providers/cloudflare/index.md +0 -773
- package/docs/sync-providers/custom/index.md +0 -65
- package/docs/sync-providers/electricsql/index.md +0 -159
- package/docs/sync-providers/s2/index.md +0 -230
- package/docs/tutorial/0-welcome/index.md +0 -48
- package/docs/tutorial/1-setup-starter-project/index.md +0 -105
- package/docs/tutorial/2-deploy-to-cloudflare/index.md +0 -195
- package/docs/tutorial/3-read-and-write-todos-via-livestore/index.md +0 -511
- package/docs/tutorial/4-sync-data-via-cloudflare/index.md +0 -210
- package/docs/tutorial/5-expand-business-logic/index.md +0 -174
- package/docs/tutorial/6-persist-ui-state/index.md +0 -453
- package/docs/tutorial/7-next-steps/index.md +0 -22
- package/docs/understanding-livestore/design-decisions/index.md +0 -33
- package/docs/understanding-livestore/event-sourcing/index.md +0 -40
|
@@ -1,306 +0,0 @@
|
|
|
1
|
-
# SQLite state schema
|
|
2
|
-
|
|
3
|
-
LiveStore provides a schema definition language for defining your database tables and mutation definitions using explicit column configurations. LiveStore automatically migrates your database schema when you change your schema definitions.
|
|
4
|
-
|
|
5
|
-
> **Alternative Approach**: You can also define tables using [Effect Schema with annotations](/building-with-livestore/state/sqlite-schema-effect) for type-safe schema definitions.
|
|
6
|
-
|
|
7
|
-
### Example
|
|
8
|
-
|
|
9
|
-
<Tabs syncKey="package-manager">
|
|
10
|
-
<TabItem label="schema.ts">
|
|
11
|
-
|
|
12
|
-
## `reference/state/sqlite-schema/schema.ts`
|
|
13
|
-
|
|
14
|
-
```ts filename="reference/state/sqlite-schema/schema.ts"
|
|
15
|
-
|
|
16
|
-
// You can model your state as SQLite tables (https://docs.livestore.dev/reference/state/sqlite-schema)
|
|
17
|
-
export const tables = {
|
|
18
|
-
todos: State.SQLite.table({
|
|
19
|
-
name: 'todos',
|
|
20
|
-
columns: {
|
|
21
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
22
|
-
text: State.SQLite.text({ default: '' }),
|
|
23
|
-
completed: State.SQLite.boolean({ default: false }),
|
|
24
|
-
deletedAt: State.SQLite.integer({ nullable: true, schema: Schema.DateFromNumber }),
|
|
25
|
-
},
|
|
26
|
-
}),
|
|
27
|
-
// Client documents can be used for local-only state (e.g. form inputs)
|
|
28
|
-
uiState: State.SQLite.clientDocument({
|
|
29
|
-
name: 'uiState',
|
|
30
|
-
schema: Schema.Struct({ newTodoText: Schema.String, filter: Schema.Literal('all', 'active', 'completed') }),
|
|
31
|
-
default: { id: SessionIdSymbol, value: { newTodoText: '', filter: 'all' } },
|
|
32
|
-
}),
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Events describe data changes (https://docs.livestore.dev/reference/events)
|
|
36
|
-
export const events = {
|
|
37
|
-
todoCreated: Events.synced({
|
|
38
|
-
name: 'v1.TodoCreated',
|
|
39
|
-
schema: Schema.Struct({ id: Schema.String, text: Schema.String }),
|
|
40
|
-
}),
|
|
41
|
-
todoCompleted: Events.synced({
|
|
42
|
-
name: 'v1.TodoCompleted',
|
|
43
|
-
schema: Schema.Struct({ id: Schema.String }),
|
|
44
|
-
}),
|
|
45
|
-
todoUncompleted: Events.synced({
|
|
46
|
-
name: 'v1.TodoUncompleted',
|
|
47
|
-
schema: Schema.Struct({ id: Schema.String }),
|
|
48
|
-
}),
|
|
49
|
-
todoDeleted: Events.synced({
|
|
50
|
-
name: 'v1.TodoDeleted',
|
|
51
|
-
schema: Schema.Struct({ id: Schema.String, deletedAt: Schema.Date }),
|
|
52
|
-
}),
|
|
53
|
-
todoClearedCompleted: Events.synced({
|
|
54
|
-
name: 'v1.TodoClearedCompleted',
|
|
55
|
-
schema: Schema.Struct({ deletedAt: Schema.Date }),
|
|
56
|
-
}),
|
|
57
|
-
uiStateSet: tables.uiState.set,
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Materializers are used to map events to state (https://docs.livestore.dev/reference/state/materializers)
|
|
61
|
-
const materializers = State.SQLite.materializers(events, {
|
|
62
|
-
'v1.TodoCreated': ({ id, text }) => tables.todos.insert({ id, text, completed: false }),
|
|
63
|
-
'v1.TodoCompleted': ({ id }) => tables.todos.update({ completed: true }).where({ id }),
|
|
64
|
-
'v1.TodoUncompleted': ({ id }) => tables.todos.update({ completed: false }).where({ id }),
|
|
65
|
-
'v1.TodoDeleted': ({ id, deletedAt }) => tables.todos.update({ deletedAt }).where({ id }),
|
|
66
|
-
'v1.TodoClearedCompleted': ({ deletedAt }) => tables.todos.update({ deletedAt }).where({ completed: true }),
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
const state = State.SQLite.makeState({ tables, materializers })
|
|
70
|
-
|
|
71
|
-
export const schema = makeSchema({ events, state })
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
</TabItem>
|
|
75
|
-
</Tabs>
|
|
76
|
-
|
|
77
|
-
## Defining tables
|
|
78
|
-
|
|
79
|
-
Define SQLite tables using explicit column definitions:
|
|
80
|
-
|
|
81
|
-
## `reference/state/sqlite-schema/columns/table-basic.ts`
|
|
82
|
-
|
|
83
|
-
```ts filename="reference/state/sqlite-schema/columns/table-basic.ts"
|
|
84
|
-
|
|
85
|
-
export const userTable = State.SQLite.table({
|
|
86
|
-
name: 'users',
|
|
87
|
-
columns: {
|
|
88
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
89
|
-
email: State.SQLite.text(),
|
|
90
|
-
name: State.SQLite.text(),
|
|
91
|
-
age: State.SQLite.integer({ default: 0 }),
|
|
92
|
-
isActive: State.SQLite.boolean({ default: true }),
|
|
93
|
-
metadata: State.SQLite.json({ nullable: true }),
|
|
94
|
-
},
|
|
95
|
-
indexes: [{ name: 'idx_users_email', columns: ['email'], isUnique: true }],
|
|
96
|
-
})
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
Use the optional `indexes` array to declare secondary indexes or enforce uniqueness (set `isUnique: true`).
|
|
100
|
-
|
|
101
|
-
## Column types
|
|
102
|
-
|
|
103
|
-
You can use these column types when defining tables:
|
|
104
|
-
|
|
105
|
-
### Core SQLite column types
|
|
106
|
-
|
|
107
|
-
- `State.SQLite.text`: A text field, returns `string`.
|
|
108
|
-
- `State.SQLite.integer`: An integer field, returns `number`.
|
|
109
|
-
- `State.SQLite.real`: A real field (floating point number), returns `number`.
|
|
110
|
-
- `State.SQLite.blob`: A blob field (binary data), returns `Uint8Array`.
|
|
111
|
-
|
|
112
|
-
### Higher level column types
|
|
113
|
-
|
|
114
|
-
- `State.SQLite.boolean`: An integer field that stores `0` for `false` and `1` for `true` and returns a `boolean`.
|
|
115
|
-
- `State.SQLite.json`: A text field that stores a stringified JSON object and returns a decoded JSON value.
|
|
116
|
-
- `State.SQLite.datetime`: A text field that stores dates as ISO 8601 strings and returns a `Date`.
|
|
117
|
-
- `State.SQLite.datetimeInteger`: A integer field that stores dates as the number of milliseconds since the epoch and returns a `Date`.
|
|
118
|
-
|
|
119
|
-
### Custom column schemas
|
|
120
|
-
|
|
121
|
-
You can also provide a custom schema for a column which is used to automatically encode and decode the column value.
|
|
122
|
-
|
|
123
|
-
### Example: JSON-encoded struct
|
|
124
|
-
|
|
125
|
-
## `reference/state/sqlite-schema/columns/json-struct.ts`
|
|
126
|
-
|
|
127
|
-
```ts filename="reference/state/sqlite-schema/columns/json-struct.ts"
|
|
128
|
-
|
|
129
|
-
export const UserMetadata = Schema.Struct({
|
|
130
|
-
petName: Schema.String,
|
|
131
|
-
favoriteColor: Schema.Literal('red', 'blue', 'green'),
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
export const userTable = State.SQLite.table({
|
|
135
|
-
name: 'user',
|
|
136
|
-
columns: {
|
|
137
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
138
|
-
name: State.SQLite.text(),
|
|
139
|
-
metadata: State.SQLite.json({ schema: UserMetadata }),
|
|
140
|
-
},
|
|
141
|
-
})
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
### Schema migrations
|
|
145
|
-
|
|
146
|
-
Migration strategies:
|
|
147
|
-
|
|
148
|
-
- `auto`: Automatically migrate the database to the newest schema and rematerializes the state from the eventlog.
|
|
149
|
-
- `manual`: Manually migrate the database to the newest schema.
|
|
150
|
-
|
|
151
|
-
## Client documents
|
|
152
|
-
|
|
153
|
-
- Meant for convenience
|
|
154
|
-
- Client-only
|
|
155
|
-
- Goal: Similar ease of use as `React.useState`
|
|
156
|
-
- When schema changes in a non-backwards compatible way, previous events are dropped and the state is reset
|
|
157
|
-
- Don't use client documents for sensitive data which must not be lost
|
|
158
|
-
- Implies
|
|
159
|
-
- Table with `id` and `value` columns
|
|
160
|
-
- `${MyTable}Set` event + materializer (which are auto-registered)
|
|
161
|
-
|
|
162
|
-
### Basic usage
|
|
163
|
-
|
|
164
|
-
## `reference/state/sqlite-schema/columns/client-document-basic.tsx`
|
|
165
|
-
|
|
166
|
-
```tsx filename="reference/state/sqlite-schema/columns/client-document-basic.tsx"
|
|
167
|
-
|
|
168
|
-
export const uiState = State.SQLite.clientDocument({
|
|
169
|
-
name: 'UiState',
|
|
170
|
-
schema: Schema.Struct({
|
|
171
|
-
newTodoText: Schema.String,
|
|
172
|
-
filter: Schema.Literal('all', 'active', 'completed'),
|
|
173
|
-
}),
|
|
174
|
-
default: { id: SessionIdSymbol, value: { newTodoText: '', filter: 'all' } },
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
export const readUiState = (store: Store): { newTodoText: string; filter: 'all' | 'active' | 'completed' } =>
|
|
178
|
-
store.query(uiState.get())
|
|
179
|
-
|
|
180
|
-
export const setNewTodoText = (store: Store, newTodoText: string): void => {
|
|
181
|
-
store.commit(uiState.set({ newTodoText }))
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
export const UiStateFilter: React.FC = () => {
|
|
185
|
-
const [state, setState] = useClientDocument(uiState)
|
|
186
|
-
|
|
187
|
-
const showActive = React.useCallback(() => {
|
|
188
|
-
setState({ filter: 'active' })
|
|
189
|
-
}, [setState])
|
|
190
|
-
|
|
191
|
-
const showAll = React.useCallback(() => {
|
|
192
|
-
setState({ filter: 'all' })
|
|
193
|
-
}, [setState])
|
|
194
|
-
|
|
195
|
-
return (
|
|
196
|
-
<div>
|
|
197
|
-
<button type="button" onClick={showAll}>
|
|
198
|
-
All
|
|
199
|
-
</button>
|
|
200
|
-
<button type="button" onClick={showActive}>
|
|
201
|
-
Active ({state.filter === 'active' ? 'selected' : 'select'})
|
|
202
|
-
</button>
|
|
203
|
-
</div>
|
|
204
|
-
)
|
|
205
|
-
}
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
### KV-style client document
|
|
209
|
-
|
|
210
|
-
Sometimes you want a simple key-value store for arbitrary values without partial merging. You can model this by using `Schema.Any` as the value schema. With `Schema.Any`, updates fully replace the stored value (no partial merge semantics).
|
|
211
|
-
|
|
212
|
-
## `reference/state/sqlite-schema/columns/client-document-kv.tsx`
|
|
213
|
-
|
|
214
|
-
```tsx filename="reference/state/sqlite-schema/columns/client-document-kv.tsx"
|
|
215
|
-
|
|
216
|
-
export const kv = State.SQLite.clientDocument({
|
|
217
|
-
name: 'Kv',
|
|
218
|
-
schema: Schema.Any,
|
|
219
|
-
default: { value: null },
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
export const readKvValue = (store: Store, id: string): unknown => store.query(kv.get(id))
|
|
223
|
-
|
|
224
|
-
export const setKvValue = (store: Store, id: string, value: unknown): void => {
|
|
225
|
-
store.commit(kv.set(value, id))
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
export const KvViewer: React.FC<{ id: string }> = ({ id }) => {
|
|
229
|
-
const [value, setValue] = useClientDocument(kv, id)
|
|
230
|
-
|
|
231
|
-
return (
|
|
232
|
-
<button type="button" onClick={() => setValue('hello')}>
|
|
233
|
-
Current value: {JSON.stringify(value)}
|
|
234
|
-
</button>
|
|
235
|
-
)
|
|
236
|
-
}
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
## Column types
|
|
240
|
-
|
|
241
|
-
You can use these column types:
|
|
242
|
-
|
|
243
|
-
#### Core SQLite column types
|
|
244
|
-
|
|
245
|
-
- `State.SQLite.text`: A text field, returns `string`.
|
|
246
|
-
- `State.SQLite.integer`: An integer field, returns `number`.
|
|
247
|
-
- `State.SQLite.real`: A real field (floating point number), returns `number`.
|
|
248
|
-
- `State.SQLite.blob`: A blob field (binary data), returns `Uint8Array`.
|
|
249
|
-
|
|
250
|
-
#### Higher level column types
|
|
251
|
-
|
|
252
|
-
- `State.SQLite.boolean`: An integer field that stores `0` for `false` and `1` for `true` and returns a `boolean`.
|
|
253
|
-
- `State.SQLite.json`: A text field that stores a stringified JSON object and returns a decoded JSON value.
|
|
254
|
-
- `State.SQLite.datetime`: A text field that stores dates as ISO 8601 strings and returns a `Date`.
|
|
255
|
-
- `State.SQLite.datetimeInteger`: A integer field that stores dates as the number of milliseconds since the epoch and returns a `Date`.
|
|
256
|
-
|
|
257
|
-
#### Custom column schemas
|
|
258
|
-
|
|
259
|
-
You can also provide a custom schema for a column which is used to automatically encode and decode the column value.
|
|
260
|
-
|
|
261
|
-
#### Example: JSON-encoded struct
|
|
262
|
-
|
|
263
|
-
## `reference/state/sqlite-schema/columns/json-struct.ts`
|
|
264
|
-
|
|
265
|
-
```ts filename="reference/state/sqlite-schema/columns/json-struct.ts"
|
|
266
|
-
|
|
267
|
-
export const UserMetadata = Schema.Struct({
|
|
268
|
-
petName: Schema.String,
|
|
269
|
-
favoriteColor: Schema.Literal('red', 'blue', 'green'),
|
|
270
|
-
})
|
|
271
|
-
|
|
272
|
-
export const userTable = State.SQLite.table({
|
|
273
|
-
name: 'user',
|
|
274
|
-
columns: {
|
|
275
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
276
|
-
name: State.SQLite.text(),
|
|
277
|
-
metadata: State.SQLite.json({ schema: UserMetadata }),
|
|
278
|
-
},
|
|
279
|
-
})
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
## Best practices
|
|
283
|
-
|
|
284
|
-
### Column configuration
|
|
285
|
-
|
|
286
|
-
- Use appropriate SQLite column types for your data (text, integer, real, blob)
|
|
287
|
-
- Set `primaryKey: true` for primary key columns
|
|
288
|
-
- Use `nullable: true` for columns that can contain NULL values
|
|
289
|
-
- Provide meaningful `default` values where appropriate
|
|
290
|
-
- Add unique constraints via table `indexes` using `isUnique: true`
|
|
291
|
-
|
|
292
|
-
### Schema design
|
|
293
|
-
|
|
294
|
-
- Choose column types that match your data requirements
|
|
295
|
-
- Use custom schemas with `State.SQLite.json()` for complex data structures
|
|
296
|
-
- Group related table definitions in the same module
|
|
297
|
-
- Use descriptive table and column names
|
|
298
|
-
|
|
299
|
-
### General practices
|
|
300
|
-
|
|
301
|
-
- It's usually recommend to **not distinguish** between app state vs app data but rather keep all state in LiveStore.
|
|
302
|
-
- This means you'll rarely use `React.useState` when using LiveStore
|
|
303
|
-
- In some cases for "fast changing values" it can make sense to keep a version of a state value outside of LiveStore with a reactive setter for React and a debounced setter for LiveStore to avoid excessive LiveStore mutations. Cases where this can make sense can include:
|
|
304
|
-
- Text input / rich text editing
|
|
305
|
-
- Scroll position tracking, resize events, move/drag events
|
|
306
|
-
- ...
|
|
@@ -1,300 +0,0 @@
|
|
|
1
|
-
# SQLite state schema (Effect schema)
|
|
2
|
-
|
|
3
|
-
LiveStore supports defining SQLite tables using Effect Schema with annotations for database constraints. This approach provides strong type safety, composability, and automatic type mapping from TypeScript to SQLite.
|
|
4
|
-
|
|
5
|
-
> **Note**: This approach will become the default once Effect Schema v4 is released. See [livestore#382](https://github.com/livestorejs/livestore/issues/382) for details.
|
|
6
|
-
>
|
|
7
|
-
> For the traditional column-based approach, see [SQLite State Schema](/building-with-livestore/state/sqlite-schema).
|
|
8
|
-
|
|
9
|
-
## Basic usage
|
|
10
|
-
|
|
11
|
-
Define tables using Effect Schema with database constraint annotations:
|
|
12
|
-
|
|
13
|
-
## `reference/state/sqlite-schema/effect/basic.ts`
|
|
14
|
-
|
|
15
|
-
```ts filename="reference/state/sqlite-schema/effect/basic.ts"
|
|
16
|
-
|
|
17
|
-
const UserSchema = Schema.Struct({
|
|
18
|
-
id: Schema.String.pipe(State.SQLite.withPrimaryKey),
|
|
19
|
-
email: Schema.String.pipe(State.SQLite.withUnique),
|
|
20
|
-
name: Schema.String,
|
|
21
|
-
age: Schema.Int.pipe(State.SQLite.withDefault(0)),
|
|
22
|
-
isActive: Schema.Boolean.pipe(State.SQLite.withDefault(true)),
|
|
23
|
-
metadata: Schema.optional(
|
|
24
|
-
Schema.Record({
|
|
25
|
-
key: Schema.String,
|
|
26
|
-
value: Schema.Unknown,
|
|
27
|
-
}),
|
|
28
|
-
),
|
|
29
|
-
}).annotations({ title: 'users' })
|
|
30
|
-
|
|
31
|
-
export const userTable = State.SQLite.table({ schema: UserSchema })
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
## Schema annotations
|
|
35
|
-
|
|
36
|
-
You can annotate schema fields with database constraints:
|
|
37
|
-
|
|
38
|
-
### Primary keys
|
|
39
|
-
|
|
40
|
-
## `reference/state/sqlite-schema/effect/primary-key.ts`
|
|
41
|
-
|
|
42
|
-
```ts filename="reference/state/sqlite-schema/effect/primary-key.ts"
|
|
43
|
-
|
|
44
|
-
const _schema = Schema.Struct({
|
|
45
|
-
id: Schema.String.pipe(State.SQLite.withPrimaryKey),
|
|
46
|
-
// Other fields...
|
|
47
|
-
})
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
**Important**: Primary key columns cannot be nullable. This will throw an error:
|
|
51
|
-
|
|
52
|
-
## `reference/state/sqlite-schema/effect/primary-key-nullable.ts`
|
|
53
|
-
|
|
54
|
-
```ts filename="reference/state/sqlite-schema/effect/primary-key-nullable.ts"
|
|
55
|
-
|
|
56
|
-
// ❌ This will throw an error at runtime because primary keys cannot be nullable
|
|
57
|
-
const _badSchema = Schema.Struct({
|
|
58
|
-
id: Schema.NullOr(Schema.String).pipe(State.SQLite.withPrimaryKey),
|
|
59
|
-
})
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
### Auto-Increment
|
|
63
|
-
|
|
64
|
-
## `reference/state/sqlite-schema/effect/auto-increment.ts`
|
|
65
|
-
|
|
66
|
-
```ts filename="reference/state/sqlite-schema/effect/auto-increment.ts"
|
|
67
|
-
|
|
68
|
-
const _schema = Schema.Struct({
|
|
69
|
-
id: Schema.Int.pipe(State.SQLite.withPrimaryKey, State.SQLite.withAutoIncrement),
|
|
70
|
-
// Other fields...
|
|
71
|
-
})
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
### Default values
|
|
75
|
-
|
|
76
|
-
## `reference/state/sqlite-schema/effect/default-values.ts`
|
|
77
|
-
|
|
78
|
-
```ts filename="reference/state/sqlite-schema/effect/default-values.ts"
|
|
79
|
-
|
|
80
|
-
const _schema = Schema.Struct({
|
|
81
|
-
status: Schema.String.pipe(State.SQLite.withDefault('active')),
|
|
82
|
-
createdAt: Schema.String.pipe(State.SQLite.withDefault('CURRENT_TIMESTAMP')),
|
|
83
|
-
count: Schema.Int.pipe(State.SQLite.withDefault(0)),
|
|
84
|
-
})
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
### Unique constraints
|
|
88
|
-
|
|
89
|
-
## `reference/state/sqlite-schema/effect/unique-constraints.ts`
|
|
90
|
-
|
|
91
|
-
```ts filename="reference/state/sqlite-schema/effect/unique-constraints.ts"
|
|
92
|
-
|
|
93
|
-
const _schema = Schema.Struct({
|
|
94
|
-
email: Schema.String.pipe(State.SQLite.withUnique),
|
|
95
|
-
username: Schema.String.pipe(State.SQLite.withUnique),
|
|
96
|
-
})
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
Unique annotations automatically create unique indexes.
|
|
100
|
-
|
|
101
|
-
### Custom column types
|
|
102
|
-
|
|
103
|
-
Override the automatically inferred SQLite column type:
|
|
104
|
-
|
|
105
|
-
## `reference/state/sqlite-schema/effect/custom-column-types.ts`
|
|
106
|
-
|
|
107
|
-
```ts filename="reference/state/sqlite-schema/effect/custom-column-types.ts"
|
|
108
|
-
|
|
109
|
-
const _schema = Schema.Struct({
|
|
110
|
-
// Store a number as text instead of real
|
|
111
|
-
version: Schema.Number.pipe(State.SQLite.withColumnType('text')),
|
|
112
|
-
// Store binary data as blob
|
|
113
|
-
data: Schema.Uint8Array.pipe(State.SQLite.withColumnType('blob')),
|
|
114
|
-
})
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
### Combining annotations
|
|
118
|
-
|
|
119
|
-
Annotations can be chained together:
|
|
120
|
-
|
|
121
|
-
## `reference/state/sqlite-schema/effect/combining-annotations.ts`
|
|
122
|
-
|
|
123
|
-
```ts filename="reference/state/sqlite-schema/effect/combining-annotations.ts"
|
|
124
|
-
|
|
125
|
-
const _schema = Schema.Struct({
|
|
126
|
-
id: Schema.Int.pipe(State.SQLite.withPrimaryKey, State.SQLite.withAutoIncrement),
|
|
127
|
-
email: Schema.String.pipe(State.SQLite.withUnique, State.SQLite.withColumnType('text')),
|
|
128
|
-
})
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
## Table naming
|
|
132
|
-
|
|
133
|
-
You can specify table names in several ways:
|
|
134
|
-
|
|
135
|
-
### Using schema annotations
|
|
136
|
-
|
|
137
|
-
## `reference/state/sqlite-schema/effect/table-name-annotations.ts`
|
|
138
|
-
|
|
139
|
-
```ts filename="reference/state/sqlite-schema/effect/table-name-annotations.ts"
|
|
140
|
-
|
|
141
|
-
// Using title annotation
|
|
142
|
-
const UserSchema = Schema.Struct({
|
|
143
|
-
id: Schema.String.pipe(State.SQLite.withPrimaryKey),
|
|
144
|
-
name: Schema.String,
|
|
145
|
-
}).annotations({ title: 'users' })
|
|
146
|
-
|
|
147
|
-
export const userTable = State.SQLite.table({ schema: UserSchema })
|
|
148
|
-
|
|
149
|
-
// Using identifier annotation
|
|
150
|
-
const PostSchema = Schema.Struct({
|
|
151
|
-
id: Schema.String.pipe(State.SQLite.withPrimaryKey),
|
|
152
|
-
title: Schema.String,
|
|
153
|
-
}).annotations({ identifier: 'posts' })
|
|
154
|
-
|
|
155
|
-
export const postTable = State.SQLite.table({ schema: PostSchema })
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
### Explicit name
|
|
159
|
-
|
|
160
|
-
## `reference/state/sqlite-schema/effect/table-name-explicit.ts`
|
|
161
|
-
|
|
162
|
-
```ts filename="reference/state/sqlite-schema/effect/table-name-explicit.ts"
|
|
163
|
-
|
|
164
|
-
const UserSchema = Schema.Struct({
|
|
165
|
-
id: Schema.String.pipe(State.SQLite.withPrimaryKey),
|
|
166
|
-
name: Schema.String,
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
export const userTable = State.SQLite.table({
|
|
170
|
-
name: 'users',
|
|
171
|
-
schema: UserSchema,
|
|
172
|
-
})
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
**Note**: Title annotation takes precedence over identifier annotation.
|
|
176
|
-
|
|
177
|
-
## Type mapping
|
|
178
|
-
|
|
179
|
-
Effect Schema types are automatically mapped to SQLite column types:
|
|
180
|
-
|
|
181
|
-
| Schema Type | SQLite Type | TypeScript Type |
|
|
182
|
-
|-------------|-------------|-----------------|
|
|
183
|
-
| `Schema.String` | `text` | `string` |
|
|
184
|
-
| `Schema.Number` | `real` | `number` |
|
|
185
|
-
| `Schema.Int` | `integer` | `number` |
|
|
186
|
-
| `Schema.Boolean` | `integer` | `boolean` |
|
|
187
|
-
| `Schema.Date` | `text` | `Date` |
|
|
188
|
-
| `Schema.BigInt` | `text` | `bigint` |
|
|
189
|
-
| Complex types (Struct, Array, etc.) | `text` (JSON encoded) | Decoded type |
|
|
190
|
-
| `Schema.optional(T)` | Nullable column | `T \| undefined` |
|
|
191
|
-
| `Schema.NullOr(T)` | Nullable column | `T \| null` |
|
|
192
|
-
|
|
193
|
-
## Advanced examples
|
|
194
|
-
|
|
195
|
-
### Complex schema with multiple constraints
|
|
196
|
-
|
|
197
|
-
## `reference/state/sqlite-schema/effect/advanced-product.ts`
|
|
198
|
-
|
|
199
|
-
```ts filename="reference/state/sqlite-schema/effect/advanced-product.ts"
|
|
200
|
-
|
|
201
|
-
const ProductSchema = Schema.Struct({
|
|
202
|
-
id: Schema.Int.pipe(State.SQLite.withPrimaryKey, State.SQLite.withAutoIncrement),
|
|
203
|
-
sku: Schema.String.pipe(State.SQLite.withUnique),
|
|
204
|
-
name: Schema.String,
|
|
205
|
-
price: Schema.Number.pipe(State.SQLite.withDefault(0)),
|
|
206
|
-
category: Schema.Literal('electronics', 'clothing', 'books'),
|
|
207
|
-
metadata: Schema.optional(
|
|
208
|
-
Schema.Struct({
|
|
209
|
-
weight: Schema.Number,
|
|
210
|
-
dimensions: Schema.Struct({
|
|
211
|
-
width: Schema.Number,
|
|
212
|
-
height: Schema.Number,
|
|
213
|
-
depth: Schema.Number,
|
|
214
|
-
}),
|
|
215
|
-
}),
|
|
216
|
-
),
|
|
217
|
-
isActive: Schema.Boolean.pipe(State.SQLite.withDefault(true)),
|
|
218
|
-
createdAt: Schema.Date.pipe(State.SQLite.withDefault('CURRENT_TIMESTAMP')),
|
|
219
|
-
}).annotations({ title: 'products' })
|
|
220
|
-
|
|
221
|
-
export const productTable = State.SQLite.table({ schema: ProductSchema })
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
### Working with Schema.Class
|
|
225
|
-
|
|
226
|
-
## `reference/state/sqlite-schema/effect/schema-class.ts`
|
|
227
|
-
|
|
228
|
-
```ts filename="reference/state/sqlite-schema/effect/schema-class.ts"
|
|
229
|
-
|
|
230
|
-
class User extends Schema.Class<User>('User')({
|
|
231
|
-
id: Schema.String.pipe(State.SQLite.withPrimaryKey),
|
|
232
|
-
email: Schema.String.pipe(State.SQLite.withUnique),
|
|
233
|
-
name: Schema.String,
|
|
234
|
-
age: Schema.Int,
|
|
235
|
-
}) {}
|
|
236
|
-
|
|
237
|
-
export const userTable = State.SQLite.table({
|
|
238
|
-
name: 'users',
|
|
239
|
-
schema: User,
|
|
240
|
-
})
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
### Custom indexes
|
|
244
|
-
|
|
245
|
-
## `reference/state/sqlite-schema/effect/custom-indexes.ts`
|
|
246
|
-
|
|
247
|
-
```ts filename="reference/state/sqlite-schema/effect/custom-indexes.ts"
|
|
248
|
-
|
|
249
|
-
const PostSchema = Schema.Struct({
|
|
250
|
-
id: Schema.String.pipe(State.SQLite.withPrimaryKey),
|
|
251
|
-
title: Schema.String,
|
|
252
|
-
authorId: Schema.String,
|
|
253
|
-
createdAt: Schema.Date,
|
|
254
|
-
}).annotations({ title: 'posts' })
|
|
255
|
-
|
|
256
|
-
export const postTable = State.SQLite.table({
|
|
257
|
-
schema: PostSchema,
|
|
258
|
-
indexes: [
|
|
259
|
-
{ name: 'idx_posts_author', columns: ['authorId'] },
|
|
260
|
-
{ name: 'idx_posts_created', columns: ['createdAt'] },
|
|
261
|
-
],
|
|
262
|
-
})
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
## Best Practices
|
|
266
|
-
|
|
267
|
-
### Schema Design
|
|
268
|
-
|
|
269
|
-
- Always use `withPrimaryKey` for primary key columns - never combine it with nullable types
|
|
270
|
-
- Use `Schema.optional()` for truly optional fields that can be undefined
|
|
271
|
-
- Use `Schema.NullOr()` for fields that can explicitly be set to null
|
|
272
|
-
- Leverage schema annotations like `title` or `identifier` to avoid repeating table names
|
|
273
|
-
- Group related schemas in the same module for better organization
|
|
274
|
-
|
|
275
|
-
### Type safety
|
|
276
|
-
|
|
277
|
-
- Let TypeScript infer table types rather than explicitly typing them
|
|
278
|
-
- Use Effect Schema's refinements and transformations for data validation
|
|
279
|
-
- Prefer Effect Schema's built-in types (`Schema.Int`, `Schema.Date`) over generic types where appropriate
|
|
280
|
-
|
|
281
|
-
### Performance
|
|
282
|
-
|
|
283
|
-
- Be mindful of complex types stored as JSON - they can impact query performance
|
|
284
|
-
- Use appropriate indexes for frequently queried columns
|
|
285
|
-
- Consider using `withColumnType` to optimize storage for specific use cases
|
|
286
|
-
|
|
287
|
-
## When to Use This Approach
|
|
288
|
-
|
|
289
|
-
**Use Effect Schema-based tables when:**
|
|
290
|
-
- You already have Effect Schema definitions to reuse
|
|
291
|
-
- You prefer Effect Schema's composability and transformations
|
|
292
|
-
- Your schemas are shared across different parts of your application
|
|
293
|
-
- You want automatic type mapping and strong type safety
|
|
294
|
-
- You plan to migrate to Effect Schema v4 when it becomes available
|
|
295
|
-
|
|
296
|
-
**Consider column-based tables when:**
|
|
297
|
-
- You need precise control over SQLite column types
|
|
298
|
-
- You're migrating from existing SQLite schemas
|
|
299
|
-
- You prefer explicit column configuration
|
|
300
|
-
- You're not already using Effect Schema extensively in your project
|