orez 0.1.7 → 0.1.8
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 +2 -1
- package/dist/cli.js +7 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +54 -6
- package/dist/index.js.map +1 -1
- package/dist/integration/test-permissions.d.ts +2 -0
- package/dist/integration/test-permissions.d.ts.map +1 -1
- package/dist/integration/test-permissions.js +28 -0
- package/dist/integration/test-permissions.js.map +1 -1
- package/dist/sqlite-mode/native-binary.js +1 -1
- package/dist/sqlite-mode/native-binary.js.map +1 -1
- package/dist/sqlite-mode/package-resolve.d.ts +6 -0
- package/dist/sqlite-mode/package-resolve.d.ts.map +1 -0
- package/dist/sqlite-mode/package-resolve.js +20 -0
- package/dist/sqlite-mode/package-resolve.js.map +1 -0
- package/dist/sqlite-mode/resolve-mode.d.ts +12 -7
- package/dist/sqlite-mode/resolve-mode.d.ts.map +1 -1
- package/dist/sqlite-mode/resolve-mode.js +27 -23
- package/dist/sqlite-mode/resolve-mode.js.map +1 -1
- package/package.json +2 -2
- package/src/cli.ts +7 -1
- package/src/config.ts +2 -0
- package/src/index.ts +69 -6
- package/src/integration/integration.test.ts +49 -42
- package/src/integration/restore-live-stress.test.ts +61 -65
- package/src/integration/restore-reset.test.ts +63 -66
- package/src/integration/test-permissions.ts +36 -0
- package/src/sqlite-mode/native-binary.ts +1 -1
- package/src/sqlite-mode/package-resolve.ts +17 -0
- package/src/sqlite-mode/resolve-mode.ts +31 -21
- package/src/sqlite-mode/sqlite-mode.test.ts +11 -5
|
@@ -18,10 +18,25 @@ import WebSocket from 'ws'
|
|
|
18
18
|
|
|
19
19
|
import { execDumpFile } from '../cli.js'
|
|
20
20
|
import { startZeroLite } from '../index.js'
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
ensureTablesInPublications,
|
|
23
|
+
hasNonNullPermissions,
|
|
24
|
+
installAllowAllPermissions,
|
|
25
|
+
} from './test-permissions.js'
|
|
22
26
|
|
|
23
27
|
// zero-cache protocol version (from @rocicorp/zero/out/zero-protocol/src/protocol-version.js)
|
|
24
28
|
const PROTOCOL_VERSION = 45
|
|
29
|
+
const RESET_CLIENT_SCHEMA = {
|
|
30
|
+
tables: {
|
|
31
|
+
reset_probe: {
|
|
32
|
+
columns: {
|
|
33
|
+
id: { type: 'string' },
|
|
34
|
+
value: { type: 'string' },
|
|
35
|
+
},
|
|
36
|
+
primaryKey: ['id'],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
}
|
|
25
40
|
|
|
26
41
|
// encode initConnection message for sec-websocket-protocol header
|
|
27
42
|
// matches zero-protocol's encodeSecProtocols implementation
|
|
@@ -117,6 +132,7 @@ describe('restore/reset integration regression', { timeout: 150_000 }, () => {
|
|
|
117
132
|
let zeroPort: number
|
|
118
133
|
let shutdown: () => Promise<void>
|
|
119
134
|
let restartZero: (() => Promise<void>) | undefined
|
|
135
|
+
let resetZeroFull: (() => Promise<void>) | undefined
|
|
120
136
|
let dataDir: string
|
|
121
137
|
let dumpFile: string
|
|
122
138
|
let dumpFileIsTemp = false
|
|
@@ -143,6 +159,7 @@ describe('restore/reset integration regression', { timeout: 150_000 }, () => {
|
|
|
143
159
|
zeroPort = started.zeroPort
|
|
144
160
|
shutdown = started.stop
|
|
145
161
|
restartZero = started.restartZero
|
|
162
|
+
resetZeroFull = started.resetZeroFull
|
|
146
163
|
|
|
147
164
|
await waitForZero(zeroPort, 90_000)
|
|
148
165
|
}, 120_000)
|
|
@@ -205,6 +222,7 @@ describe('restore/reset integration regression', { timeout: 150_000 }, () => {
|
|
|
205
222
|
AFTER INSERT OR UPDATE OR DELETE ON public.reset_probe
|
|
206
223
|
FOR EACH STATEMENT EXECUTE FUNCTION public._zero_notify_change();
|
|
207
224
|
`)
|
|
225
|
+
await ensureTablesInPublications(db, ['reset_probe'])
|
|
208
226
|
const pubName = process.env.ZERO_APP_PUBLICATIONS?.trim()
|
|
209
227
|
if (pubName) {
|
|
210
228
|
const quotedPub = '"' + pubName.replace(/"/g, '""') + '"'
|
|
@@ -213,7 +231,11 @@ describe('restore/reset integration regression', { timeout: 150_000 }, () => {
|
|
|
213
231
|
.catch(() => {})
|
|
214
232
|
}
|
|
215
233
|
await installAllowAllPermissions(db, ['reset_probe'])
|
|
216
|
-
|
|
234
|
+
expect(await hasNonNullPermissions(db)).toBe(true)
|
|
235
|
+
if (resetZeroFull) {
|
|
236
|
+
await resetZeroFull()
|
|
237
|
+
await waitForZero(zeroPort, 90_000)
|
|
238
|
+
} else if (restartZero) {
|
|
217
239
|
await restartZero()
|
|
218
240
|
await waitForZero(zeroPort, 60_000)
|
|
219
241
|
}
|
|
@@ -259,79 +281,54 @@ function connectAndSubscribe(
|
|
|
259
281
|
const ts = Date.now()
|
|
260
282
|
const clientGroupID = `restore-reset-cg-${ts}`
|
|
261
283
|
const clientID = 'restore-reset-client'
|
|
262
|
-
const
|
|
284
|
+
const initConnectionMessage: [string, Record<string, unknown>] = [
|
|
285
|
+
'initConnection',
|
|
286
|
+
{
|
|
287
|
+
desiredQueriesPatch: [{ op: 'put', hash: 'q1', ast: query }],
|
|
288
|
+
clientSchema: RESET_CLIENT_SCHEMA,
|
|
289
|
+
},
|
|
290
|
+
]
|
|
291
|
+
const secProtocol = encodeSecProtocols(initConnectionMessage, undefined)
|
|
292
|
+
const ws = new WebSocket(
|
|
263
293
|
`ws://127.0.0.1:${port}/sync/v${PROTOCOL_VERSION}/connect` +
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
// bootstrap the client group first so the query connection is not "new group"
|
|
267
|
-
const bootstrapProtocol = encodeSecProtocols(
|
|
268
|
-
['initConnection', { desiredQueriesPatch: [] }],
|
|
269
|
-
undefined
|
|
294
|
+
`?clientGroupID=${clientGroupID}&clientID=${clientID}&wsid=ws1&schemaVersion=1&baseCookie=&ts=${ts}&lmid=0`,
|
|
295
|
+
secProtocol
|
|
270
296
|
)
|
|
271
|
-
const bootstrapWs = new WebSocket(`${urlBase}&wsid=bootstrap`, bootstrapProtocol)
|
|
272
|
-
const bootstrapTimer = setTimeout(() => {
|
|
273
|
-
fail(new Error('bootstrap websocket timeout'))
|
|
274
|
-
}, 7000)
|
|
275
|
-
|
|
276
|
-
const fail = (err: unknown) => {
|
|
277
|
-
clearTimeout(bootstrapTimer)
|
|
278
|
-
try {
|
|
279
|
-
bootstrapWs.close()
|
|
280
|
-
} catch {}
|
|
281
|
-
reject(err)
|
|
282
|
-
}
|
|
283
297
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
298
|
+
let settled = false
|
|
299
|
+
let sawMessage = false
|
|
300
|
+
const failTimer = setTimeout(() => {
|
|
301
|
+
if (settled) return
|
|
302
|
+
settled = true
|
|
287
303
|
try {
|
|
288
|
-
|
|
304
|
+
ws.close()
|
|
289
305
|
} catch {}
|
|
306
|
+
reject(new Error('websocket connected but no downstream messages'))
|
|
307
|
+
}, 7000)
|
|
290
308
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
]
|
|
297
|
-
const secProtocol = encodeSecProtocols(initConnectionMessage, undefined)
|
|
298
|
-
const ws = new WebSocket(`${urlBase}&wsid=ws1`, secProtocol)
|
|
299
|
-
|
|
300
|
-
let settled = false
|
|
301
|
-
let sawMessage = false
|
|
302
|
-
const failTimer = setTimeout(() => {
|
|
303
|
-
if (settled) return
|
|
304
|
-
settled = true
|
|
305
|
-
try {
|
|
306
|
-
ws.close()
|
|
307
|
-
} catch {}
|
|
308
|
-
reject(new Error('websocket connected but no downstream messages'))
|
|
309
|
-
}, 7000)
|
|
310
|
-
|
|
311
|
-
ws.on('message', (data) => {
|
|
312
|
-
const msg = JSON.parse(data.toString())
|
|
313
|
-
downstream.enqueue(msg)
|
|
314
|
-
if (!sawMessage && !settled) {
|
|
315
|
-
sawMessage = true
|
|
316
|
-
settled = true
|
|
317
|
-
clearTimeout(failTimer)
|
|
318
|
-
resolve(ws)
|
|
319
|
-
}
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
ws.once('error', (err) => {
|
|
323
|
-
if (settled) return
|
|
309
|
+
ws.on('message', (data) => {
|
|
310
|
+
const msg = JSON.parse(data.toString())
|
|
311
|
+
downstream.enqueue(msg)
|
|
312
|
+
if (!sawMessage && !settled) {
|
|
313
|
+
sawMessage = true
|
|
324
314
|
settled = true
|
|
325
315
|
clearTimeout(failTimer)
|
|
326
|
-
|
|
327
|
-
}
|
|
316
|
+
resolve(ws)
|
|
317
|
+
}
|
|
318
|
+
})
|
|
328
319
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
320
|
+
ws.once('error', (err) => {
|
|
321
|
+
if (settled) return
|
|
322
|
+
settled = true
|
|
323
|
+
clearTimeout(failTimer)
|
|
324
|
+
reject(err)
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
ws.once('close', () => {
|
|
328
|
+
if (settled) return
|
|
329
|
+
settled = true
|
|
330
|
+
clearTimeout(failTimer)
|
|
331
|
+
reject(new Error('websocket closed before initial downstream message'))
|
|
335
332
|
})
|
|
336
333
|
})
|
|
337
334
|
}
|
|
@@ -84,6 +84,42 @@ export async function installAllowAllPermissions(
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
export async function hasNonNullPermissions(db: DbLike): Promise<boolean> {
|
|
88
|
+
const schemas = await findPermissionsSchemas(db)
|
|
89
|
+
for (const schema of schemas) {
|
|
90
|
+
const quotedSchema = '"' + schema.replace(/"/g, '""') + '"'
|
|
91
|
+
const result = await db.query<{ has_permissions: boolean }>(
|
|
92
|
+
`SELECT (permissions IS NOT NULL) AS has_permissions
|
|
93
|
+
FROM ${quotedSchema}.permissions
|
|
94
|
+
WHERE lock = true
|
|
95
|
+
LIMIT 1`
|
|
96
|
+
)
|
|
97
|
+
if (result.rows[0]?.has_permissions) return true
|
|
98
|
+
}
|
|
99
|
+
return false
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function ensureTablesInPublications(
|
|
103
|
+
db: DbLike,
|
|
104
|
+
tables: string[]
|
|
105
|
+
): Promise<void> {
|
|
106
|
+
const pubs = await db.query<{ pubname: string }>(
|
|
107
|
+
`SELECT pubname
|
|
108
|
+
FROM pg_publication
|
|
109
|
+
WHERE pubname NOT LIKE '%metadata%'
|
|
110
|
+
ORDER BY pubname`
|
|
111
|
+
)
|
|
112
|
+
for (const { pubname } of pubs.rows) {
|
|
113
|
+
const quotedPub = '"' + pubname.replace(/"/g, '""') + '"'
|
|
114
|
+
for (const table of tables) {
|
|
115
|
+
const quotedTable = '"' + table.replace(/"/g, '""') + '"'
|
|
116
|
+
await db
|
|
117
|
+
.exec(`ALTER PUBLICATION ${quotedPub} ADD TABLE "public".${quotedTable}`)
|
|
118
|
+
.catch(() => {})
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
87
123
|
function parsePermissions(value: unknown): { tables?: Record<string, unknown> } {
|
|
88
124
|
if (!value) return {}
|
|
89
125
|
if (typeof value === 'string') {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs'
|
|
2
2
|
import { dirname, resolve } from 'node:path'
|
|
3
3
|
|
|
4
|
-
import { resolvePackage } from './resolve
|
|
4
|
+
import { resolvePackage } from './package-resolve.js'
|
|
5
5
|
|
|
6
6
|
const NATIVE_BINARY_RELATIVE_PATHS = ['build/Release/better_sqlite3.node']
|
|
7
7
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* resolve a package entry path
|
|
5
|
+
* import.meta.resolve doesn't work in vitest, so we fall back to require.resolve
|
|
6
|
+
*/
|
|
7
|
+
export function resolvePackage(pkg: string): string {
|
|
8
|
+
try {
|
|
9
|
+
const resolved = import.meta.resolve(pkg)
|
|
10
|
+
if (resolved) return resolved.replace('file://', '')
|
|
11
|
+
} catch {}
|
|
12
|
+
try {
|
|
13
|
+
const require = createRequire(import.meta.url)
|
|
14
|
+
return require.resolve(pkg)
|
|
15
|
+
} catch {}
|
|
16
|
+
return ''
|
|
17
|
+
}
|
|
@@ -1,33 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* mode resolution - canonical place to determine sqlite mode from config/env
|
|
3
|
+
*
|
|
4
|
+
* priority:
|
|
5
|
+
* 1. explicit --disable-wasm-sqlite flag → native
|
|
6
|
+
* 2. explicit --force-wasm-sqlite flag → wasm
|
|
7
|
+
* 3. native binary available → native (auto-detect)
|
|
8
|
+
* 4. fallback → wasm
|
|
3
9
|
*/
|
|
4
10
|
|
|
5
|
-
import {
|
|
11
|
+
import { inspectNativeSqliteBinary } from './native-binary.js'
|
|
12
|
+
import { resolvePackage } from './package-resolve.js'
|
|
6
13
|
|
|
7
14
|
import type { SqliteMode, SqliteModeConfig } from './types.js'
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* resolve a package entry path
|
|
11
|
-
* import.meta.resolve doesn't work in vitest, so we fall back to require.resolve
|
|
12
|
-
*/
|
|
13
|
-
export function resolvePackage(pkg: string): string {
|
|
14
|
-
try {
|
|
15
|
-
const resolved = import.meta.resolve(pkg)
|
|
16
|
-
if (resolved) return resolved.replace('file://', '')
|
|
17
|
-
} catch {}
|
|
18
|
-
try {
|
|
19
|
-
const require = createRequire(import.meta.url)
|
|
20
|
-
return require.resolve(pkg)
|
|
21
|
-
} catch {}
|
|
22
|
-
return ''
|
|
23
|
-
}
|
|
15
|
+
export { resolvePackage } from './package-resolve.js'
|
|
24
16
|
|
|
25
17
|
/**
|
|
26
18
|
* resolve sqlite mode from config
|
|
27
19
|
* single source of truth for mode selection
|
|
20
|
+
*
|
|
21
|
+
* @param disableWasmSqlite - explicit flag to force native mode
|
|
22
|
+
* @param forceWasmSqlite - explicit flag to force wasm mode (overrides auto-detect)
|
|
28
23
|
*/
|
|
29
|
-
export function resolveSqliteMode(
|
|
30
|
-
|
|
24
|
+
export function resolveSqliteMode(
|
|
25
|
+
disableWasmSqlite: boolean,
|
|
26
|
+
forceWasmSqlite: boolean = false
|
|
27
|
+
): SqliteMode {
|
|
28
|
+
// explicit native request
|
|
29
|
+
if (disableWasmSqlite) return 'native'
|
|
30
|
+
|
|
31
|
+
// explicit wasm request
|
|
32
|
+
if (forceWasmSqlite) return 'wasm'
|
|
33
|
+
|
|
34
|
+
// auto-detect: prefer native if binary is available
|
|
35
|
+
const nativeCheck = inspectNativeSqliteBinary()
|
|
36
|
+
if (nativeCheck.found) return 'native'
|
|
37
|
+
|
|
38
|
+
// fallback to wasm
|
|
39
|
+
return 'wasm'
|
|
31
40
|
}
|
|
32
41
|
|
|
33
42
|
/**
|
|
@@ -35,9 +44,10 @@ export function resolveSqliteMode(disableWasmSqlite: boolean): SqliteMode {
|
|
|
35
44
|
* returns null if required packages aren't installed
|
|
36
45
|
*/
|
|
37
46
|
export function resolveSqliteModeConfig(
|
|
38
|
-
disableWasmSqlite: boolean
|
|
47
|
+
disableWasmSqlite: boolean,
|
|
48
|
+
forceWasmSqlite: boolean = false
|
|
39
49
|
): SqliteModeConfig | null {
|
|
40
|
-
const mode = resolveSqliteMode(disableWasmSqlite)
|
|
50
|
+
const mode = resolveSqliteMode(disableWasmSqlite, forceWasmSqlite)
|
|
41
51
|
const zeroSqlitePath = resolvePackage('@rocicorp/zero-sqlite3') || undefined
|
|
42
52
|
|
|
43
53
|
// native mode may still need zero-sqlite3 path for restoring from a prior shim
|
|
@@ -28,16 +28,22 @@ describe('sqlite mode types', () => {
|
|
|
28
28
|
})
|
|
29
29
|
|
|
30
30
|
describe('sqlite mode resolution', () => {
|
|
31
|
-
it('resolves
|
|
32
|
-
expect(resolveSqliteMode(false)).toBe('
|
|
31
|
+
it('resolves native mode when disableWasmSqlite is true', () => {
|
|
32
|
+
expect(resolveSqliteMode(true, false)).toBe('native')
|
|
33
33
|
})
|
|
34
34
|
|
|
35
|
-
it('resolves
|
|
36
|
-
expect(resolveSqliteMode(true)).toBe('
|
|
35
|
+
it('resolves wasm mode when forceWasmSqlite is true', () => {
|
|
36
|
+
expect(resolveSqliteMode(false, true)).toBe('wasm')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('auto-detects mode based on native binary availability', () => {
|
|
40
|
+
// when neither flag is set, mode depends on whether native binary exists
|
|
41
|
+
const mode = resolveSqliteMode(false, false)
|
|
42
|
+
expect(['wasm', 'native']).toContain(mode)
|
|
37
43
|
})
|
|
38
44
|
|
|
39
45
|
it('returns config with mode for native', () => {
|
|
40
|
-
const config = resolveSqliteModeConfig(true)
|
|
46
|
+
const config = resolveSqliteModeConfig(true, false)
|
|
41
47
|
expect(config).not.toBeNull()
|
|
42
48
|
expect(config?.mode).toBe('native')
|
|
43
49
|
})
|