@livestore/common 0.0.42-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 -0
- package/dist/database.d.ts +32 -0
- package/dist/database.d.ts.map +1 -0
- package/dist/database.js +2 -0
- package/dist/database.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/schema/index.d.ts +42 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +42 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/mutations.d.ts +81 -0
- package/dist/schema/mutations.d.ts.map +1 -0
- package/dist/schema/mutations.js +29 -0
- package/dist/schema/mutations.js.map +1 -0
- package/dist/schema/parse-utils.d.ts +6 -0
- package/dist/schema/parse-utils.d.ts.map +1 -0
- package/dist/schema/parse-utils.js +22 -0
- package/dist/schema/parse-utils.js.map +1 -0
- package/dist/schema/system-tables.d.ts +76 -0
- package/dist/schema/system-tables.d.ts.map +1 -0
- package/dist/schema/system-tables.js +12 -0
- package/dist/schema/system-tables.js.map +1 -0
- package/dist/schema/table-def.d.ts +100 -0
- package/dist/schema/table-def.d.ts.map +1 -0
- package/dist/schema/table-def.js +76 -0
- package/dist/schema/table-def.js.map +1 -0
- package/dist/sql-queries/index.d.ts +4 -0
- package/dist/sql-queries/index.d.ts.map +1 -0
- package/dist/sql-queries/index.js +4 -0
- package/dist/sql-queries/index.js.map +1 -0
- package/dist/sql-queries/misc.d.ts +2 -0
- package/dist/sql-queries/misc.d.ts.map +1 -0
- package/dist/sql-queries/misc.js +2 -0
- package/dist/sql-queries/misc.js.map +1 -0
- package/dist/sql-queries/sql-queries.d.ts +65 -0
- package/dist/sql-queries/sql-queries.d.ts.map +1 -0
- package/dist/sql-queries/sql-queries.js +181 -0
- package/dist/sql-queries/sql-queries.js.map +1 -0
- package/dist/sql-queries/sql-query-builder.d.ts +47 -0
- package/dist/sql-queries/sql-query-builder.d.ts.map +1 -0
- package/dist/sql-queries/sql-query-builder.js +60 -0
- package/dist/sql-queries/sql-query-builder.js.map +1 -0
- package/dist/sql-queries/types.d.ts +50 -0
- package/dist/sql-queries/types.d.ts.map +1 -0
- package/dist/sql-queries/types.js +5 -0
- package/dist/sql-queries/types.js.map +1 -0
- package/dist/util.d.ts +21 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +33 -0
- package/dist/util.js.map +1 -0
- package/package.json +37 -0
- package/src/database.ts +37 -0
- package/src/index.ts +3 -0
- package/src/schema/index.ts +100 -0
- package/src/schema/mutations.ts +128 -0
- package/src/schema/parse-utils.ts +42 -0
- package/src/schema/system-tables.ts +22 -0
- package/src/schema/table-def.ts +274 -0
- package/src/sql-queries/index.ts +3 -0
- package/src/sql-queries/misc.ts +2 -0
- package/src/sql-queries/sql-queries.ts +335 -0
- package/src/sql-queries/sql-query-builder.ts +135 -0
- package/src/sql-queries/types.ts +97 -0
- package/src/util.ts +46 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { pipe, ReadonlyArray, Schema } from '@livestore/utils/effect'
|
|
2
|
+
import type { SqliteDsl } from 'effect-db-schema'
|
|
3
|
+
|
|
4
|
+
import { sql } from '../util.js'
|
|
5
|
+
import { objectEntries } from './misc.js'
|
|
6
|
+
import * as ClientTypes from './types.js'
|
|
7
|
+
|
|
8
|
+
export type BindValues = {
|
|
9
|
+
readonly [columnName: string]: any
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const findManyRows = <TColumns extends SqliteDsl.Columns>({
|
|
13
|
+
columns,
|
|
14
|
+
tableName,
|
|
15
|
+
where,
|
|
16
|
+
limit,
|
|
17
|
+
}: {
|
|
18
|
+
tableName: string
|
|
19
|
+
columns: TColumns
|
|
20
|
+
where: ClientTypes.WhereValuesForColumns<TColumns>
|
|
21
|
+
limit?: number
|
|
22
|
+
}): [string, BindValues] => {
|
|
23
|
+
const whereSql = buildWhereSql({ where })
|
|
24
|
+
const whereModifier = whereSql === '' ? '' : `WHERE ${whereSql}`
|
|
25
|
+
const limitModifier = limit ? `LIMIT ${limit}` : ''
|
|
26
|
+
|
|
27
|
+
const whereBindValues = makeBindValues({ columns, values: where, variablePrefix: 'where_', skipNil: true })
|
|
28
|
+
|
|
29
|
+
return [sql`SELECT * FROM ${tableName} ${whereModifier} ${limitModifier}`, whereBindValues]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const countRows = <TColumns extends SqliteDsl.Columns>({
|
|
33
|
+
columns,
|
|
34
|
+
tableName,
|
|
35
|
+
where,
|
|
36
|
+
}: {
|
|
37
|
+
tableName: string
|
|
38
|
+
columns: TColumns
|
|
39
|
+
where: ClientTypes.WhereValuesForColumns<TColumns>
|
|
40
|
+
}): [string, BindValues] => {
|
|
41
|
+
const whereSql = buildWhereSql({ where })
|
|
42
|
+
const whereModifier = whereSql === '' ? '' : `WHERE ${whereSql}`
|
|
43
|
+
|
|
44
|
+
const whereBindValues = makeBindValues({ columns, values: where, variablePrefix: 'where_', skipNil: true })
|
|
45
|
+
|
|
46
|
+
return [sql`SELECT count(1) FROM ${tableName} ${whereModifier}`, whereBindValues]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const insertRow = <TColumns extends SqliteDsl.Columns>({
|
|
50
|
+
tableName,
|
|
51
|
+
columns,
|
|
52
|
+
values,
|
|
53
|
+
options = { orReplace: false },
|
|
54
|
+
}: {
|
|
55
|
+
tableName: string
|
|
56
|
+
columns: TColumns
|
|
57
|
+
values: ClientTypes.DecodedValuesForColumns<TColumns>
|
|
58
|
+
options: { orReplace: boolean }
|
|
59
|
+
}): [string, BindValues] => {
|
|
60
|
+
const keysStr = Object.keys(values).join(', ')
|
|
61
|
+
const valuesStr = Object.keys(values)
|
|
62
|
+
.map((_) => `$${_}`)
|
|
63
|
+
.join(', ')
|
|
64
|
+
|
|
65
|
+
return [
|
|
66
|
+
sql`INSERT ${options.orReplace ? 'OR REPLACE' : ''} INTO ${tableName} (${keysStr}) VALUES (${valuesStr})`,
|
|
67
|
+
makeBindValues({ columns, values }),
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const insertRows = <TColumns extends SqliteDsl.Columns>({
|
|
72
|
+
columns,
|
|
73
|
+
tableName,
|
|
74
|
+
valuesArray,
|
|
75
|
+
}: {
|
|
76
|
+
tableName: string
|
|
77
|
+
columns: TColumns
|
|
78
|
+
valuesArray: ClientTypes.DecodedValuesForColumns<TColumns>[]
|
|
79
|
+
}): [string, BindValues] => {
|
|
80
|
+
const keysStr = Object.keys(valuesArray[0]!).join(', ')
|
|
81
|
+
|
|
82
|
+
// NOTE consider batching for large arrays (https://sqlite.org/forum/info/f832398c19d30a4a)
|
|
83
|
+
const valuesStrs = valuesArray
|
|
84
|
+
.map((values, itemIndex) =>
|
|
85
|
+
Object.keys(values)
|
|
86
|
+
.map((_) => `$item_${itemIndex}_${_}`)
|
|
87
|
+
.join(', '),
|
|
88
|
+
)
|
|
89
|
+
.map((_) => `(${_})`)
|
|
90
|
+
.join(', ')
|
|
91
|
+
|
|
92
|
+
const bindValues = valuesArray.reduce(
|
|
93
|
+
(acc, values, itemIndex) => ({
|
|
94
|
+
...acc,
|
|
95
|
+
...makeBindValues({ columns, values, variablePrefix: `item_${itemIndex}_` }),
|
|
96
|
+
}),
|
|
97
|
+
{},
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
return [sql`INSERT INTO ${tableName} (${keysStr}) VALUES ${valuesStrs}`, bindValues]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const insertOrIgnoreRow = <TColumns extends SqliteDsl.Columns>({
|
|
104
|
+
columns,
|
|
105
|
+
tableName,
|
|
106
|
+
values: values_,
|
|
107
|
+
returnRow,
|
|
108
|
+
}: {
|
|
109
|
+
tableName: string
|
|
110
|
+
columns: TColumns
|
|
111
|
+
values: ClientTypes.DecodedValuesForColumns<TColumns>
|
|
112
|
+
returnRow: boolean
|
|
113
|
+
}): [string, BindValues] => {
|
|
114
|
+
const values = filterUndefinedFields(values_)
|
|
115
|
+
const keysStr = Object.keys(values).join(', ')
|
|
116
|
+
const valuesStr = Object.keys(values)
|
|
117
|
+
.map((_) => `$${_}`)
|
|
118
|
+
.join(', ')
|
|
119
|
+
|
|
120
|
+
const bindValues = makeBindValues({ columns, values })
|
|
121
|
+
const returningStmt = returnRow ? 'RETURNING *' : ''
|
|
122
|
+
|
|
123
|
+
return [sql`INSERT OR IGNORE INTO ${tableName} (${keysStr}) VALUES (${valuesStr}) ${returningStmt}`, bindValues]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const updateRows = <TColumns extends SqliteDsl.Columns>({
|
|
127
|
+
columns,
|
|
128
|
+
tableName,
|
|
129
|
+
updateValues: updateValues_,
|
|
130
|
+
where,
|
|
131
|
+
}: {
|
|
132
|
+
columns: TColumns
|
|
133
|
+
tableName: string
|
|
134
|
+
updateValues: Partial<ClientTypes.DecodedValuesForColumnsAll<TColumns>>
|
|
135
|
+
where: ClientTypes.WhereValuesForColumns<TColumns>
|
|
136
|
+
}): [string, BindValues] => {
|
|
137
|
+
const updateValues = filterUndefinedFields(updateValues_)
|
|
138
|
+
|
|
139
|
+
// TODO return an Option instead of `select 1` if there are no update values
|
|
140
|
+
if (Object.keys(updateValues).length === 0) {
|
|
141
|
+
return [sql`select 1`, {}]
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const updateValueStr = Object.keys(updateValues)
|
|
145
|
+
.map((columnName) => `${columnName} = $update_${columnName}`)
|
|
146
|
+
.join(', ')
|
|
147
|
+
|
|
148
|
+
const bindValues = {
|
|
149
|
+
...makeBindValues({ columns, values: updateValues, variablePrefix: 'update_' }),
|
|
150
|
+
...makeBindValues({ columns, values: where, variablePrefix: 'where_', skipNil: true }),
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const whereSql = buildWhereSql({ where })
|
|
154
|
+
const whereModifier = whereSql === '' ? '' : `WHERE ${whereSql}`
|
|
155
|
+
|
|
156
|
+
return [sql`UPDATE ${tableName} SET ${updateValueStr} ${whereModifier}`, bindValues]
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const deleteRows = <TColumns extends SqliteDsl.Columns>({
|
|
160
|
+
columns,
|
|
161
|
+
tableName,
|
|
162
|
+
where,
|
|
163
|
+
}: {
|
|
164
|
+
columns: TColumns
|
|
165
|
+
tableName: string
|
|
166
|
+
where: ClientTypes.WhereValuesForColumns<TColumns>
|
|
167
|
+
}): [string, BindValues] => {
|
|
168
|
+
const bindValues = {
|
|
169
|
+
...makeBindValues({ columns, values: where, variablePrefix: 'where_', skipNil: true }),
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const whereSql = buildWhereSql({ where })
|
|
173
|
+
const whereModifier = whereSql === '' ? '' : `WHERE ${whereSql}`
|
|
174
|
+
|
|
175
|
+
return [sql`DELETE FROM ${tableName} ${whereModifier}`, bindValues]
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export const upsertRow = <TColumns extends SqliteDsl.Columns>({
|
|
179
|
+
tableName,
|
|
180
|
+
columns,
|
|
181
|
+
createValues: createValues_,
|
|
182
|
+
updateValues: updateValues_,
|
|
183
|
+
where,
|
|
184
|
+
}: {
|
|
185
|
+
tableName: string
|
|
186
|
+
columns: TColumns
|
|
187
|
+
createValues: ClientTypes.DecodedValuesForColumns<TColumns>
|
|
188
|
+
updateValues: Partial<ClientTypes.DecodedValuesForColumnsAll<TColumns>>
|
|
189
|
+
// TODO where VALUES are actually not used here. Maybe adjust API?
|
|
190
|
+
where: ClientTypes.WhereValuesForColumns<TColumns>
|
|
191
|
+
}): [string, BindValues] => {
|
|
192
|
+
const createValues = filterUndefinedFields(createValues_)
|
|
193
|
+
const updateValues = filterUndefinedFields(updateValues_)
|
|
194
|
+
|
|
195
|
+
const keysStr = Object.keys(createValues).join(', ')
|
|
196
|
+
|
|
197
|
+
const createValuesStr = Object.keys(createValues)
|
|
198
|
+
.map((_) => `$create_${_}`)
|
|
199
|
+
.join(', ')
|
|
200
|
+
|
|
201
|
+
const conflictStr = Object.keys(where).join(', ')
|
|
202
|
+
|
|
203
|
+
const updateValueStr = Object.keys(updateValues)
|
|
204
|
+
.map((columnName) => `${columnName} = $update_${columnName}`)
|
|
205
|
+
.join(', ')
|
|
206
|
+
|
|
207
|
+
const bindValues = {
|
|
208
|
+
...makeBindValues({ columns, values: createValues, variablePrefix: 'create_' }),
|
|
209
|
+
...makeBindValues({ columns, values: updateValues, variablePrefix: 'update_' }),
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return [
|
|
213
|
+
sql`
|
|
214
|
+
INSERT INTO ${tableName} (${keysStr})
|
|
215
|
+
VALUES (${createValuesStr})
|
|
216
|
+
ON CONFLICT (${conflictStr}) DO UPDATE SET ${updateValueStr}
|
|
217
|
+
`,
|
|
218
|
+
bindValues,
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export const createTable = ({
|
|
223
|
+
table,
|
|
224
|
+
tableName,
|
|
225
|
+
}: {
|
|
226
|
+
table: SqliteDsl.TableDefinition<any, SqliteDsl.Columns>
|
|
227
|
+
tableName: string
|
|
228
|
+
}): string => {
|
|
229
|
+
const primaryKeys = Object.entries(table.columns)
|
|
230
|
+
.filter(([_, columnDef]) => columnDef.primaryKey)
|
|
231
|
+
.map(([columnName, _]) => columnName)
|
|
232
|
+
const columnDefStrs = Object.entries(table.columns).map(([columnName, columnDef]) => {
|
|
233
|
+
const nullModifier = columnDef.nullable === true ? '' : 'NOT NULL'
|
|
234
|
+
const defaultModifier = columnDef.default._tag === 'None' ? '' : `DEFAULT ${columnDef.default.value}`
|
|
235
|
+
return sql`${columnName} ${columnDef.columnType} ${nullModifier} ${defaultModifier}`
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
if (primaryKeys.length > 0) {
|
|
239
|
+
columnDefStrs.push(`PRIMARY KEY (${primaryKeys.join(', ')})`)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return sql`CREATE TABLE ${tableName} (${columnDefStrs.join(', ')});`
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export const makeBindValues = <TColumns extends SqliteDsl.Columns, TKeys extends string>({
|
|
246
|
+
columns,
|
|
247
|
+
values,
|
|
248
|
+
variablePrefix = '',
|
|
249
|
+
skipNil,
|
|
250
|
+
}: {
|
|
251
|
+
columns: TColumns
|
|
252
|
+
values: Record<TKeys, any>
|
|
253
|
+
variablePrefix?: string
|
|
254
|
+
/** So far only used to prepare `where` statements */
|
|
255
|
+
skipNil?: boolean
|
|
256
|
+
}): Record<string, any> => {
|
|
257
|
+
const codecMap = pipe(
|
|
258
|
+
columns,
|
|
259
|
+
objectEntries,
|
|
260
|
+
ReadonlyArray.map(([columnName, columnDef]) => [
|
|
261
|
+
columnName,
|
|
262
|
+
(value: any) => {
|
|
263
|
+
if (columnDef.nullable === true && (value === null || value === undefined)) return null
|
|
264
|
+
const res = Schema.encodeEither(columnDef.schema)(value)
|
|
265
|
+
if (res._tag === 'Left') {
|
|
266
|
+
debugger
|
|
267
|
+
throw res.left
|
|
268
|
+
} else {
|
|
269
|
+
return res.right
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
]),
|
|
273
|
+
Object.fromEntries,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
return pipe(
|
|
277
|
+
Object.entries(values)
|
|
278
|
+
// NOTE null/undefined values are handled via explicit SQL syntax and don't need to be provided as bind values
|
|
279
|
+
.filter(([, value]) => skipNil !== true || (value !== null && value !== undefined))
|
|
280
|
+
.flatMap(([columnName, value]: [string, any]) => {
|
|
281
|
+
// remap complex where-values with `op`
|
|
282
|
+
if (typeof value === 'object' && value !== null && 'op' in value) {
|
|
283
|
+
switch (value.op) {
|
|
284
|
+
case 'in': {
|
|
285
|
+
return value.val.map((value: any, i: number) => [
|
|
286
|
+
`${variablePrefix}${columnName}_${i}`,
|
|
287
|
+
codecMap[columnName]!(value),
|
|
288
|
+
])
|
|
289
|
+
}
|
|
290
|
+
case '=':
|
|
291
|
+
case '>':
|
|
292
|
+
case '<': {
|
|
293
|
+
return [[`${variablePrefix}${columnName}`, codecMap[columnName]!(value.val)]]
|
|
294
|
+
}
|
|
295
|
+
default: {
|
|
296
|
+
throw new Error(`Unknown op: ${value.op}`)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
} else {
|
|
300
|
+
return [[`${variablePrefix}${columnName}`, codecMap[columnName]!(value)]]
|
|
301
|
+
}
|
|
302
|
+
}),
|
|
303
|
+
Object.fromEntries,
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const buildWhereSql = <TColumns extends SqliteDsl.Columns>({
|
|
308
|
+
where,
|
|
309
|
+
}: {
|
|
310
|
+
where: ClientTypes.WhereValuesForColumns<TColumns>
|
|
311
|
+
}) => {
|
|
312
|
+
const getWhereOp = (columnName: string, value: ClientTypes.WhereValueForDecoded<any>) => {
|
|
313
|
+
if (value === null) {
|
|
314
|
+
return `IS NULL`
|
|
315
|
+
} else if (typeof value === 'object' && typeof value.op === 'string' && ClientTypes.isValidWhereOp(value.op)) {
|
|
316
|
+
return `${value.op} $where_${columnName}`
|
|
317
|
+
} else if (typeof value === 'object' && typeof value.op === 'string' && value.op === 'in') {
|
|
318
|
+
return `in (${value.val.map((_: any, i: number) => `$where_${columnName}_${i}`).join(', ')})`
|
|
319
|
+
} else {
|
|
320
|
+
return `= $where_${columnName}`
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return pipe(
|
|
325
|
+
where,
|
|
326
|
+
objectEntries,
|
|
327
|
+
ReadonlyArray.map(([columnName, value]) => `${columnName} ${getWhereOp(columnName, value)}`),
|
|
328
|
+
ReadonlyArray.join(' AND '),
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// TODO better typing
|
|
333
|
+
const filterUndefinedFields = <T extends Record<string, any>>(obj: T): T => {
|
|
334
|
+
return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined)) as T
|
|
335
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { SqliteDsl } from 'effect-db-schema'
|
|
2
|
+
|
|
3
|
+
import type { BindValues } from './sql-queries.js'
|
|
4
|
+
import * as SqlQueries from './sql-queries.js'
|
|
5
|
+
import type * as ClientTypes from './types.js'
|
|
6
|
+
|
|
7
|
+
export type SqlQuery = [stmt: string, bindValues: BindValues, tableName: string]
|
|
8
|
+
|
|
9
|
+
export const makeSqlQueryBuilder = <TSchema extends SqliteDsl.DbSchema>(schema: TSchema) => {
|
|
10
|
+
const findManyRows = <TTableName extends keyof TSchema & string>({
|
|
11
|
+
tableName,
|
|
12
|
+
where,
|
|
13
|
+
limit,
|
|
14
|
+
}: {
|
|
15
|
+
tableName: TTableName
|
|
16
|
+
where: ClientTypes.WhereValuesForTable<TSchema, TTableName>
|
|
17
|
+
limit?: number
|
|
18
|
+
}): [string, BindValues, TTableName] => {
|
|
19
|
+
const columns = schema[tableName]!.columns
|
|
20
|
+
const [stmt, bindValues] = SqlQueries.findManyRows({ columns, tableName, where, limit })
|
|
21
|
+
return [stmt, bindValues, tableName]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const countRows = <TTableName extends keyof TSchema & string>({
|
|
25
|
+
tableName,
|
|
26
|
+
where,
|
|
27
|
+
}: {
|
|
28
|
+
tableName: TTableName
|
|
29
|
+
where: ClientTypes.WhereValuesForTable<TSchema, TTableName>
|
|
30
|
+
}): [string, BindValues, TTableName] => {
|
|
31
|
+
const columns = schema[tableName]!.columns
|
|
32
|
+
const [stmt, bindValues] = SqlQueries.countRows({ columns, tableName, where })
|
|
33
|
+
return [stmt, bindValues, tableName]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const insertRow = <TTableName extends keyof TSchema & string>({
|
|
37
|
+
tableName,
|
|
38
|
+
values,
|
|
39
|
+
options = { orReplace: false },
|
|
40
|
+
}: {
|
|
41
|
+
tableName: TTableName
|
|
42
|
+
values: ClientTypes.DecodedValuesForTable<TSchema, TTableName>
|
|
43
|
+
options?: { orReplace: boolean }
|
|
44
|
+
}): [string, BindValues, TTableName] => {
|
|
45
|
+
const columns = schema[tableName]!.columns
|
|
46
|
+
const [stmt, bindValues] = SqlQueries.insertRow({ columns, tableName, values, options })
|
|
47
|
+
return [stmt, bindValues, tableName]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const insertRows = <TTableName extends keyof TSchema & string>({
|
|
51
|
+
tableName,
|
|
52
|
+
valuesArray,
|
|
53
|
+
}: {
|
|
54
|
+
tableName: TTableName
|
|
55
|
+
valuesArray: ClientTypes.DecodedValuesForTable<TSchema, TTableName>[]
|
|
56
|
+
}): [string, BindValues, TTableName] => {
|
|
57
|
+
const columns = schema[tableName]!.columns
|
|
58
|
+
const [stmt, bindValues] = SqlQueries.insertRows({ columns, tableName, valuesArray })
|
|
59
|
+
return [stmt, bindValues, tableName]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const insertOrIgnoreRow = <TTableName extends keyof TSchema & string>({
|
|
63
|
+
tableName,
|
|
64
|
+
values,
|
|
65
|
+
returnRow = false,
|
|
66
|
+
}: {
|
|
67
|
+
tableName: TTableName
|
|
68
|
+
values: ClientTypes.DecodedValuesForTable<TSchema, TTableName>
|
|
69
|
+
returnRow?: boolean
|
|
70
|
+
}): [string, BindValues, TTableName] => {
|
|
71
|
+
const columns = schema[tableName]!.columns
|
|
72
|
+
const [stmt, bindValues] = SqlQueries.insertOrIgnoreRow({ columns, tableName, values, returnRow })
|
|
73
|
+
return [stmt, bindValues, tableName]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const updateRows = <TTableName extends keyof TSchema & string>({
|
|
77
|
+
tableName,
|
|
78
|
+
updateValues,
|
|
79
|
+
where,
|
|
80
|
+
}: {
|
|
81
|
+
tableName: TTableName
|
|
82
|
+
updateValues: Partial<ClientTypes.DecodedValuesForTableAll<TSchema, TTableName>>
|
|
83
|
+
where: ClientTypes.WhereValuesForTable<TSchema, TTableName>
|
|
84
|
+
}): [string, BindValues, TTableName] => {
|
|
85
|
+
const columns = schema[tableName]!.columns
|
|
86
|
+
const [stmt, bindValues] = SqlQueries.updateRows({ columns, tableName, updateValues, where })
|
|
87
|
+
return [stmt, bindValues, tableName]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const deleteRows = <TTableName extends keyof TSchema & string>({
|
|
91
|
+
tableName,
|
|
92
|
+
where,
|
|
93
|
+
}: {
|
|
94
|
+
tableName: TTableName
|
|
95
|
+
where: ClientTypes.WhereValuesForTable<TSchema, TTableName>
|
|
96
|
+
}): [string, BindValues, TTableName] => {
|
|
97
|
+
const columns = schema[tableName]!.columns
|
|
98
|
+
const [stmt, bindValues] = SqlQueries.deleteRows({ columns, tableName, where })
|
|
99
|
+
return [stmt, bindValues, tableName]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const upsertRow = <TTableName extends keyof TSchema & string>({
|
|
103
|
+
tableName,
|
|
104
|
+
createValues,
|
|
105
|
+
updateValues,
|
|
106
|
+
where,
|
|
107
|
+
}: {
|
|
108
|
+
tableName: TTableName
|
|
109
|
+
createValues: ClientTypes.DecodedValuesForTable<TSchema, TTableName>
|
|
110
|
+
updateValues: Partial<ClientTypes.DecodedValuesForTableAll<TSchema, TTableName>>
|
|
111
|
+
// TODO where VALUES are actually not used here. Maybe adjust API?
|
|
112
|
+
where: ClientTypes.WhereValuesForTable<TSchema, TTableName>
|
|
113
|
+
}): [string, BindValues, TTableName] => {
|
|
114
|
+
const columns = schema[tableName]!.columns
|
|
115
|
+
const [stmt, bindValues] = SqlQueries.upsertRow({
|
|
116
|
+
columns,
|
|
117
|
+
tableName,
|
|
118
|
+
createValues: createValues as any, // TODO investigate why types don't match
|
|
119
|
+
updateValues,
|
|
120
|
+
where,
|
|
121
|
+
})
|
|
122
|
+
return [stmt, bindValues, tableName]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
findManyRows,
|
|
127
|
+
countRows,
|
|
128
|
+
insertRow,
|
|
129
|
+
insertRows,
|
|
130
|
+
insertOrIgnoreRow,
|
|
131
|
+
updateRows,
|
|
132
|
+
deleteRows,
|
|
133
|
+
upsertRow,
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { Schema } from '@livestore/utils/effect'
|
|
2
|
+
import type { Prettify, SqliteDsl } from 'effect-db-schema'
|
|
3
|
+
|
|
4
|
+
export type DecodedValuesForTableAll<TSchema extends SqliteDsl.DbSchema, TTableName extends keyof TSchema> = {
|
|
5
|
+
[K in keyof GetColumns<TSchema, TTableName>]: Schema.Schema.To<GetColumn<TSchema, TTableName, K>['schema']>
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type DecodedValuesForTablePretty<
|
|
9
|
+
TSchema extends SqliteDsl.DbSchema,
|
|
10
|
+
TTableName extends keyof TSchema,
|
|
11
|
+
> = Prettify<DecodedValuesForTable<TSchema, TTableName>>
|
|
12
|
+
|
|
13
|
+
export type DecodedValuesForTable<TSchema extends SqliteDsl.DbSchema, TTableName extends keyof TSchema> = Partial<
|
|
14
|
+
Pick<DecodedValuesForTableAll<TSchema, TTableName>, GetNullableColumnNamesForTable<TSchema, TTableName>>
|
|
15
|
+
> &
|
|
16
|
+
Omit<DecodedValuesForTableAll<TSchema, TTableName>, GetNullableColumnNamesForTable<TSchema, TTableName>>
|
|
17
|
+
|
|
18
|
+
export type DecodedValuesForTableOrNull<
|
|
19
|
+
TSchema extends SqliteDsl.DbSchema,
|
|
20
|
+
TTableName extends keyof TSchema,
|
|
21
|
+
> = NullableObj<
|
|
22
|
+
Pick<DecodedValuesForTableAll<TSchema, TTableName>, GetNullableColumnNamesForTable<TSchema, TTableName>>
|
|
23
|
+
> &
|
|
24
|
+
Omit<DecodedValuesForTableAll<TSchema, TTableName>, GetNullableColumnNamesForTable<TSchema, TTableName>>
|
|
25
|
+
|
|
26
|
+
export type WhereValuesForTable<TSchema extends SqliteDsl.DbSchema, TTableName extends keyof TSchema> = PartialOrNull<{
|
|
27
|
+
[K in keyof DecodedValuesForTableAll<TSchema, TTableName>]: WhereValueForDecoded<
|
|
28
|
+
DecodedValuesForTableAll<TSchema, TTableName>[K]
|
|
29
|
+
>
|
|
30
|
+
}>
|
|
31
|
+
|
|
32
|
+
export type WhereValueForDecoded<TDecoded> = TDecoded | { op: WhereOp; val: TDecoded } | { op: 'in'; val: TDecoded[] }
|
|
33
|
+
export type WhereOp = '>' | '<' | '='
|
|
34
|
+
|
|
35
|
+
export const isValidWhereOp = (op: string): op is WhereOp => {
|
|
36
|
+
const validWhereOps = ['>', '<', '=']
|
|
37
|
+
return validWhereOps.includes(op)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type EncodedValuesForTableAll<TSchema extends SqliteDsl.DbSchema, TTableName extends keyof TSchema> = {
|
|
41
|
+
[K in keyof GetColumns<TSchema, TTableName>]: Schema.Schema.To<GetColumn<TSchema, TTableName, K>['schema']>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type EncodedValuesForTable<TSchema extends SqliteDsl.DbSchema, TTableName extends keyof TSchema> = Partial<
|
|
45
|
+
Pick<EncodedValuesForTableAll<TSchema, TTableName>, GetNullableColumnNamesForTable<TSchema, TTableName>>
|
|
46
|
+
> &
|
|
47
|
+
Omit<EncodedValuesForTableAll<TSchema, TTableName>, GetNullableColumnNamesForTable<TSchema, TTableName>>
|
|
48
|
+
|
|
49
|
+
export type GetNullableColumnNamesForTable<
|
|
50
|
+
TSchema extends SqliteDsl.DbSchema,
|
|
51
|
+
TTableName extends keyof TSchema,
|
|
52
|
+
> = keyof {
|
|
53
|
+
[K in keyof GetColumns<TSchema, TTableName> as GetColumn<TSchema, TTableName, K>['nullable'] extends true
|
|
54
|
+
? K
|
|
55
|
+
: never]: {}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type GetColumns<
|
|
59
|
+
TSchema extends SqliteDsl.DbSchema,
|
|
60
|
+
TTableName extends keyof TSchema,
|
|
61
|
+
> = TSchema[TTableName]['columns']
|
|
62
|
+
|
|
63
|
+
export type GetColumn<
|
|
64
|
+
TSchema extends SqliteDsl.DbSchema,
|
|
65
|
+
TTableName extends keyof TSchema,
|
|
66
|
+
TColumnName extends keyof TSchema[TTableName]['columns'],
|
|
67
|
+
> = TSchema[TTableName]['columns'][TColumnName]
|
|
68
|
+
|
|
69
|
+
export type DecodedValuesForColumnsAll<TColumns extends SqliteDsl.Columns> = {
|
|
70
|
+
[K in keyof TColumns]: Schema.Schema.To<TColumns[K]['schema']>
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type DecodedValuesForColumns<TColumns extends SqliteDsl.Columns> = Partial<
|
|
74
|
+
Pick<DecodedValuesForColumnsAll<TColumns>, GetNullableColumnNames<TColumns>>
|
|
75
|
+
> &
|
|
76
|
+
Omit<DecodedValuesForColumnsAll<TColumns>, GetNullableColumnNames<TColumns>>
|
|
77
|
+
|
|
78
|
+
export type EncodedValuesForColumnsAll<TColumns extends SqliteDsl.Columns> = {
|
|
79
|
+
[K in keyof TColumns]: Schema.Schema.From<TColumns[K]['schema']>
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type EncodedValuesForColumns<TColumns extends SqliteDsl.Columns> = Partial<
|
|
83
|
+
Pick<EncodedValuesForColumnsAll<TColumns>, GetNullableColumnNames<TColumns>>
|
|
84
|
+
> &
|
|
85
|
+
Omit<EncodedValuesForColumnsAll<TColumns>, GetNullableColumnNames<TColumns>>
|
|
86
|
+
|
|
87
|
+
export type WhereValuesForColumns<TColumns extends SqliteDsl.Columns> = PartialOrNull<{
|
|
88
|
+
[K in keyof EncodedValuesForColumns<TColumns>]: WhereValueForDecoded<DecodedValuesForColumnsAll<TColumns>[K]>
|
|
89
|
+
}>
|
|
90
|
+
|
|
91
|
+
export type GetNullableColumnNames<TColumns extends SqliteDsl.Columns> = keyof {
|
|
92
|
+
[K in keyof TColumns as TColumns[K] extends SqliteDsl.ColumnDefinition<any, true> ? K : never]: {}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type PartialOrNull<T> = { [P in keyof T]?: T[P] | null }
|
|
96
|
+
|
|
97
|
+
export type NullableObj<T> = { [P in keyof T]: T[P] | null }
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/// <reference lib="es2022" />
|
|
2
|
+
|
|
3
|
+
import type { Brand } from '@livestore/utils/effect'
|
|
4
|
+
|
|
5
|
+
export type ParamsObject = Record<string, SqlValue>
|
|
6
|
+
export type SqlValue = string | number | Uint8Array | null
|
|
7
|
+
|
|
8
|
+
export type Bindable = SqlValue[] | ParamsObject
|
|
9
|
+
|
|
10
|
+
export type PreparedBindValues = Brand.Branded<Bindable, 'PreparedBindValues'>
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* This is a tag function for tagged literals.
|
|
14
|
+
* it lets us get syntax highlighting on SQL queries in VSCode, but
|
|
15
|
+
* doesn't do anything at runtime.
|
|
16
|
+
* Code copied from: https://esdiscuss.org/topic/string-identity-template-tag
|
|
17
|
+
*/
|
|
18
|
+
export const sql = (template: TemplateStringsArray, ...args: unknown[]): string => {
|
|
19
|
+
let str = ''
|
|
20
|
+
|
|
21
|
+
for (const [i, arg] of args.entries()) {
|
|
22
|
+
str += template[i] + String(arg)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// eslint-disable-next-line unicorn/prefer-at
|
|
26
|
+
return str + template[template.length - 1]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Prepare bind values to send to SQLite
|
|
30
|
+
/* Add $ to the beginning of keys; which we use as our interpolation syntax
|
|
31
|
+
/* We also strip out any params that aren't used in the statement,
|
|
32
|
+
/* because rusqlite doesn't allow unused named params
|
|
33
|
+
/* TODO: Search for unused params via proper parsing, not string search
|
|
34
|
+
**/
|
|
35
|
+
export const prepareBindValues = (values: Bindable, statement: string): PreparedBindValues => {
|
|
36
|
+
if (Array.isArray(values)) return values as PreparedBindValues
|
|
37
|
+
|
|
38
|
+
const result: ParamsObject = {}
|
|
39
|
+
for (const [key, value] of Object.entries(values)) {
|
|
40
|
+
if (statement.includes(key)) {
|
|
41
|
+
result[`$${key}`] = value
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return result as PreparedBindValues
|
|
46
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"tsBuildInfoFile": "./dist/.tsbuildinfo"
|
|
7
|
+
},
|
|
8
|
+
"include": ["./src"],
|
|
9
|
+
"references": [{ "path": "../../effect-db-schema" }, { "path": "../utils" }]
|
|
10
|
+
}
|