orez 0.1.6 → 0.1.7
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 +185 -225
- package/dist/admin/log-store.d.ts.map +1 -1
- package/dist/admin/log-store.js +17 -6
- package/dist/admin/log-store.js.map +1 -1
- package/dist/admin/server.d.ts +1 -0
- package/dist/admin/server.d.ts.map +1 -1
- package/dist/admin/server.js +10 -0
- package/dist/admin/server.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +89 -45
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +104 -17
- package/dist/index.js.map +1 -1
- package/dist/integration/test-permissions.d.ts +5 -0
- package/dist/integration/test-permissions.d.ts.map +1 -0
- package/dist/integration/test-permissions.js +89 -0
- package/dist/integration/test-permissions.js.map +1 -0
- package/dist/pg-proxy.js +2 -2
- package/dist/pg-proxy.js.map +1 -1
- package/dist/replication/change-tracker.d.ts.map +1 -1
- package/dist/replication/change-tracker.js +15 -13
- package/dist/replication/change-tracker.js.map +1 -1
- package/dist/replication/handler.d.ts.map +1 -1
- package/dist/replication/handler.js +27 -2
- package/dist/replication/handler.js.map +1 -1
- package/dist/sqlite-mode/index.d.ts +1 -0
- package/dist/sqlite-mode/index.d.ts.map +1 -1
- package/dist/sqlite-mode/index.js +1 -0
- package/dist/sqlite-mode/index.js.map +1 -1
- package/dist/sqlite-mode/native-binary.d.ts +11 -0
- package/dist/sqlite-mode/native-binary.d.ts.map +1 -0
- package/dist/sqlite-mode/native-binary.js +67 -0
- package/dist/sqlite-mode/native-binary.js.map +1 -0
- package/package.json +8 -2
- package/src/admin/log-store.ts +19 -9
- package/src/admin/server.ts +12 -0
- package/src/cli.ts +92 -43
- package/src/index.ts +117 -18
- package/src/integration/integration.test.ts +86 -15
- package/src/integration/native-binary.guard.test.ts +13 -0
- package/src/integration/native-startup.test.ts +44 -0
- package/src/integration/restore-live-stress.test.ts +437 -0
- package/src/integration/restore-reset.test.ts +135 -16
- package/src/integration/test-permissions.ts +111 -0
- package/src/pg-proxy.ts +2 -2
- package/src/replication/change-tracker.test.ts +1 -1
- package/src/replication/change-tracker.ts +16 -13
- package/src/replication/handler.test.ts +2 -2
- package/src/replication/handler.ts +30 -2
- package/src/sqlite-mode/index.ts +1 -0
- package/src/sqlite-mode/native-binary.ts +89 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { PGlite } from '@electric-sql/pglite'
|
|
2
|
+
|
|
3
|
+
type DbLike = Pick<PGlite, 'query' | 'exec'>
|
|
4
|
+
|
|
5
|
+
const ALLOW_ALL_CONDITION = { type: 'and', conditions: [] as unknown[] }
|
|
6
|
+
const ALLOW_ALL_POLICY = [['allow', ALLOW_ALL_CONDITION]]
|
|
7
|
+
const DEFAULT_APP_ID = process.env.ZERO_APP_ID?.trim() || 'zero'
|
|
8
|
+
|
|
9
|
+
export async function installAllowAllPermissions(
|
|
10
|
+
db: DbLike,
|
|
11
|
+
tables: string[]
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
const schemas = await findPermissionsSchemas(db)
|
|
14
|
+
if (schemas.length === 0) {
|
|
15
|
+
schemas.push(DEFAULT_APP_ID)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
for (const schema of schemas) {
|
|
19
|
+
const quotedSchema = '"' + schema.replace(/"/g, '""') + '"'
|
|
20
|
+
|
|
21
|
+
// Bootstrap the same global permissions table shape zero-cache expects.
|
|
22
|
+
await db.exec(`
|
|
23
|
+
CREATE SCHEMA IF NOT EXISTS ${quotedSchema};
|
|
24
|
+
|
|
25
|
+
CREATE TABLE IF NOT EXISTS ${quotedSchema}.permissions (
|
|
26
|
+
"permissions" JSONB,
|
|
27
|
+
"hash" TEXT,
|
|
28
|
+
"lock" BOOL PRIMARY KEY DEFAULT true CHECK (lock)
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
CREATE OR REPLACE FUNCTION ${quotedSchema}.set_permissions_hash()
|
|
32
|
+
RETURNS TRIGGER AS $$
|
|
33
|
+
BEGIN
|
|
34
|
+
NEW.hash = md5(NEW.permissions::text);
|
|
35
|
+
RETURN NEW;
|
|
36
|
+
END;
|
|
37
|
+
$$ LANGUAGE plpgsql;
|
|
38
|
+
|
|
39
|
+
DROP TRIGGER IF EXISTS on_set_permissions ON ${quotedSchema}.permissions;
|
|
40
|
+
CREATE TRIGGER on_set_permissions
|
|
41
|
+
BEFORE INSERT OR UPDATE ON ${quotedSchema}.permissions
|
|
42
|
+
FOR EACH ROW
|
|
43
|
+
EXECUTE FUNCTION ${quotedSchema}.set_permissions_hash();
|
|
44
|
+
|
|
45
|
+
INSERT INTO ${quotedSchema}.permissions ("permissions")
|
|
46
|
+
VALUES (NULL)
|
|
47
|
+
ON CONFLICT DO NOTHING;
|
|
48
|
+
`)
|
|
49
|
+
|
|
50
|
+
const existing = await db.query<{ permissions: unknown }>(
|
|
51
|
+
`SELECT permissions FROM ${quotedSchema}.permissions WHERE lock = true LIMIT 1`
|
|
52
|
+
)
|
|
53
|
+
const existingPermissions = parsePermissions(existing.rows[0]?.permissions)
|
|
54
|
+
|
|
55
|
+
const tablesToAdd = Object.fromEntries(
|
|
56
|
+
tables.map((table) => [
|
|
57
|
+
table,
|
|
58
|
+
{
|
|
59
|
+
row: {
|
|
60
|
+
select: ALLOW_ALL_POLICY,
|
|
61
|
+
insert: ALLOW_ALL_POLICY,
|
|
62
|
+
update: {
|
|
63
|
+
preMutation: ALLOW_ALL_POLICY,
|
|
64
|
+
postMutation: ALLOW_ALL_POLICY,
|
|
65
|
+
},
|
|
66
|
+
delete: ALLOW_ALL_POLICY,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
])
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
const permissions = {
|
|
73
|
+
...existingPermissions,
|
|
74
|
+
tables: {
|
|
75
|
+
...(existingPermissions.tables || {}),
|
|
76
|
+
...tablesToAdd,
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await db.query(
|
|
81
|
+
`UPDATE ${quotedSchema}.permissions SET permissions = $1 WHERE lock = true`,
|
|
82
|
+
[JSON.stringify(permissions)]
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parsePermissions(value: unknown): { tables?: Record<string, unknown> } {
|
|
88
|
+
if (!value) return {}
|
|
89
|
+
if (typeof value === 'string') {
|
|
90
|
+
try {
|
|
91
|
+
return JSON.parse(value)
|
|
92
|
+
} catch {
|
|
93
|
+
return {}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (typeof value === 'object') return value as { tables?: Record<string, unknown> }
|
|
97
|
+
return {}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function findPermissionsSchemas(db: DbLike): Promise<string[]> {
|
|
101
|
+
const result = await db.query<{ schemaname: string }>(
|
|
102
|
+
`SELECT schemaname
|
|
103
|
+
FROM pg_tables
|
|
104
|
+
WHERE tablename = 'permissions'
|
|
105
|
+
AND schemaname NOT IN ('pg_catalog', 'information_schema')
|
|
106
|
+
AND schemaname NOT LIKE 'pg_%'
|
|
107
|
+
ORDER BY CASE WHEN schemaname = $1 THEN 0 ELSE 1 END, schemaname`,
|
|
108
|
+
[DEFAULT_APP_ID]
|
|
109
|
+
)
|
|
110
|
+
return result.rows.map((r) => r.schemaname)
|
|
111
|
+
}
|
package/src/pg-proxy.ts
CHANGED
|
@@ -57,10 +57,10 @@ const QUERY_REWRITES: Array<{ match: RegExp; replace: string }> = [
|
|
|
57
57
|
match: /\bSET\s+TRANSACTION\s*;/gi,
|
|
58
58
|
replace: ';',
|
|
59
59
|
},
|
|
60
|
-
// redirect pg_replication_slots to our fake table
|
|
60
|
+
// redirect pg_replication_slots to our fake table in _orez schema
|
|
61
61
|
{
|
|
62
62
|
match: /\bpg_replication_slots\b/g,
|
|
63
|
-
replace: '
|
|
63
|
+
replace: '_orez._zero_replication_slots',
|
|
64
64
|
},
|
|
65
65
|
]
|
|
66
66
|
|
|
@@ -150,7 +150,7 @@ describe('change-tracker', () => {
|
|
|
150
150
|
try {
|
|
151
151
|
await db.exec(`CREATE PUBLICATION "zero_scope"`)
|
|
152
152
|
await installChangeTracking(db) // reinstall picks up publication scope
|
|
153
|
-
await db.exec(`TRUNCATE
|
|
153
|
+
await db.exec(`TRUNCATE _orez._zero_changes`)
|
|
154
154
|
|
|
155
155
|
await db.exec(`INSERT INTO public.items (name, value) VALUES ('x', 1)`)
|
|
156
156
|
const changes = await getChangesSince(db, 0)
|
|
@@ -13,13 +13,16 @@ export interface ChangeRecord {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export async function installChangeTracking(db: PGlite): Promise<void> {
|
|
16
|
+
// use _orez schema for internal tables - survives pg_restore of public schema
|
|
17
|
+
await db.exec(`CREATE SCHEMA IF NOT EXISTS _orez`)
|
|
18
|
+
|
|
16
19
|
// create changes table and watermark sequence
|
|
17
20
|
await db.exec(`
|
|
18
|
-
CREATE SEQUENCE IF NOT EXISTS
|
|
21
|
+
CREATE SEQUENCE IF NOT EXISTS _orez._zero_watermark;
|
|
19
22
|
|
|
20
|
-
CREATE TABLE IF NOT EXISTS
|
|
23
|
+
CREATE TABLE IF NOT EXISTS _orez._zero_changes (
|
|
21
24
|
id BIGSERIAL PRIMARY KEY,
|
|
22
|
-
watermark BIGINT NOT NULL DEFAULT nextval('
|
|
25
|
+
watermark BIGINT NOT NULL DEFAULT nextval('_orez._zero_watermark'),
|
|
23
26
|
table_name TEXT NOT NULL,
|
|
24
27
|
op TEXT NOT NULL,
|
|
25
28
|
row_data JSONB,
|
|
@@ -27,9 +30,9 @@ export async function installChangeTracking(db: PGlite): Promise<void> {
|
|
|
27
30
|
changed_at TIMESTAMPTZ DEFAULT NOW()
|
|
28
31
|
);
|
|
29
32
|
|
|
30
|
-
CREATE INDEX IF NOT EXISTS _zero_changes_watermark_idx ON
|
|
33
|
+
CREATE INDEX IF NOT EXISTS _zero_changes_watermark_idx ON _orez._zero_changes (watermark);
|
|
31
34
|
|
|
32
|
-
CREATE TABLE IF NOT EXISTS
|
|
35
|
+
CREATE TABLE IF NOT EXISTS _orez._zero_replication_slots (
|
|
33
36
|
slot_name TEXT PRIMARY KEY,
|
|
34
37
|
restart_lsn TEXT NOT NULL DEFAULT '0/1000000',
|
|
35
38
|
confirmed_flush_lsn TEXT NOT NULL DEFAULT '0/1000000',
|
|
@@ -42,7 +45,7 @@ export async function installChangeTracking(db: PGlite): Promise<void> {
|
|
|
42
45
|
);
|
|
43
46
|
`)
|
|
44
47
|
|
|
45
|
-
// create trigger function
|
|
48
|
+
// create trigger function (writes to _orez schema)
|
|
46
49
|
await db.exec(`
|
|
47
50
|
CREATE OR REPLACE FUNCTION public._zero_track_change() RETURNS TRIGGER AS $$
|
|
48
51
|
DECLARE
|
|
@@ -50,15 +53,15 @@ export async function installChangeTracking(db: PGlite): Promise<void> {
|
|
|
50
53
|
BEGIN
|
|
51
54
|
qualified_name := TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME;
|
|
52
55
|
IF TG_OP = 'DELETE' THEN
|
|
53
|
-
INSERT INTO
|
|
56
|
+
INSERT INTO _orez._zero_changes (table_name, op, old_data)
|
|
54
57
|
VALUES (qualified_name, 'DELETE', row_to_json(OLD)::jsonb);
|
|
55
58
|
RETURN OLD;
|
|
56
59
|
ELSIF TG_OP = 'UPDATE' THEN
|
|
57
|
-
INSERT INTO
|
|
60
|
+
INSERT INTO _orez._zero_changes (table_name, op, row_data, old_data)
|
|
58
61
|
VALUES (qualified_name, 'UPDATE', row_to_json(NEW)::jsonb, row_to_json(OLD)::jsonb);
|
|
59
62
|
RETURN NEW;
|
|
60
63
|
ELSIF TG_OP = 'INSERT' THEN
|
|
61
|
-
INSERT INTO
|
|
64
|
+
INSERT INTO _orez._zero_changes (table_name, op, row_data)
|
|
62
65
|
VALUES (qualified_name, 'INSERT', row_to_json(NEW)::jsonb);
|
|
63
66
|
RETURN NEW;
|
|
64
67
|
END IF;
|
|
@@ -98,7 +101,7 @@ async function installTriggersOnAllTables(db: PGlite): Promise<void> {
|
|
|
98
101
|
const all = await db.query<{ tablename: string }>(
|
|
99
102
|
`SELECT tablename FROM pg_tables
|
|
100
103
|
WHERE schemaname = 'public'
|
|
101
|
-
AND tablename NOT IN ('migrations'
|
|
104
|
+
AND tablename NOT IN ('migrations')
|
|
102
105
|
AND tablename NOT LIKE '_zero_%'`
|
|
103
106
|
)
|
|
104
107
|
tables = all.rows
|
|
@@ -208,7 +211,7 @@ export async function getChangesSince(
|
|
|
208
211
|
limit = 1000
|
|
209
212
|
): Promise<ChangeRecord[]> {
|
|
210
213
|
const result = await db.query<ChangeRecord>(
|
|
211
|
-
'SELECT * FROM
|
|
214
|
+
'SELECT * FROM _orez._zero_changes WHERE watermark > $1 ORDER BY watermark LIMIT $2',
|
|
212
215
|
[watermark, limit]
|
|
213
216
|
)
|
|
214
217
|
return result.rows
|
|
@@ -219,7 +222,7 @@ export async function purgeConsumedChanges(
|
|
|
219
222
|
watermark: number
|
|
220
223
|
): Promise<number> {
|
|
221
224
|
const result = await db.query<{ count: string }>(
|
|
222
|
-
'WITH deleted AS (DELETE FROM
|
|
225
|
+
'WITH deleted AS (DELETE FROM _orez._zero_changes WHERE watermark <= $1 RETURNING 1) SELECT count(*)::text AS count FROM deleted',
|
|
223
226
|
[watermark]
|
|
224
227
|
)
|
|
225
228
|
return Number(result.rows[0]?.count || 0)
|
|
@@ -227,7 +230,7 @@ export async function purgeConsumedChanges(
|
|
|
227
230
|
|
|
228
231
|
export async function getCurrentWatermark(db: PGlite): Promise<number> {
|
|
229
232
|
const result = await db.query<{ last_value: string; is_called: boolean }>(
|
|
230
|
-
'SELECT last_value, is_called FROM
|
|
233
|
+
'SELECT last_value, is_called FROM _orez._zero_watermark'
|
|
231
234
|
)
|
|
232
235
|
const { last_value, is_called } = result.rows[0]
|
|
233
236
|
if (!is_called) return 0
|
|
@@ -74,7 +74,7 @@ describe('handleReplicationQuery', () => {
|
|
|
74
74
|
expect(parsed!.values[3]).toBe('pgoutput')
|
|
75
75
|
|
|
76
76
|
const slots = await db.query<{ slot_name: string }>(
|
|
77
|
-
`SELECT slot_name FROM
|
|
77
|
+
`SELECT slot_name FROM _orez._zero_replication_slots WHERE slot_name = 'test_slot'`
|
|
78
78
|
)
|
|
79
79
|
expect(slots.rows).toHaveLength(1)
|
|
80
80
|
})
|
|
@@ -87,7 +87,7 @@ describe('handleReplicationQuery', () => {
|
|
|
87
87
|
await handleReplicationQuery('DROP_REPLICATION_SLOT "drop_me"', db)
|
|
88
88
|
|
|
89
89
|
const slots = await db.query<{ count: string }>(
|
|
90
|
-
`SELECT count(*) as count FROM
|
|
90
|
+
`SELECT count(*) as count FROM _orez._zero_replication_slots WHERE slot_name = 'drop_me'`
|
|
91
91
|
)
|
|
92
92
|
expect(Number(slots.rows[0].count)).toBe(0)
|
|
93
93
|
})
|
|
@@ -210,7 +210,7 @@ export async function handleReplicationQuery(
|
|
|
210
210
|
|
|
211
211
|
// persist slot so pg_replication_slots queries find it
|
|
212
212
|
await db.query(
|
|
213
|
-
`INSERT INTO
|
|
213
|
+
`INSERT INTO _orez._zero_replication_slots (slot_name, restart_lsn, confirmed_flush_lsn)
|
|
214
214
|
VALUES ($1, $2, $2)
|
|
215
215
|
ON CONFLICT (slot_name) DO UPDATE SET restart_lsn = $2, confirmed_flush_lsn = $2`,
|
|
216
216
|
[slotName, lsn]
|
|
@@ -226,7 +226,7 @@ export async function handleReplicationQuery(
|
|
|
226
226
|
const match = trimmed.match(/DROP_REPLICATION_SLOT\s+(?:"([^"]+)"|'([^']+)'|(\S+))/i)
|
|
227
227
|
const slotName = match?.[1] || match?.[2] || match?.[3]
|
|
228
228
|
if (slotName) {
|
|
229
|
-
await db.query(`DELETE FROM
|
|
229
|
+
await db.query(`DELETE FROM _orez._zero_replication_slots WHERE slot_name = $1`, [
|
|
230
230
|
slotName,
|
|
231
231
|
])
|
|
232
232
|
}
|
|
@@ -612,6 +612,17 @@ async function streamChanges(
|
|
|
612
612
|
}
|
|
613
613
|
}
|
|
614
614
|
|
|
615
|
+
// zero-cache expects specific camel-cased keys in shard clients rows.
|
|
616
|
+
// Some upstream paths can surface lower-cased variants; normalize them.
|
|
617
|
+
if (schema !== 'public' && tableName === 'clients') {
|
|
618
|
+
const sample = rowData || oldData
|
|
619
|
+
if (sample) {
|
|
620
|
+
log.debug.proxy(`shard clients keys: ${Object.keys(sample).join(', ')}`)
|
|
621
|
+
}
|
|
622
|
+
rowData = normalizeShardClientsRow(rowData)
|
|
623
|
+
oldData = normalizeShardClientsRow(oldData)
|
|
624
|
+
}
|
|
625
|
+
|
|
615
626
|
const row = rowData || oldData
|
|
616
627
|
if (!row) continue
|
|
617
628
|
|
|
@@ -658,4 +669,21 @@ async function streamChanges(
|
|
|
658
669
|
writer.write(wrapCopyData(commitMsg))
|
|
659
670
|
}
|
|
660
671
|
|
|
672
|
+
function normalizeShardClientsRow(
|
|
673
|
+
row: Record<string, unknown> | null
|
|
674
|
+
): Record<string, unknown> | null {
|
|
675
|
+
if (!row) return row
|
|
676
|
+
const out: Record<string, unknown> = { ...row }
|
|
677
|
+
if (out.clientGroupID === undefined && out.clientgroupid !== undefined) {
|
|
678
|
+
out.clientGroupID = out.clientgroupid
|
|
679
|
+
}
|
|
680
|
+
if (out.clientID === undefined && out.clientid !== undefined) {
|
|
681
|
+
out.clientID = out.clientid
|
|
682
|
+
}
|
|
683
|
+
if (out.lastMutationID === undefined && out.lastmutationid !== undefined) {
|
|
684
|
+
out.lastMutationID = out.lastmutationid
|
|
685
|
+
}
|
|
686
|
+
return out
|
|
687
|
+
}
|
|
688
|
+
|
|
661
689
|
export { buildErrorResponse }
|
package/src/sqlite-mode/index.ts
CHANGED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { dirname, resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { resolvePackage } from './resolve-mode.js'
|
|
5
|
+
|
|
6
|
+
const NATIVE_BINARY_RELATIVE_PATHS = ['build/Release/better_sqlite3.node']
|
|
7
|
+
|
|
8
|
+
export interface NativeBinaryCheckResult {
|
|
9
|
+
packageEntryPath: string
|
|
10
|
+
packageRoot: string
|
|
11
|
+
expectedPaths: string[]
|
|
12
|
+
existingPaths: string[]
|
|
13
|
+
found: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function findPackageRoot(entryPath: string): string {
|
|
17
|
+
if (!entryPath) return ''
|
|
18
|
+
|
|
19
|
+
let dir = dirname(entryPath)
|
|
20
|
+
for (let i = 0; i < 12; i++) {
|
|
21
|
+
const pkgJson = resolve(dir, 'package.json')
|
|
22
|
+
if (existsSync(pkgJson)) {
|
|
23
|
+
try {
|
|
24
|
+
const pkg = JSON.parse(readFileSync(pkgJson, 'utf-8'))
|
|
25
|
+
if (pkg?.name === '@rocicorp/zero-sqlite3') {
|
|
26
|
+
return dir
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
// ignore malformed package.json and continue searching upward
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const parent = dirname(dir)
|
|
34
|
+
if (parent === dir) break
|
|
35
|
+
dir = parent
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return ''
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function inspectNativeSqliteBinary(): NativeBinaryCheckResult {
|
|
42
|
+
const packageEntryPath = resolvePackage('@rocicorp/zero-sqlite3')
|
|
43
|
+
const packageRoot = findPackageRoot(packageEntryPath)
|
|
44
|
+
const expectedPaths = packageRoot
|
|
45
|
+
? NATIVE_BINARY_RELATIVE_PATHS.map((relativePath) =>
|
|
46
|
+
resolve(packageRoot, relativePath)
|
|
47
|
+
)
|
|
48
|
+
: []
|
|
49
|
+
const existingPaths = expectedPaths.filter((filePath) => existsSync(filePath))
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
packageEntryPath,
|
|
53
|
+
packageRoot,
|
|
54
|
+
expectedPaths,
|
|
55
|
+
existingPaths,
|
|
56
|
+
found: existingPaths.length > 0,
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function hasMissingNativeBinarySignature(message: string): boolean {
|
|
61
|
+
const text = message.toLowerCase()
|
|
62
|
+
return (
|
|
63
|
+
text.includes('better_sqlite3.node') ||
|
|
64
|
+
(text.includes('could not locate the bindings file') &&
|
|
65
|
+
text.includes('zero-sqlite3')) ||
|
|
66
|
+
(text.includes('err_dlopen_failed') && text.includes('better_sqlite3')) ||
|
|
67
|
+
(text.includes('no native build was found') && text.includes('sqlite'))
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function formatNativeBootstrapInstructions(
|
|
72
|
+
result: NativeBinaryCheckResult
|
|
73
|
+
): string {
|
|
74
|
+
const expectedList =
|
|
75
|
+
result.expectedPaths.length > 0
|
|
76
|
+
? result.expectedPaths.map((filePath) => ` - ${filePath}`).join('\n')
|
|
77
|
+
: ' - <unable to resolve @rocicorp/zero-sqlite3 package root>'
|
|
78
|
+
|
|
79
|
+
return [
|
|
80
|
+
'native sqlite binary is missing.',
|
|
81
|
+
`resolved package entry: ${result.packageEntryPath || '<not resolved>'}`,
|
|
82
|
+
'expected native binary path(s):',
|
|
83
|
+
expectedList,
|
|
84
|
+
'fix:',
|
|
85
|
+
' bun i @rocicorp/zero-sqlite3',
|
|
86
|
+
' bun run native:bootstrap',
|
|
87
|
+
'manual emergency fallback (not automated): copy a known-good better_sqlite3.node into build/Release and re-run native:bootstrap.',
|
|
88
|
+
].join('\n')
|
|
89
|
+
}
|