on-zero 0.6.13 → 0.6.15
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/cjs/createSchemaFromDrizzle.cjs +4 -4
- package/dist/cjs/createSchemaFromDrizzle.native.js +4 -4
- package/dist/cjs/createSchemaFromDrizzle.native.js.map +1 -1
- package/dist/cjs/createZeroDirectServer.cjs +279 -0
- package/dist/cjs/createZeroDirectServer.native.js +292 -0
- package/dist/cjs/createZeroDirectServer.native.js.map +1 -0
- package/dist/cjs/createZeroSQLiteServer.cjs +8 -277
- package/dist/cjs/createZeroSQLiteServer.native.js +8 -390
- package/dist/cjs/createZeroSQLiteServer.native.js.map +1 -1
- package/dist/cjs/createZeroSQLiteServer.test.cjs +30 -3
- package/dist/cjs/createZeroSQLiteServer.test.native.js +31 -3
- package/dist/cjs/createZeroSQLiteServer.test.native.js.map +1 -1
- package/dist/cjs/createZeroServerCore.cjs +20 -205
- package/dist/cjs/createZeroServerCore.native.js +19 -213
- package/dist/cjs/createZeroServerCore.native.js.map +1 -1
- package/dist/cjs/generate.cjs +10 -3
- package/dist/cjs/generate.native.js +10 -3
- package/dist/cjs/generate.native.js.map +1 -1
- package/dist/cjs/generate.test.cjs +39 -0
- package/dist/cjs/generate.test.native.js +39 -0
- package/dist/cjs/generate.test.native.js.map +1 -1
- package/dist/cjs/sqliteRuntime.cjs +318 -0
- package/dist/cjs/sqliteRuntime.native.js +438 -0
- package/dist/cjs/sqliteRuntime.native.js.map +1 -0
- package/dist/cjs/syncHost.cjs +16 -9
- package/dist/cjs/syncHost.native.js +21 -12
- package/dist/cjs/syncHost.native.js.map +1 -1
- package/dist/cjs/syncHost.test.cjs +13 -2
- package/dist/cjs/syncHost.test.native.js +14 -2
- package/dist/cjs/syncHost.test.native.js.map +1 -1
- package/dist/esm/createSchemaFromDrizzle.mjs +2 -2
- package/dist/esm/createSchemaFromDrizzle.native.js +2 -2
- package/dist/esm/createZeroDirectServer.mjs +251 -0
- package/dist/esm/createZeroDirectServer.mjs.map +1 -0
- package/dist/esm/createZeroDirectServer.native.js +261 -0
- package/dist/esm/createZeroDirectServer.native.js.map +1 -0
- package/dist/esm/createZeroSQLiteServer.mjs +3 -272
- package/dist/esm/createZeroSQLiteServer.mjs.map +1 -1
- package/dist/esm/createZeroSQLiteServer.native.js +3 -385
- package/dist/esm/createZeroSQLiteServer.native.js.map +1 -1
- package/dist/esm/createZeroSQLiteServer.test.mjs +30 -3
- package/dist/esm/createZeroSQLiteServer.test.mjs.map +1 -1
- package/dist/esm/createZeroSQLiteServer.test.native.js +31 -3
- package/dist/esm/createZeroSQLiteServer.test.native.js.map +1 -1
- package/dist/esm/createZeroServerCore.mjs +18 -202
- package/dist/esm/createZeroServerCore.mjs.map +1 -1
- package/dist/esm/createZeroServerCore.native.js +18 -211
- package/dist/esm/createZeroServerCore.native.js.map +1 -1
- package/dist/esm/generate.mjs +10 -3
- package/dist/esm/generate.mjs.map +1 -1
- package/dist/esm/generate.native.js +10 -3
- package/dist/esm/generate.native.js.map +1 -1
- package/dist/esm/generate.test.mjs +39 -1
- package/dist/esm/generate.test.mjs.map +1 -1
- package/dist/esm/generate.test.native.js +39 -1
- package/dist/esm/generate.test.native.js.map +1 -1
- package/dist/esm/sqliteRuntime.mjs +289 -0
- package/dist/esm/sqliteRuntime.mjs.map +1 -0
- package/dist/esm/sqliteRuntime.native.js +406 -0
- package/dist/esm/sqliteRuntime.native.js.map +1 -0
- package/dist/esm/syncHost.mjs +14 -7
- package/dist/esm/syncHost.mjs.map +1 -1
- package/dist/esm/syncHost.native.js +19 -10
- package/dist/esm/syncHost.native.js.map +1 -1
- package/dist/esm/syncHost.test.mjs +13 -2
- package/dist/esm/syncHost.test.mjs.map +1 -1
- package/dist/esm/syncHost.test.native.js +14 -2
- package/dist/esm/syncHost.test.native.js.map +1 -1
- package/package.json +5 -6
- package/src/createSchemaFromDrizzle.ts +2 -2
- package/src/createZeroDirectServer.ts +427 -0
- package/src/createZeroSQLiteServer.test.ts +20 -5
- package/src/createZeroSQLiteServer.ts +14 -481
- package/src/createZeroServerCore.ts +32 -337
- package/src/generate.test.ts +57 -1
- package/src/generate.ts +23 -3
- package/src/sqliteRuntime.ts +512 -0
- package/src/syncHost.test.ts +10 -3
- package/src/syncHost.ts +49 -12
- package/test-fixtures/server-return-types.ts +8 -1
- package/types/createSchemaFromDrizzle.d.ts +2 -2
- package/types/createSchemaFromDrizzle.d.ts.map +1 -1
- package/types/createZeroDirectServer.d.ts +80 -0
- package/types/createZeroDirectServer.d.ts.map +1 -0
- package/types/createZeroSQLiteServer.d.ts +5 -30
- package/types/createZeroSQLiteServer.d.ts.map +1 -1
- package/types/createZeroServerCore.d.ts +30 -87
- package/types/createZeroServerCore.d.ts.map +1 -1
- package/types/generate.d.ts.map +1 -1
- package/types/sqliteRuntime.d.ts +71 -0
- package/types/sqliteRuntime.d.ts.map +1 -0
- package/types/syncHost.d.ts +10 -19
- package/types/syncHost.d.ts.map +1 -1
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import { createBuilder } from '@rocicorp/zero'
|
|
2
|
+
import { asQueryInternals } from '@rocicorp/zero/bindings'
|
|
3
|
+
|
|
4
|
+
import type { ZeroServerDatabase } from './createZeroDirectServer'
|
|
5
|
+
|
|
6
|
+
import type { Transaction } from './types'
|
|
7
|
+
import type { AST, Schema as ZeroSchema } from '@rocicorp/zero'
|
|
8
|
+
import type {
|
|
9
|
+
TransactionProviderHooks,
|
|
10
|
+
TransactionProviderInput,
|
|
11
|
+
} from '@rocicorp/zero/server'
|
|
12
|
+
|
|
13
|
+
export type SQLiteTransactionExecutor = {
|
|
14
|
+
exec(
|
|
15
|
+
sql: string,
|
|
16
|
+
params?: readonly unknown[],
|
|
17
|
+
metadata?: SqlStatementMetadata,
|
|
18
|
+
): SQLiteExecResult | Promise<SQLiteExecResult>
|
|
19
|
+
query<Row extends Record<string, unknown> = Record<string, unknown>>(
|
|
20
|
+
sql: string,
|
|
21
|
+
params?: readonly unknown[],
|
|
22
|
+
): Row[] | Promise<Row[]>
|
|
23
|
+
queryAst<Result = unknown>(
|
|
24
|
+
ast: AST,
|
|
25
|
+
format: SQLiteQueryFormat,
|
|
26
|
+
queryName?: string,
|
|
27
|
+
): Result | Promise<Result>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type SQLiteExecResult = {
|
|
31
|
+
changes: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Identifies an application write without asking the host to parse its SQL.
|
|
36
|
+
* `table` is the physical SQLite table used by storage triggers; `publicTable`
|
|
37
|
+
* is the logical Zero table name exposed by the change feed.
|
|
38
|
+
*/
|
|
39
|
+
export type SqlStatementMetadata = {
|
|
40
|
+
table: string
|
|
41
|
+
publicTable: string
|
|
42
|
+
kind: 'delete' | 'insert' | 'update' | 'upsert'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type SQLiteQueryFormat = {
|
|
46
|
+
relationships: Record<string, SQLiteQueryFormat>
|
|
47
|
+
singular: boolean
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type SQLiteTransactionProvider = <R>(
|
|
51
|
+
callback: (tx: SQLiteTransactionExecutor) => R | Promise<R>,
|
|
52
|
+
) => Promise<R>
|
|
53
|
+
|
|
54
|
+
type SQLiteColumn = ZeroSchema['tables'][string]['columns'][string]
|
|
55
|
+
|
|
56
|
+
export type SQLiteValueContext = {
|
|
57
|
+
column: SQLiteColumn
|
|
58
|
+
columnName: string
|
|
59
|
+
tableName: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type ZeroSQLiteTransactionOptions<Schema extends ZeroSchema> = {
|
|
63
|
+
schema: Schema
|
|
64
|
+
encodeValue?: (value: unknown, context: SQLiteValueContext) => unknown
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type SQLiteLedgerTables = {
|
|
68
|
+
clients: string
|
|
69
|
+
mutations: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type PersistAlreadyProcessedArgs = {
|
|
73
|
+
clientGroupID: string
|
|
74
|
+
mutations: Array<{
|
|
75
|
+
id: { clientID: string; id: number }
|
|
76
|
+
result: { error: 'alreadyProcessed'; details?: unknown }
|
|
77
|
+
}>
|
|
78
|
+
upstreamSchema: string
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
type SQLiteTransactionOptions<Schema extends ZeroSchema> = {
|
|
82
|
+
encodeValue: (value: unknown, context: SQLiteValueContext) => unknown
|
|
83
|
+
schema: Schema
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type SQLiteDatabaseOptions<Schema extends ZeroSchema> =
|
|
87
|
+
SQLiteTransactionOptions<Schema> & {
|
|
88
|
+
initializedLedgers: Set<string>
|
|
89
|
+
ledgerTables: (upstreamSchema: string) => SQLiteLedgerTables
|
|
90
|
+
transactionProvider: SQLiteTransactionProvider
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function createSQLiteDatabase<Schema extends ZeroSchema>(
|
|
94
|
+
options: SQLiteDatabaseOptions<Schema>,
|
|
95
|
+
): ZeroServerDatabase {
|
|
96
|
+
return {
|
|
97
|
+
async transaction<R>(
|
|
98
|
+
callback: (
|
|
99
|
+
tx: Transaction,
|
|
100
|
+
transactionHooks: TransactionProviderHooks,
|
|
101
|
+
) => R | Promise<R>,
|
|
102
|
+
input?: TransactionProviderInput,
|
|
103
|
+
) {
|
|
104
|
+
let committedLedgers = new Set<string>()
|
|
105
|
+
const value = await options.transactionProvider(async (executor) => {
|
|
106
|
+
const attemptLedgers = new Set<string>()
|
|
107
|
+
const ensureLedger = async (upstreamSchema: string) => {
|
|
108
|
+
const tables = options.ledgerTables(upstreamSchema)
|
|
109
|
+
const key = ledgerKey(tables)
|
|
110
|
+
if (options.initializedLedgers.has(key) || attemptLedgers.has(key)) {
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
await createLedgerTables(executor, tables)
|
|
114
|
+
attemptLedgers.add(key)
|
|
115
|
+
}
|
|
116
|
+
const transaction = createZeroSQLiteTransaction(options, executor, input)
|
|
117
|
+
const result = await callback(
|
|
118
|
+
transaction,
|
|
119
|
+
createLedgerHooks(options, executor, ensureLedger, input),
|
|
120
|
+
)
|
|
121
|
+
committedLedgers = attemptLedgers
|
|
122
|
+
return result
|
|
123
|
+
})
|
|
124
|
+
for (const key of committedLedgers) options.initializedLedgers.add(key)
|
|
125
|
+
return value
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function createZeroSQLiteTransaction<Schema extends ZeroSchema>(
|
|
131
|
+
options: ZeroSQLiteTransactionOptions<Schema>,
|
|
132
|
+
executor: SQLiteTransactionExecutor,
|
|
133
|
+
input?: TransactionProviderInput,
|
|
134
|
+
): Transaction {
|
|
135
|
+
const transactionOptions = {
|
|
136
|
+
encodeValue: options.encodeValue ?? encodeSQLiteValue,
|
|
137
|
+
schema: options.schema,
|
|
138
|
+
}
|
|
139
|
+
const clientID = input?.clientID ?? ''
|
|
140
|
+
const mutationID = input?.mutationID ?? 0
|
|
141
|
+
const mutate = new Proxy(
|
|
142
|
+
{},
|
|
143
|
+
{
|
|
144
|
+
get(_target, tableKey) {
|
|
145
|
+
const tableName = String(tableKey)
|
|
146
|
+
if (!options.schema.tables[tableName]) {
|
|
147
|
+
throw new Error(`unknown SQLite mutation table ${tableName}`)
|
|
148
|
+
}
|
|
149
|
+
return new Proxy(
|
|
150
|
+
{},
|
|
151
|
+
{
|
|
152
|
+
get(_innerTarget, kindKey) {
|
|
153
|
+
const kind = String(kindKey)
|
|
154
|
+
if (!isCrudKind(kind)) return
|
|
155
|
+
return (value: unknown) =>
|
|
156
|
+
executeCrud(transactionOptions, executor, tableName, kind, value)
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
)
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
const runQuery = (
|
|
165
|
+
ast: AST,
|
|
166
|
+
format: SQLiteQueryFormat,
|
|
167
|
+
queryName?: string,
|
|
168
|
+
): Promise<unknown> => Promise.resolve(executor.queryAst(ast, format, queryName))
|
|
169
|
+
const dbTransaction = {
|
|
170
|
+
wrappedTransaction: executor,
|
|
171
|
+
query: (sql: string, args: unknown[]) => executor.query(sql, args),
|
|
172
|
+
runQuery,
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
clientID,
|
|
177
|
+
dbTransaction,
|
|
178
|
+
location: 'server',
|
|
179
|
+
mutate,
|
|
180
|
+
mutationID,
|
|
181
|
+
query: transactionOptions.schema.enableLegacyQueries
|
|
182
|
+
? createLegacyQueryBuilder(transactionOptions.schema, (query) => {
|
|
183
|
+
const internals = asQueryInternals(query as never)
|
|
184
|
+
return runQuery(internals.ast, internals.format, internals.customQueryID?.name)
|
|
185
|
+
})
|
|
186
|
+
: undefined,
|
|
187
|
+
reason: 'authoritative',
|
|
188
|
+
async run(query: unknown) {
|
|
189
|
+
const internals = asQueryInternals(query as never)
|
|
190
|
+
return runQuery(internals.ast, internals.format, internals.customQueryID?.name)
|
|
191
|
+
},
|
|
192
|
+
} as unknown as Transaction
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
type CrudKind = 'delete' | 'insert' | 'update' | 'upsert'
|
|
196
|
+
|
|
197
|
+
function isCrudKind(value: string): value is CrudKind {
|
|
198
|
+
return (
|
|
199
|
+
value === 'delete' || value === 'insert' || value === 'update' || value === 'upsert'
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function executeCrud<Schema extends ZeroSchema>(
|
|
204
|
+
options: SQLiteTransactionOptions<Schema>,
|
|
205
|
+
executor: SQLiteTransactionExecutor,
|
|
206
|
+
tableName: string,
|
|
207
|
+
kind: CrudKind,
|
|
208
|
+
raw: unknown,
|
|
209
|
+
) {
|
|
210
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
211
|
+
throw new Error(`${tableName}.${kind} expects an object`)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const table = options.schema.tables[tableName]!
|
|
215
|
+
const values = Object.fromEntries(
|
|
216
|
+
Object.entries(raw as Record<string, unknown>).filter(
|
|
217
|
+
([, value]) => value !== undefined,
|
|
218
|
+
),
|
|
219
|
+
)
|
|
220
|
+
for (const columnName of Object.keys(values)) {
|
|
221
|
+
if (!table.columns[columnName]) {
|
|
222
|
+
throw new Error(`unknown SQLite column ${tableName}.${columnName}`)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const primaryValues = table.primaryKey.map((columnName) => {
|
|
227
|
+
if (!(columnName in values)) {
|
|
228
|
+
throw new Error(`${tableName}.${kind} is missing primary key ${columnName}`)
|
|
229
|
+
}
|
|
230
|
+
return encodeColumn(options, tableName, columnName, values[columnName])
|
|
231
|
+
})
|
|
232
|
+
const primaryWhere = table.primaryKey
|
|
233
|
+
.map((columnName) => `${quoteSQLiteIdentifier(serverColumn(table, columnName))} = ?`)
|
|
234
|
+
.join(' AND ')
|
|
235
|
+
const serverTableName = quoteSQLiteIdentifier(table.serverName ?? table.name)
|
|
236
|
+
|
|
237
|
+
if (kind === 'delete') {
|
|
238
|
+
await executor.exec(
|
|
239
|
+
`DELETE FROM ${serverTableName} WHERE ${primaryWhere}`,
|
|
240
|
+
primaryValues,
|
|
241
|
+
{ table: table.serverName ?? table.name, publicTable: tableName, kind },
|
|
242
|
+
)
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const columnNames = Object.keys(values)
|
|
247
|
+
if (columnNames.length === 0) {
|
|
248
|
+
throw new Error(`${tableName}.${kind} has no values`)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const serverColumns = columnNames.map((columnName) =>
|
|
252
|
+
quoteSQLiteIdentifier(serverColumn(table, columnName)),
|
|
253
|
+
)
|
|
254
|
+
const params = columnNames.map((columnName) =>
|
|
255
|
+
encodeColumn(options, tableName, columnName, values[columnName]),
|
|
256
|
+
)
|
|
257
|
+
const insert = `INSERT INTO ${serverTableName} (${serverColumns.join(
|
|
258
|
+
', ',
|
|
259
|
+
)}) VALUES (${columnNames.map(() => '?').join(', ')})`
|
|
260
|
+
|
|
261
|
+
if (kind === 'insert') {
|
|
262
|
+
await executor.exec(insert, params, {
|
|
263
|
+
table: table.serverName ?? table.name,
|
|
264
|
+
publicTable: tableName,
|
|
265
|
+
kind,
|
|
266
|
+
})
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (kind === 'upsert') {
|
|
271
|
+
const mutableColumns = columnNames.filter(
|
|
272
|
+
(columnName) => !table.primaryKey.includes(columnName),
|
|
273
|
+
)
|
|
274
|
+
const conflictColumns = table.primaryKey
|
|
275
|
+
.map((columnName) => quoteSQLiteIdentifier(serverColumn(table, columnName)))
|
|
276
|
+
.join(', ')
|
|
277
|
+
const conflictAction =
|
|
278
|
+
mutableColumns.length === 0
|
|
279
|
+
? 'DO NOTHING'
|
|
280
|
+
: `DO UPDATE SET ${mutableColumns
|
|
281
|
+
.map((columnName) => {
|
|
282
|
+
const column = quoteSQLiteIdentifier(serverColumn(table, columnName))
|
|
283
|
+
return `${column} = excluded.${column}`
|
|
284
|
+
})
|
|
285
|
+
.join(', ')}`
|
|
286
|
+
await executor.exec(
|
|
287
|
+
`${insert} ON CONFLICT (${conflictColumns}) ${conflictAction}`,
|
|
288
|
+
params,
|
|
289
|
+
{ table: table.serverName ?? table.name, publicTable: tableName, kind },
|
|
290
|
+
)
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const mutableColumns = columnNames.filter(
|
|
295
|
+
(columnName) => !table.primaryKey.includes(columnName),
|
|
296
|
+
)
|
|
297
|
+
if (mutableColumns.length === 0) return
|
|
298
|
+
await executor.exec(
|
|
299
|
+
`UPDATE ${serverTableName} SET ${mutableColumns
|
|
300
|
+
.map(
|
|
301
|
+
(columnName) => `${quoteSQLiteIdentifier(serverColumn(table, columnName))} = ?`,
|
|
302
|
+
)
|
|
303
|
+
.join(', ')} WHERE ${primaryWhere}`,
|
|
304
|
+
[
|
|
305
|
+
...mutableColumns.map((columnName) =>
|
|
306
|
+
encodeColumn(options, tableName, columnName, values[columnName]),
|
|
307
|
+
),
|
|
308
|
+
...primaryValues,
|
|
309
|
+
],
|
|
310
|
+
{ table: table.serverName ?? table.name, publicTable: tableName, kind },
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function serverColumn(table: ZeroSchema['tables'][string], columnName: string) {
|
|
315
|
+
const column = table.columns[columnName]
|
|
316
|
+
if (!column) throw new Error(`unknown SQLite column ${table.name}.${columnName}`)
|
|
317
|
+
return column.serverName ?? columnName
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function encodeColumn<Schema extends ZeroSchema>(
|
|
321
|
+
options: SQLiteTransactionOptions<Schema>,
|
|
322
|
+
tableName: string,
|
|
323
|
+
columnName: string,
|
|
324
|
+
value: unknown,
|
|
325
|
+
) {
|
|
326
|
+
return options.encodeValue(value, {
|
|
327
|
+
column: options.schema.tables[tableName]!.columns[columnName]!,
|
|
328
|
+
columnName,
|
|
329
|
+
tableName,
|
|
330
|
+
})
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function createLegacyQueryBuilder<Schema extends ZeroSchema>(
|
|
334
|
+
schema: Schema,
|
|
335
|
+
run: (query: unknown) => Promise<unknown>,
|
|
336
|
+
) {
|
|
337
|
+
const builder = createBuilder(schema)
|
|
338
|
+
return new Proxy(builder, {
|
|
339
|
+
get(target, property) {
|
|
340
|
+
return makeRunnableQuery(Reflect.get(target, property, target), run)
|
|
341
|
+
},
|
|
342
|
+
})
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function makeRunnableQuery(
|
|
346
|
+
query: unknown,
|
|
347
|
+
run: (query: unknown) => Promise<unknown>,
|
|
348
|
+
): unknown {
|
|
349
|
+
if (!isQueryBuilder(query)) return query
|
|
350
|
+
return new Proxy(query as object, {
|
|
351
|
+
get(target, property) {
|
|
352
|
+
if (property === 'run') return () => run(target)
|
|
353
|
+
if (property === 'materialize' || property === 'preload') {
|
|
354
|
+
return () => {
|
|
355
|
+
throw new Error(`${String(property)}() is not supported in server transactions`)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const value = Reflect.get(target, property, target)
|
|
359
|
+
if (typeof value !== 'function') return value
|
|
360
|
+
return (...args: unknown[]) =>
|
|
361
|
+
makeRunnableQuery(Reflect.apply(value, target, args), run)
|
|
362
|
+
},
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function isQueryBuilder(value: unknown) {
|
|
367
|
+
if (!value || typeof value !== 'object') return false
|
|
368
|
+
try {
|
|
369
|
+
asQueryInternals(value as never)
|
|
370
|
+
return true
|
|
371
|
+
} catch {
|
|
372
|
+
return false
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function createLedgerHooks<Schema extends ZeroSchema>(
|
|
377
|
+
options: SQLiteDatabaseOptions<Schema>,
|
|
378
|
+
executor: SQLiteTransactionExecutor,
|
|
379
|
+
ensureLedger: (upstreamSchema: string) => Promise<void>,
|
|
380
|
+
input?: TransactionProviderInput,
|
|
381
|
+
): TransactionProviderHooks {
|
|
382
|
+
const upstreamSchema = input?.upstreamSchema ?? ''
|
|
383
|
+
const clientGroupID = input?.clientGroupID ?? ''
|
|
384
|
+
const clientID = input?.clientID ?? ''
|
|
385
|
+
const tables = options.ledgerTables(upstreamSchema)
|
|
386
|
+
const clientsTable = quoteSQLiteIdentifier(tables.clients)
|
|
387
|
+
const mutationsTable = quoteSQLiteIdentifier(tables.mutations)
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
async updateClientMutationID() {
|
|
391
|
+
await ensureLedger(upstreamSchema)
|
|
392
|
+
const rows = await executor.query<{ lastMutationID: number | bigint }>(
|
|
393
|
+
`INSERT INTO ${clientsTable}
|
|
394
|
+
(clientGroupID, clientID, lastMutationID)
|
|
395
|
+
VALUES (?, ?, 1)
|
|
396
|
+
ON CONFLICT (clientGroupID, clientID)
|
|
397
|
+
DO UPDATE SET lastMutationID = lastMutationID + 1
|
|
398
|
+
RETURNING lastMutationID`,
|
|
399
|
+
[clientGroupID, clientID],
|
|
400
|
+
)
|
|
401
|
+
const lastMutationID = rows[0]?.lastMutationID
|
|
402
|
+
if (typeof lastMutationID !== 'number' && typeof lastMutationID !== 'bigint') {
|
|
403
|
+
throw new Error('SQLite mutation ledger returned no lastMutationID')
|
|
404
|
+
}
|
|
405
|
+
return { lastMutationID }
|
|
406
|
+
},
|
|
407
|
+
async writeMutationResult(result) {
|
|
408
|
+
await ensureLedger(upstreamSchema)
|
|
409
|
+
await executor.exec(
|
|
410
|
+
`INSERT INTO ${mutationsTable}
|
|
411
|
+
(clientGroupID, clientID, mutationID, result)
|
|
412
|
+
VALUES (?, ?, ?, ?)`,
|
|
413
|
+
[clientGroupID, result.id.clientID, result.id.id, JSON.stringify(result.result)],
|
|
414
|
+
)
|
|
415
|
+
},
|
|
416
|
+
async deleteMutationResults(args) {
|
|
417
|
+
await ensureLedger(upstreamSchema)
|
|
418
|
+
if ('type' in args && args.type === 'bulk') {
|
|
419
|
+
if (args.clientIDs.length === 0) return
|
|
420
|
+
await executor.exec(
|
|
421
|
+
`DELETE FROM ${mutationsTable}
|
|
422
|
+
WHERE clientGroupID = ?
|
|
423
|
+
AND clientID IN (${args.clientIDs.map(() => '?').join(', ')})`,
|
|
424
|
+
[args.clientGroupID, ...args.clientIDs],
|
|
425
|
+
)
|
|
426
|
+
return
|
|
427
|
+
}
|
|
428
|
+
await executor.exec(
|
|
429
|
+
`DELETE FROM ${mutationsTable}
|
|
430
|
+
WHERE clientGroupID = ?
|
|
431
|
+
AND clientID = ?
|
|
432
|
+
AND mutationID <= ?`,
|
|
433
|
+
[args.clientGroupID, args.clientID, args.upToMutationID],
|
|
434
|
+
)
|
|
435
|
+
},
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export async function persistAlreadyProcessedMutationResults<Schema extends ZeroSchema>(
|
|
440
|
+
options: SQLiteDatabaseOptions<Schema>,
|
|
441
|
+
{ clientGroupID, mutations, upstreamSchema }: PersistAlreadyProcessedArgs,
|
|
442
|
+
) {
|
|
443
|
+
const tables = options.ledgerTables(upstreamSchema)
|
|
444
|
+
const table = quoteSQLiteIdentifier(tables.mutations)
|
|
445
|
+
const key = ledgerKey(tables)
|
|
446
|
+
await options.transactionProvider(async (executor) => {
|
|
447
|
+
if (!options.initializedLedgers.has(key)) {
|
|
448
|
+
await createLedgerTables(executor, tables)
|
|
449
|
+
}
|
|
450
|
+
for (const mutation of mutations) {
|
|
451
|
+
await executor.exec(
|
|
452
|
+
`INSERT INTO ${table}
|
|
453
|
+
(clientGroupID, clientID, mutationID, result)
|
|
454
|
+
VALUES (?, ?, ?, ?)
|
|
455
|
+
ON CONFLICT (clientGroupID, clientID, mutationID) DO NOTHING`,
|
|
456
|
+
[
|
|
457
|
+
clientGroupID,
|
|
458
|
+
mutation.id.clientID,
|
|
459
|
+
mutation.id.id,
|
|
460
|
+
JSON.stringify(mutation.result),
|
|
461
|
+
],
|
|
462
|
+
)
|
|
463
|
+
}
|
|
464
|
+
})
|
|
465
|
+
options.initializedLedgers.add(key)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async function createLedgerTables(
|
|
469
|
+
executor: SQLiteTransactionExecutor,
|
|
470
|
+
tables: SQLiteLedgerTables,
|
|
471
|
+
) {
|
|
472
|
+
await executor.exec(
|
|
473
|
+
`CREATE TABLE IF NOT EXISTS ${quoteSQLiteIdentifier(tables.clients)} (
|
|
474
|
+
clientGroupID TEXT NOT NULL,
|
|
475
|
+
clientID TEXT NOT NULL,
|
|
476
|
+
lastMutationID INTEGER NOT NULL,
|
|
477
|
+
PRIMARY KEY (clientGroupID, clientID)
|
|
478
|
+
)`,
|
|
479
|
+
)
|
|
480
|
+
await executor.exec(
|
|
481
|
+
`CREATE TABLE IF NOT EXISTS ${quoteSQLiteIdentifier(tables.mutations)} (
|
|
482
|
+
clientGroupID TEXT NOT NULL,
|
|
483
|
+
clientID TEXT NOT NULL,
|
|
484
|
+
mutationID INTEGER NOT NULL,
|
|
485
|
+
result TEXT NOT NULL,
|
|
486
|
+
PRIMARY KEY (clientGroupID, clientID, mutationID)
|
|
487
|
+
)`,
|
|
488
|
+
)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function ledgerKey(tables: SQLiteLedgerTables) {
|
|
492
|
+
return `${tables.clients}\0${tables.mutations}`
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export function defaultLedgerTables(upstreamSchema: string): SQLiteLedgerTables {
|
|
496
|
+
const prefix = upstreamSchema ? `${upstreamSchema}_` : ''
|
|
497
|
+
return {
|
|
498
|
+
clients: `${prefix}clients`,
|
|
499
|
+
mutations: `${prefix}mutations`,
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export function encodeSQLiteValue(value: unknown, { column }: SQLiteValueContext) {
|
|
504
|
+
if (value === null) return null
|
|
505
|
+
if (column.type === 'boolean') return value ? 1 : 0
|
|
506
|
+
if (column.type === 'json') return JSON.stringify(value)
|
|
507
|
+
return value
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function quoteSQLiteIdentifier(value: string) {
|
|
511
|
+
return `"${value.replaceAll('"', '""')}"`
|
|
512
|
+
}
|
package/src/syncHost.test.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { zeroHttpFixtureSchema } from './httpPull/fixtureSchema'
|
|
|
9
9
|
import { createZeroSyncHostAdapter } from './syncHost'
|
|
10
10
|
import { zql } from './zql'
|
|
11
11
|
|
|
12
|
-
import type { SQLiteQueryFormat } from './createZeroSQLiteServer'
|
|
12
|
+
import type { SqlStatementMetadata, SQLiteQueryFormat } from './createZeroSQLiteServer'
|
|
13
13
|
import type {
|
|
14
14
|
SyncHostApplicationTransaction,
|
|
15
15
|
SyncHostMutatorContext,
|
|
@@ -29,15 +29,19 @@ describe('createZeroSyncHostAdapter', () => {
|
|
|
29
29
|
let executor: SyncHostTransactionExecutor
|
|
30
30
|
let transaction: SyncHostApplicationTransaction
|
|
31
31
|
let lastQueryFormat: SQLiteQueryFormat | undefined
|
|
32
|
+
let statements: SqlStatementMetadata[]
|
|
32
33
|
|
|
33
34
|
beforeEach(() => {
|
|
34
35
|
db = new DatabaseSync(':memory:')
|
|
35
36
|
db.exec(
|
|
36
37
|
'CREATE TABLE project (id TEXT PRIMARY KEY, ownerId TEXT NOT NULL, name TEXT NOT NULL)',
|
|
37
38
|
)
|
|
39
|
+
statements = []
|
|
38
40
|
executor = {
|
|
39
|
-
exec(sql, params = []) {
|
|
40
|
-
|
|
41
|
+
exec(sql, params = [], metadata) {
|
|
42
|
+
if (metadata) statements.push(metadata)
|
|
43
|
+
const result = db.prepare(sql).run(...(params as SQLInputValue[]))
|
|
44
|
+
return { changes: Number(result.changes) }
|
|
41
45
|
},
|
|
42
46
|
query<Row extends Record<string, unknown>>(sql, params = []) {
|
|
43
47
|
return db.prepare(sql).all(...(params as SQLInputValue[])) as Row[]
|
|
@@ -114,6 +118,9 @@ describe('createZeroSyncHostAdapter', () => {
|
|
|
114
118
|
])
|
|
115
119
|
expect(validatedAuthID).toBe('u1')
|
|
116
120
|
expect(effectAuthID).toBe('u1')
|
|
121
|
+
expect(statements).toEqual([
|
|
122
|
+
{ table: 'project', publicTable: 'project', kind: 'insert' },
|
|
123
|
+
])
|
|
117
124
|
})
|
|
118
125
|
|
|
119
126
|
test('returns a structural application error and drops rolled-back effects', async () => {
|
package/src/syncHost.ts
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
import { asQueryInternals } from '@rocicorp/zero/bindings'
|
|
2
2
|
|
|
3
3
|
import { createPermissions } from './createPermissions'
|
|
4
|
-
import { resolveServerQuery, type ZeroServerCommonOptions } from './createZeroServerCore'
|
|
5
4
|
import {
|
|
6
|
-
|
|
5
|
+
createZeroDirectServer,
|
|
6
|
+
resolveServerQuery,
|
|
7
|
+
type ZeroDirectServer,
|
|
8
|
+
type ZeroServerCommonOptions,
|
|
9
|
+
} from './createZeroDirectServer'
|
|
10
|
+
import {
|
|
11
|
+
createSQLiteDatabase,
|
|
7
12
|
createZeroSQLiteTransaction,
|
|
13
|
+
defaultLedgerTables,
|
|
14
|
+
encodeSQLiteValue,
|
|
15
|
+
type SqlStatementMetadata,
|
|
16
|
+
type SQLiteExecResult,
|
|
8
17
|
type SQLiteTransactionExecutor,
|
|
18
|
+
type SQLiteTransactionProvider,
|
|
9
19
|
type ZeroSQLiteTransactionOptions,
|
|
10
|
-
} from './
|
|
20
|
+
} from './sqliteRuntime'
|
|
11
21
|
import { createMutators } from './helpers/createMutators'
|
|
12
22
|
import { runWithAuthScope } from './helpers/mutatorContext'
|
|
13
23
|
import { runWithQueryContext } from './helpers/queryContext'
|
|
@@ -34,14 +44,18 @@ export type SyncHostTransactionContext = {
|
|
|
34
44
|
}
|
|
35
45
|
|
|
36
46
|
export type SyncHostTransactionExecutor = {
|
|
37
|
-
exec(
|
|
47
|
+
exec(
|
|
48
|
+
sql: string,
|
|
49
|
+
params?: readonly unknown[],
|
|
50
|
+
metadata?: SqlStatementMetadata,
|
|
51
|
+
): SQLiteExecResult | Promise<SQLiteExecResult>
|
|
38
52
|
query<Row extends Record<string, unknown> = Record<string, unknown>>(
|
|
39
53
|
sql: string,
|
|
40
54
|
params?: readonly unknown[],
|
|
41
55
|
): Row[] | Promise<Row[]>
|
|
42
56
|
queryAst<Result = unknown>(
|
|
43
57
|
ast: SyncHostJsonValue,
|
|
44
|
-
format: import('./
|
|
58
|
+
format: import('./sqliteRuntime').SQLiteQueryFormat,
|
|
45
59
|
queryName?: string,
|
|
46
60
|
): Result | Promise<Result>
|
|
47
61
|
}
|
|
@@ -79,6 +93,19 @@ export type CreateZeroSyncHostAdapterOptions<
|
|
|
79
93
|
defaultMutateAuthData?: Pick<AuthData, 'email' | 'id'> & Partial<AuthData>
|
|
80
94
|
}
|
|
81
95
|
|
|
96
|
+
export type ZeroSyncHostAdapter<
|
|
97
|
+
Schema extends ZeroSchema,
|
|
98
|
+
Models extends GenericModels,
|
|
99
|
+
> = {
|
|
100
|
+
mutators: Readonly<Record<string, SyncHostRegisteredMutator>>
|
|
101
|
+
resolveQuery(
|
|
102
|
+
name: string,
|
|
103
|
+
args: readonly SyncHostJsonValue[],
|
|
104
|
+
claims: SyncHostClaims,
|
|
105
|
+
): Promise<SyncHostJsonValue>
|
|
106
|
+
server: ZeroDirectServer<Schema, Models>
|
|
107
|
+
}
|
|
108
|
+
|
|
82
109
|
export class MutationApplicationError extends Error {
|
|
83
110
|
constructor(
|
|
84
111
|
readonly details: string,
|
|
@@ -98,7 +125,10 @@ export function createZeroSyncHostAdapter<
|
|
|
98
125
|
mapAuthData = defaultMapAuthData,
|
|
99
126
|
transaction,
|
|
100
127
|
...options
|
|
101
|
-
}: CreateZeroSyncHostAdapterOptions<Schema, Models, ServerActions>)
|
|
128
|
+
}: CreateZeroSyncHostAdapterOptions<Schema, Models, ServerActions>): ZeroSyncHostAdapter<
|
|
129
|
+
Schema,
|
|
130
|
+
Models
|
|
131
|
+
> {
|
|
102
132
|
setSchema(options.schema)
|
|
103
133
|
setEnvironment('server')
|
|
104
134
|
|
|
@@ -163,11 +193,18 @@ export function createZeroSyncHostAdapter<
|
|
|
163
193
|
// creating a second on-zero cleanup ledger.
|
|
164
194
|
mutators._zero_cleanupResults = async () => {}
|
|
165
195
|
|
|
166
|
-
const
|
|
196
|
+
const transactionProvider: SQLiteTransactionProvider = (work) =>
|
|
197
|
+
transaction((executor) => work(toSQLiteExecutor(executor)))
|
|
198
|
+
const directDatabaseOptions = {
|
|
199
|
+
encodeValue: encodeValue ?? encodeSQLiteValue,
|
|
200
|
+
initializedLedgers: new Set<string>(),
|
|
201
|
+
ledgerTables: defaultLedgerTables,
|
|
202
|
+
schema: options.schema,
|
|
203
|
+
transactionProvider,
|
|
204
|
+
}
|
|
205
|
+
const direct = createZeroDirectServer({
|
|
167
206
|
...options,
|
|
168
|
-
|
|
169
|
-
transactionProvider: (work) =>
|
|
170
|
-
transaction((executor) => work(toSQLiteExecutor(executor))),
|
|
207
|
+
createDatabase: () => createSQLiteDatabase(directDatabaseOptions),
|
|
171
208
|
})
|
|
172
209
|
|
|
173
210
|
return {
|
|
@@ -214,8 +251,8 @@ function toSQLiteExecutor(
|
|
|
214
251
|
executor: SyncHostTransactionExecutor,
|
|
215
252
|
): SQLiteTransactionExecutor {
|
|
216
253
|
return {
|
|
217
|
-
exec(sql, params) {
|
|
218
|
-
return executor.exec(sql, params)
|
|
254
|
+
exec(sql, params, metadata) {
|
|
255
|
+
return executor.exec(sql, params, metadata)
|
|
219
256
|
},
|
|
220
257
|
query<Row extends Record<string, unknown>>(sql: string, params?: readonly unknown[]) {
|
|
221
258
|
return executor.query<Row>(sql, params)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createZeroServer } from 'on-zero/server'
|
|
2
|
-
import { createZeroSQLiteServer } from 'on-zero/sqlite'
|
|
2
|
+
import { createZeroSQLiteServer, type SqlStatementMetadata } from 'on-zero/sqlite'
|
|
3
3
|
|
|
4
4
|
import type { Config, MutatorContext } from 'on-zero'
|
|
5
5
|
import type { Schema as ZeroSchema } from '@rocicorp/zero'
|
|
@@ -37,6 +37,13 @@ const authData: Config['authData'] = {
|
|
|
37
37
|
role: 'admin',
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
const sqliteWrite: SqlStatementMetadata = {
|
|
41
|
+
table: 'project_records',
|
|
42
|
+
publicTable: 'project',
|
|
43
|
+
kind: 'upsert',
|
|
44
|
+
}
|
|
45
|
+
void sqliteWrite
|
|
46
|
+
|
|
40
47
|
postgresServer.mutate.project.create(
|
|
41
48
|
{ id: 'pg' },
|
|
42
49
|
{ authData: { email: authData.email, id: authData.id, role: authData.role } },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { drizzleZeroConfig as createSchemaFromDrizzle } from 'drizzle-zero';
|
|
2
|
-
export { createZeroTableBuilder } from 'drizzle-zero';
|
|
1
|
+
export { drizzleZeroConfig as createSchemaFromDrizzle } from 'drizzle-zero-sqlite';
|
|
2
|
+
export { createZeroTableBuilder } from 'drizzle-zero-sqlite';
|
|
3
3
|
export { generateDrizzleSchemaFile } from './generate';
|
|
4
4
|
//# sourceMappingURL=createSchemaFromDrizzle.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createSchemaFromDrizzle.d.ts","sourceRoot":"","sources":["../src/createSchemaFromDrizzle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,IAAI,uBAAuB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"createSchemaFromDrizzle.d.ts","sourceRoot":"","sources":["../src/createSchemaFromDrizzle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,IAAI,uBAAuB,EAAE,MAAM,qBAAqB,CAAA;AAClF,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAA;AAC5D,OAAO,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAA"}
|