@tanstack/tauri-db-sqlite-persistence 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -0
- package/dist/cjs/index.cjs +10 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/cjs/index.d.cts +4 -0
- package/dist/cjs/tauri-persistence.cjs +86 -0
- package/dist/cjs/tauri-persistence.cjs.map +1 -0
- package/dist/cjs/tauri-persistence.d.cts +12 -0
- package/dist/cjs/tauri-sql-driver.cjs +207 -0
- package/dist/cjs/tauri-sql-driver.cjs.map +1 -0
- package/dist/cjs/tauri-sql-driver.d.cts +24 -0
- package/dist/cjs/tauri.cjs +8 -0
- package/dist/cjs/tauri.cjs.map +1 -0
- package/dist/cjs/tauri.d.cts +6 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/tauri-persistence.d.ts +12 -0
- package/dist/esm/tauri-persistence.js +86 -0
- package/dist/esm/tauri-persistence.js.map +1 -0
- package/dist/esm/tauri-sql-driver.d.ts +24 -0
- package/dist/esm/tauri-sql-driver.js +207 -0
- package/dist/esm/tauri-sql-driver.js.map +1 -0
- package/dist/esm/tauri.d.ts +6 -0
- package/dist/esm/tauri.js +8 -0
- package/dist/esm/tauri.js.map +1 -0
- package/package.json +71 -0
- package/src/index.ts +11 -0
- package/src/tauri-persistence.ts +173 -0
- package/src/tauri-sql-driver.ts +290 -0
- package/src/tauri.ts +20 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { InvalidPersistedCollectionConfigError } from '@tanstack/db-sqlite-persistence-core'
|
|
2
|
+
import type { SQLiteDriver } from '@tanstack/db-sqlite-persistence-core'
|
|
3
|
+
import type Database from '@tauri-apps/plugin-sql'
|
|
4
|
+
|
|
5
|
+
export type TauriSQLiteDatabaseLike = Pick<
|
|
6
|
+
Database,
|
|
7
|
+
`execute` | `select` | `close` | `path`
|
|
8
|
+
>
|
|
9
|
+
|
|
10
|
+
export type TauriSQLiteDriverOptions = {
|
|
11
|
+
database: TauriSQLiteDatabaseLike
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function assertTransactionCallbackHasDriverArg(
|
|
15
|
+
fn: (transactionDriver: SQLiteDriver) => Promise<unknown>,
|
|
16
|
+
): void {
|
|
17
|
+
if (fn.length > 0) {
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
22
|
+
`SQLiteDriver.transaction callback must accept the transaction driver argument`,
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isTauriSQLiteDatabaseLike(
|
|
27
|
+
value: unknown,
|
|
28
|
+
): value is TauriSQLiteDatabaseLike {
|
|
29
|
+
const candidate = value as Partial<TauriSQLiteDatabaseLike>
|
|
30
|
+
return (
|
|
31
|
+
typeof value === `object` &&
|
|
32
|
+
value !== null &&
|
|
33
|
+
typeof candidate.path === `string` &&
|
|
34
|
+
typeof candidate.execute === `function` &&
|
|
35
|
+
typeof candidate.select === `function`
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeQueryRows<TRow>(
|
|
40
|
+
rows: unknown,
|
|
41
|
+
sql: string,
|
|
42
|
+
): ReadonlyArray<TRow> {
|
|
43
|
+
if (Array.isArray(rows)) {
|
|
44
|
+
return rows as ReadonlyArray<TRow>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
48
|
+
`Unsupported Tauri SQL query result shape for SQL "${sql}"`,
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function convertSqlitePlaceholdersToTauri(sql: string): string {
|
|
53
|
+
let result = ``
|
|
54
|
+
let parameterIndex = 1
|
|
55
|
+
let inSingleQuote = false
|
|
56
|
+
let inDoubleQuote = false
|
|
57
|
+
let inLineComment = false
|
|
58
|
+
let inBlockComment = false
|
|
59
|
+
|
|
60
|
+
for (let index = 0; index < sql.length; index++) {
|
|
61
|
+
const currentChar = sql[index]
|
|
62
|
+
const nextChar = sql[index + 1]
|
|
63
|
+
|
|
64
|
+
if (inLineComment) {
|
|
65
|
+
result += currentChar
|
|
66
|
+
if (currentChar === `\n`) {
|
|
67
|
+
inLineComment = false
|
|
68
|
+
}
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (inBlockComment) {
|
|
73
|
+
result += currentChar
|
|
74
|
+
if (currentChar === `*` && nextChar === `/`) {
|
|
75
|
+
result += `/`
|
|
76
|
+
index++
|
|
77
|
+
inBlockComment = false
|
|
78
|
+
}
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!inSingleQuote && !inDoubleQuote) {
|
|
83
|
+
if (currentChar === `-` && nextChar === `-`) {
|
|
84
|
+
result += `--`
|
|
85
|
+
index++
|
|
86
|
+
inLineComment = true
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (currentChar === `/` && nextChar === `*`) {
|
|
91
|
+
result += `/*`
|
|
92
|
+
index++
|
|
93
|
+
inBlockComment = true
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (currentChar === `'` && !inDoubleQuote) {
|
|
99
|
+
result += currentChar
|
|
100
|
+
if (inSingleQuote && nextChar === `'`) {
|
|
101
|
+
result += `'`
|
|
102
|
+
index++
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
inSingleQuote = !inSingleQuote
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (currentChar === `"` && !inSingleQuote) {
|
|
110
|
+
result += currentChar
|
|
111
|
+
if (inDoubleQuote && nextChar === `"`) {
|
|
112
|
+
result += `"`
|
|
113
|
+
index++
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
inDoubleQuote = !inDoubleQuote
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (currentChar === `?` && !inSingleQuote && !inDoubleQuote) {
|
|
121
|
+
result += `$${String(parameterIndex)}`
|
|
122
|
+
parameterIndex++
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
result += currentChar
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return result
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export class TauriSQLiteDriver implements SQLiteDriver {
|
|
133
|
+
private readonly database: TauriSQLiteDatabaseLike
|
|
134
|
+
private queue: Promise<void> = Promise.resolve()
|
|
135
|
+
private nextSavepointId = 1
|
|
136
|
+
|
|
137
|
+
constructor(options: TauriSQLiteDriverOptions) {
|
|
138
|
+
if (!isTauriSQLiteDatabaseLike(options.database)) {
|
|
139
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
140
|
+
`Tauri SQLite database object must provide execute/select methods`,
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.database = options.database
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async exec(sql: string): Promise<void> {
|
|
148
|
+
await this.enqueue(async () => {
|
|
149
|
+
await this.executeStatement(sql)
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async query<TRow>(
|
|
154
|
+
sql: string,
|
|
155
|
+
params: ReadonlyArray<unknown> = [],
|
|
156
|
+
): Promise<ReadonlyArray<TRow>> {
|
|
157
|
+
return this.enqueue(async () => {
|
|
158
|
+
const rows = await this.database.select<ReadonlyArray<TRow>>(
|
|
159
|
+
convertSqlitePlaceholdersToTauri(sql),
|
|
160
|
+
params.length > 0 ? [...params] : undefined,
|
|
161
|
+
)
|
|
162
|
+
return normalizeQueryRows<TRow>(rows, sql)
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async run(sql: string, params: ReadonlyArray<unknown> = []): Promise<void> {
|
|
167
|
+
await this.enqueue(async () => {
|
|
168
|
+
await this.executeStatement(sql, params)
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async transaction<TReturn>(
|
|
173
|
+
fn: (transactionDriver: SQLiteDriver) => Promise<TReturn>,
|
|
174
|
+
): Promise<TReturn> {
|
|
175
|
+
assertTransactionCallbackHasDriverArg(fn)
|
|
176
|
+
return this.transactionWithDriver(fn)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async transactionWithDriver<TReturn>(
|
|
180
|
+
fn: (transactionDriver: SQLiteDriver) => Promise<TReturn>,
|
|
181
|
+
): Promise<TReturn> {
|
|
182
|
+
return this.enqueue(async () => {
|
|
183
|
+
await this.executeStatement(`BEGIN IMMEDIATE`)
|
|
184
|
+
const transactionDriver = this.createTransactionDriver()
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const result = await fn(transactionDriver)
|
|
188
|
+
await this.executeStatement(`COMMIT`)
|
|
189
|
+
return result
|
|
190
|
+
} catch (error) {
|
|
191
|
+
try {
|
|
192
|
+
await this.executeStatement(`ROLLBACK`)
|
|
193
|
+
} catch {
|
|
194
|
+
// Keep the original transaction error as the primary failure.
|
|
195
|
+
}
|
|
196
|
+
throw error
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async close(): Promise<void> {
|
|
202
|
+
if (typeof this.database.close !== `function`) {
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
await Promise.resolve(this.database.close(this.database.path))
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
getDatabase(): TauriSQLiteDatabaseLike {
|
|
210
|
+
return this.database
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private async executeStatement(
|
|
214
|
+
sql: string,
|
|
215
|
+
params: ReadonlyArray<unknown> = [],
|
|
216
|
+
): Promise<void> {
|
|
217
|
+
await this.database.execute(
|
|
218
|
+
convertSqlitePlaceholdersToTauri(sql),
|
|
219
|
+
params.length > 0 ? [...params] : undefined,
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private enqueue<TReturn>(
|
|
224
|
+
operation: () => Promise<TReturn>,
|
|
225
|
+
): Promise<TReturn> {
|
|
226
|
+
const queuedOperation = this.queue.then(operation, operation)
|
|
227
|
+
this.queue = queuedOperation.then(
|
|
228
|
+
() => undefined,
|
|
229
|
+
() => undefined,
|
|
230
|
+
)
|
|
231
|
+
return queuedOperation
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private createTransactionDriver(): SQLiteDriver {
|
|
235
|
+
const transactionDriver: SQLiteDriver = {
|
|
236
|
+
exec: async (sql) => {
|
|
237
|
+
await this.executeStatement(sql)
|
|
238
|
+
},
|
|
239
|
+
query: async <TRow>(
|
|
240
|
+
sql: string,
|
|
241
|
+
params: ReadonlyArray<unknown> = [],
|
|
242
|
+
): Promise<ReadonlyArray<TRow>> => {
|
|
243
|
+
const rows = await this.database.select<ReadonlyArray<TRow>>(
|
|
244
|
+
convertSqlitePlaceholdersToTauri(sql),
|
|
245
|
+
params.length > 0 ? [...params] : undefined,
|
|
246
|
+
)
|
|
247
|
+
return normalizeQueryRows<TRow>(rows, sql)
|
|
248
|
+
},
|
|
249
|
+
run: async (sql, params = []) => {
|
|
250
|
+
await this.executeStatement(sql, params)
|
|
251
|
+
},
|
|
252
|
+
transaction: async <TReturn>(
|
|
253
|
+
fn: (transactionDriver: SQLiteDriver) => Promise<TReturn>,
|
|
254
|
+
): Promise<TReturn> => {
|
|
255
|
+
assertTransactionCallbackHasDriverArg(fn)
|
|
256
|
+
return this.runNestedTransaction(transactionDriver, fn)
|
|
257
|
+
},
|
|
258
|
+
transactionWithDriver: async <TReturn>(
|
|
259
|
+
fn: (transactionDriver: SQLiteDriver) => Promise<TReturn>,
|
|
260
|
+
): Promise<TReturn> => this.runNestedTransaction(transactionDriver, fn),
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return transactionDriver
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private async runNestedTransaction<TReturn>(
|
|
267
|
+
transactionDriver: SQLiteDriver,
|
|
268
|
+
fn: (transactionDriver: SQLiteDriver) => Promise<TReturn>,
|
|
269
|
+
): Promise<TReturn> {
|
|
270
|
+
const savepointName = `tsdb_sp_${this.nextSavepointId}`
|
|
271
|
+
this.nextSavepointId++
|
|
272
|
+
await this.executeStatement(`SAVEPOINT ${savepointName}`)
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
const result = await fn(transactionDriver)
|
|
276
|
+
await this.executeStatement(`RELEASE SAVEPOINT ${savepointName}`)
|
|
277
|
+
return result
|
|
278
|
+
} catch (error) {
|
|
279
|
+
await this.executeStatement(`ROLLBACK TO SAVEPOINT ${savepointName}`)
|
|
280
|
+
await this.executeStatement(`RELEASE SAVEPOINT ${savepointName}`)
|
|
281
|
+
throw error
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function createTauriSQLiteDriver(
|
|
287
|
+
options: TauriSQLiteDriverOptions,
|
|
288
|
+
): TauriSQLiteDriver {
|
|
289
|
+
return new TauriSQLiteDriver(options)
|
|
290
|
+
}
|
package/src/tauri.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createTauriSQLitePersistence as createPersistence } from './tauri-persistence'
|
|
2
|
+
import type {
|
|
3
|
+
TauriSQLitePersistenceOptions as TauriSQLitePersistenceOptionsBase,
|
|
4
|
+
TauriSQLiteSchemaMismatchPolicy as TauriSQLiteSchemaMismatchPolicyBase,
|
|
5
|
+
} from './tauri-persistence'
|
|
6
|
+
import type { PersistedCollectionPersistence } from '@tanstack/db-sqlite-persistence-core'
|
|
7
|
+
|
|
8
|
+
export type TauriSQLitePersistenceOptions = TauriSQLitePersistenceOptionsBase
|
|
9
|
+
export type TauriSQLiteSchemaMismatchPolicy =
|
|
10
|
+
TauriSQLiteSchemaMismatchPolicyBase
|
|
11
|
+
export type { TauriSQLiteDatabaseLike } from './tauri-persistence'
|
|
12
|
+
|
|
13
|
+
export function createTauriSQLitePersistence<
|
|
14
|
+
T extends object,
|
|
15
|
+
TKey extends string | number = string | number,
|
|
16
|
+
>(
|
|
17
|
+
options: TauriSQLitePersistenceOptions,
|
|
18
|
+
): PersistedCollectionPersistence<T, TKey> {
|
|
19
|
+
return createPersistence<T, TKey>(options)
|
|
20
|
+
}
|