@happyvertical/sql 0.74.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/AGENT.md +33 -0
- package/LICENSE +7 -0
- package/README.md +287 -0
- package/dist/backup/index.d.ts +119 -0
- package/dist/backup/index.d.ts.map +1 -0
- package/dist/backup/index.js +212 -0
- package/dist/backup/index.js.map +1 -0
- package/dist/backup/manifest.d.ts +51 -0
- package/dist/backup/manifest.d.ts.map +1 -0
- package/dist/chunks/duckdb-Djx8WKZE.js +924 -0
- package/dist/chunks/duckdb-Djx8WKZE.js.map +1 -0
- package/dist/chunks/index-DL-5nB7l.js +3468 -0
- package/dist/chunks/index-DL-5nB7l.js.map +1 -0
- package/dist/chunks/postgres-cli-Cr6bBZ3q.js +209 -0
- package/dist/chunks/postgres-cli-Cr6bBZ3q.js.map +1 -0
- package/dist/chunks/sqlite-DkCGjXmu.js +937 -0
- package/dist/chunks/sqlite-DkCGjXmu.js.map +1 -0
- package/dist/chunks/sqlite-native-B6q-0nLs.js +1129 -0
- package/dist/chunks/sqlite-native-B6q-0nLs.js.map +1 -0
- package/dist/cli/claude-context.d.ts +3 -0
- package/dist/cli/claude-context.d.ts.map +1 -0
- package/dist/cli/claude-context.js +21 -0
- package/dist/cli/claude-context.js.map +1 -0
- package/dist/doctor.d.ts +69 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/duckdb.d.ts +9 -0
- package/dist/duckdb.d.ts.map +1 -0
- package/dist/index.d.ts +109 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/json.d.ts +30 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/postgres-cli.d.ts +135 -0
- package/dist/postgres-cli.d.ts.map +1 -0
- package/dist/postgres.d.ts +77 -0
- package/dist/postgres.d.ts.map +1 -0
- package/dist/schema-manager.d.ts +86 -0
- package/dist/schema-manager.d.ts.map +1 -0
- package/dist/shared/alter-utils.d.ts +83 -0
- package/dist/shared/alter-utils.d.ts.map +1 -0
- package/dist/shared/duckdb-schema-utils.d.ts +86 -0
- package/dist/shared/duckdb-schema-utils.d.ts.map +1 -0
- package/dist/shared/types.d.ts +1016 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/utils.d.ts +104 -0
- package/dist/shared/utils.d.ts.map +1 -0
- package/dist/sqlite-native.d.ts +4 -0
- package/dist/sqlite-native.d.ts.map +1 -0
- package/dist/sqlite.d.ts +54 -0
- package/dist/sqlite.d.ts.map +1 -0
- package/metadata.json +38 -0
- package/package.json +84 -0
package/AGENT.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# @happyvertical/sql
|
|
2
|
+
|
|
3
|
+
<!-- BEGIN AGENT:GENERATED -->
|
|
4
|
+
## Purpose
|
|
5
|
+
Database interface with support for SQLite, PostgreSQL, and DuckDB
|
|
6
|
+
|
|
7
|
+
## Package Map
|
|
8
|
+
- Package: `@happyvertical/sql`
|
|
9
|
+
- Hierarchy path: `@happyvertical/sdk > packages > sql`
|
|
10
|
+
- Workspace position: `26 of 30` local packages
|
|
11
|
+
- Internal dependencies: `@happyvertical/utils`
|
|
12
|
+
- Internal dependents: `@happyvertical/jobs`, `@happyvertical/secrets`
|
|
13
|
+
- Knowledge graph files: `AGENT.md`, `metadata.json`, `ecosystem-manifest.json`
|
|
14
|
+
|
|
15
|
+
## Build & Test
|
|
16
|
+
```bash
|
|
17
|
+
pnpm --filter @happyvertical/sql build
|
|
18
|
+
pnpm --filter @happyvertical/sql test
|
|
19
|
+
pnpm --filter @happyvertical/sql clean
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Agent Correction Loops
|
|
23
|
+
- If module resolution or export errors mention a workspace dependency, build the dependency first (`pnpm --filter @happyvertical/utils build`) and then rerun `pnpm --filter @happyvertical/sql build`.
|
|
24
|
+
- If tests or exports fail after API, type, or bundle changes, run `pnpm --filter @happyvertical/sql clean` followed by `pnpm --filter @happyvertical/sql build` and `pnpm --filter @happyvertical/sql test`.
|
|
25
|
+
- If failures span multiple packages or Turborepo ordering looks wrong, run `pnpm build` and `pnpm typecheck` from the repo root before retrying package-scoped commands.
|
|
26
|
+
|
|
27
|
+
## Ecosystem Relationships
|
|
28
|
+
- Provides: Database interface with support for SQLite, PostgreSQL, and DuckDB
|
|
29
|
+
- Implements: none
|
|
30
|
+
- Requires: @happyvertical/utils, @duckdb/node-api, @libsql/client, @russellthehippo/honker-node, @sqliteai/sqlite-vector, pg, sqlite-vss
|
|
31
|
+
- Stability: stable (Primary package surface is described as implemented and production-oriented.)
|
|
32
|
+
<!-- END AGENT:GENERATED -->
|
|
33
|
+
|
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright <2025> <Happy Vertical Corporation>
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# @happyvertical/sql
|
|
2
|
+
|
|
3
|
+
Database interface with support for SQLite (via LibSQL/Turso), PostgreSQL, DuckDB, and a JSON adapter (DuckDB-backed). Provides a unified API across all backends with template literal queries, CRUD helpers, transactions, schema synchronization, and vector search (PostgreSQL via pgvector).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @happyvertical/sql
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Connecting to a Database
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { getDatabase } from '@happyvertical/sql';
|
|
17
|
+
|
|
18
|
+
// SQLite (in-memory)
|
|
19
|
+
const db = await getDatabase({ type: 'sqlite', url: ':memory:' });
|
|
20
|
+
|
|
21
|
+
// SQLite (file)
|
|
22
|
+
const fileDb = await getDatabase({ type: 'sqlite', url: 'file:./app.db' });
|
|
23
|
+
|
|
24
|
+
// LibSQL/Turso (remote)
|
|
25
|
+
const tursoDb = await getDatabase({
|
|
26
|
+
type: 'sqlite',
|
|
27
|
+
url: 'libsql://your-database.turso.io',
|
|
28
|
+
authToken: process.env.TURSO_AUTH_TOKEN,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// PostgreSQL
|
|
32
|
+
const pgDb = await getDatabase({
|
|
33
|
+
type: 'postgres',
|
|
34
|
+
url: 'postgresql://user:pass@localhost:5432/dbname',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// DuckDB with JSON file auto-registration
|
|
38
|
+
const duckDb = await getDatabase({
|
|
39
|
+
type: 'duckdb',
|
|
40
|
+
url: ':memory:',
|
|
41
|
+
dataDir: './data',
|
|
42
|
+
autoRegisterJSON: true,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// JSON adapter (DuckDB-backed, reads/writes JSON files)
|
|
46
|
+
const jsonDb = await getDatabase({
|
|
47
|
+
type: 'json',
|
|
48
|
+
url: './data',
|
|
49
|
+
writeStrategy: 'immediate',
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Configuration is also loaded from `HAVE_SQL_*` environment variables (e.g. `HAVE_SQL_TYPE`, `HAVE_SQL_URL`). User-provided options take precedence.
|
|
54
|
+
|
|
55
|
+
### Template Literal Queries
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// Returns all rows
|
|
59
|
+
const posts = await db.many`SELECT * FROM posts WHERE published = ${true}`;
|
|
60
|
+
|
|
61
|
+
// Returns a single row or null
|
|
62
|
+
const post = await db.single`SELECT * FROM posts WHERE id = ${postId}`;
|
|
63
|
+
|
|
64
|
+
// Returns first column of first row
|
|
65
|
+
const count = await db.pluck`SELECT COUNT(*) FROM posts WHERE author = ${name}`;
|
|
66
|
+
|
|
67
|
+
// Executes without returning results
|
|
68
|
+
await db.execute`DELETE FROM posts WHERE id = ${postId}`;
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Shorthand aliases: `oo` (many), `oO` (single), `ox` (pluck), `xx` (execute).
|
|
72
|
+
|
|
73
|
+
Interpolated values are always passed as parameterized values (never string-concatenated), with placeholder format handled per adapter (`?` for SQLite/DuckDB, `$1`/`$2` for PostgreSQL).
|
|
74
|
+
|
|
75
|
+
### Raw Queries
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
// Raw queries use each adapter's native placeholder syntax.
|
|
79
|
+
await pgDb.query('SELECT * FROM posts WHERE id = $1', postId);
|
|
80
|
+
await pgDb.query('SELECT * FROM posts WHERE id = $1', [postId]);
|
|
81
|
+
await pgDb.query('SELECT * FROM posts WHERE id = ANY($1)', postIds);
|
|
82
|
+
|
|
83
|
+
// Legacy ? placeholders are converted only when unambiguous.
|
|
84
|
+
await pgDb.query('SELECT * FROM posts WHERE id = ?', postId);
|
|
85
|
+
|
|
86
|
+
// Native operators remain safe; prefer $1 placeholders when mixing operators and values.
|
|
87
|
+
await pgDb.query(`SELECT ('{"db":true}'::jsonb ? 'db') AS has_db`);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
For PostgreSQL, a single array argument is treated as a values list unless the SQL shows a single array-typed placeholder, such as `$1::text[]`, `CAST($1 AS text[])`, `ANY($1)`, or the equivalent legacy `?` placeholder form. Transaction handles follow the same raw query behavior as the root database handle.
|
|
91
|
+
|
|
92
|
+
### CRUD Helpers
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
await db.insert('posts', { id: 'p1', title: 'Hello', author: 'Alice' });
|
|
96
|
+
await db.insert('posts', [{ id: 'p2', title: 'A' }, { id: 'p3', title: 'B' }]);
|
|
97
|
+
|
|
98
|
+
const post = await db.get('posts', { id: 'p1' });
|
|
99
|
+
const recent = await db.list('posts', { author: 'Alice', 'created_at >': '2024-01-01' });
|
|
100
|
+
await db.update('posts', { id: 'p1' }, { title: 'Updated' });
|
|
101
|
+
await db.upsert('posts', ['id'], { id: 'p1', title: 'Upserted' });
|
|
102
|
+
await db.delete('posts', { id: 'p1' });
|
|
103
|
+
const total = await db.count('posts');
|
|
104
|
+
const filtered = await db.count('posts', { published: true });
|
|
105
|
+
|
|
106
|
+
const user = await db.getOrInsert('users', { email: 'a@b.com' }, { id: 'u1', email: 'a@b.com', name: 'A' });
|
|
107
|
+
|
|
108
|
+
// Table-scoped helper
|
|
109
|
+
const postsTable = db.table('posts');
|
|
110
|
+
await postsTable.insert({ id: 'p4', title: 'Scoped' });
|
|
111
|
+
const p = await postsTable.get({ id: 'p4' });
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`upsert()` treats `NULL` values in conflict columns as matching existing `NULL`
|
|
115
|
+
values so nullable composite keys update the existing row instead of inserting a
|
|
116
|
+
duplicate. Pass `{ nullsDistinct: true }` as the fourth argument to preserve the
|
|
117
|
+
database-native behavior where `NULL` conflict values are distinct.
|
|
118
|
+
|
|
119
|
+
### Transactions
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
// Callback-based (auto commit/rollback)
|
|
123
|
+
await db.transaction(async (tx) => {
|
|
124
|
+
await tx.insert('users', { id: 'u1', name: 'Alice' });
|
|
125
|
+
await tx.insert('profiles', { user_id: 'u1', bio: 'Dev' });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Manual control via beginTransaction()
|
|
129
|
+
const tx = await db.beginTransaction();
|
|
130
|
+
try {
|
|
131
|
+
await tx.insert('orders', { id: 'o1', total: 100 });
|
|
132
|
+
await tx.commit();
|
|
133
|
+
} catch (e) {
|
|
134
|
+
await tx.rollback();
|
|
135
|
+
throw e;
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### WHERE Clause Building
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import { buildWhere } from '@happyvertical/sql';
|
|
143
|
+
|
|
144
|
+
const { sql, values } = buildWhere({
|
|
145
|
+
status: 'active',
|
|
146
|
+
'price >': 100,
|
|
147
|
+
'category in': ['electronics', 'books'],
|
|
148
|
+
'name like': '%shirt%',
|
|
149
|
+
deleted_at: null, // IS NULL
|
|
150
|
+
'updated_at !=': null, // IS NOT NULL
|
|
151
|
+
});
|
|
152
|
+
// Use with raw query: db.query(`SELECT * FROM products ${sql}`, values)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Supports 2D array format for OR/AND compound logic:
|
|
156
|
+
```typescript
|
|
157
|
+
buildWhere([
|
|
158
|
+
[{ status: 'active' }, { 'price >': 100 }],
|
|
159
|
+
[{ status: 'pending' }, { priority: 'high' }],
|
|
160
|
+
]);
|
|
161
|
+
// WHERE (status = $1 AND price > $2) OR (status = $3 AND priority = $4)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Schema Synchronization
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
import { syncSchema } from '@happyvertical/sql';
|
|
168
|
+
|
|
169
|
+
await syncSchema({
|
|
170
|
+
db,
|
|
171
|
+
schema: `
|
|
172
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
173
|
+
id TEXT PRIMARY KEY,
|
|
174
|
+
name TEXT NOT NULL,
|
|
175
|
+
email TEXT UNIQUE
|
|
176
|
+
);
|
|
177
|
+
`,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const exists = await db.tableExists('users');
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Vector Search (PostgreSQL)
|
|
184
|
+
|
|
185
|
+
PostgreSQL adapters expose `db.vector` when pgvector is available:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
await db.vector.ensureColumn('documents', 'embedding', 1536);
|
|
189
|
+
await db.vector.ensureIndex('documents', 'embedding', { metric: 'cosine' });
|
|
190
|
+
await db.vector.upsertVector('documents', { id: 'doc-1' }, 'embedding', vector);
|
|
191
|
+
const results = await db.vector.search('documents', 'embedding', queryVector, { limit: 10 });
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Optional SQLite Capabilities
|
|
195
|
+
|
|
196
|
+
SQLite keeps the existing LibSQL adapter path by default. For local development
|
|
197
|
+
and tests, opt into native SQLite capabilities with `capabilities`. This switches
|
|
198
|
+
the adapter to Node's built-in `node:sqlite` for local files or `:memory:` and
|
|
199
|
+
rejects remote `libsql://`, `http://`, and `https://` URLs.
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
pnpm add -D @sqliteai/sqlite-vector @russellthehippo/honker-node
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
type SqliteCapabilitiesOptions = {
|
|
207
|
+
notifications?: boolean | {
|
|
208
|
+
watcherBackend?: 'polling' | 'kernel' | 'shm';
|
|
209
|
+
maxReaders?: number;
|
|
210
|
+
};
|
|
211
|
+
vector?: boolean | {
|
|
212
|
+
preload?: boolean;
|
|
213
|
+
quantization?: 'turbo4' | 'turbo3' | 'turbo2' | 'uint8' | 'int8' | '1bit';
|
|
214
|
+
maxMemory?: string;
|
|
215
|
+
};
|
|
216
|
+
};
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
const db = await getDatabase({
|
|
221
|
+
type: 'sqlite',
|
|
222
|
+
url: 'file:./dev.db',
|
|
223
|
+
capabilities: {
|
|
224
|
+
vector: { quantization: 'turbo4', preload: true },
|
|
225
|
+
notifications: { watcherBackend: 'polling' },
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
await db.vector?.ensureColumn('documents', 'embedding', 1536);
|
|
230
|
+
await db.vector?.upsertVector('documents', { id: 'doc-1' }, 'embedding', vector);
|
|
231
|
+
const matches = await db.vector?.search('documents', 'embedding', queryVector, {
|
|
232
|
+
limit: 10,
|
|
233
|
+
metric: 'cosine',
|
|
234
|
+
where: 'status = $2',
|
|
235
|
+
params: ['published'],
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const listener = db.notifications!.listen('jobs');
|
|
239
|
+
await db.notifications!.notify('jobs', { id: 'job-1' });
|
|
240
|
+
for await (const message of listener) {
|
|
241
|
+
console.log(message.channel, message.payload);
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
await db.notifications!.waitForUpdate({ timeoutMs: 5000 });
|
|
245
|
+
await db.close?.();
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
`@sqliteai/sqlite-vector` is loaded lazily through `getExtensionPath()` and only
|
|
249
|
+
mutates schema when `ensureColumn()` or `ensureIndex()` is called. SQLite vector
|
|
250
|
+
search uses the same `db.vector` API as PostgreSQL. `ensureIndex()` creates a
|
|
251
|
+
quantized sqlite-vector index with `turbo4` by default, and filtered searches can
|
|
252
|
+
keep PostgreSQL-style `$2`, `$3`, etc. placeholders in `VectorSearchOptions.where`.
|
|
253
|
+
|
|
254
|
+
`@russellthehippo/honker-node` is loaded lazily as a sidecar connection to the
|
|
255
|
+
same file. Honker bootstraps its `_honker_*` tables on open and requires a
|
|
256
|
+
file-backed database, so `:memory:` is rejected when notifications are enabled.
|
|
257
|
+
When notifications are enabled, `db.notifications` exposes `notify()`,
|
|
258
|
+
`listen()`, `waitForUpdate()`, and `prune()`; call `db.close?.()` when a test or
|
|
259
|
+
worker is done so watcher handles and sidecar connections are released.
|
|
260
|
+
|
|
261
|
+
Both packages are optional peers. `sqlite-vector` uses a custom license declared
|
|
262
|
+
as `SEE LICENSE IN LICENSE.md`; keep it opt-in and review the upstream
|
|
263
|
+
[license](https://github.com/sqliteai/sqlite-vector/blob/main/LICENSE.md) before
|
|
264
|
+
shipping it beyond development or test environments.
|
|
265
|
+
|
|
266
|
+
## Adapters
|
|
267
|
+
|
|
268
|
+
| Adapter | `type` | Backend | Notes |
|
|
269
|
+
|---------|--------|---------|-------|
|
|
270
|
+
| SQLite | `'sqlite'` | LibSQL (`@libsql/client`) by default; native `node:sqlite` when capabilities are enabled | Supports `:memory:`, file, and remote Turso URLs by default. Native capabilities are local-only |
|
|
271
|
+
| PostgreSQL | `'postgres'` | `pg` Pool | Connection pooling, pgvector support |
|
|
272
|
+
| DuckDB | `'duckdb'` | `@duckdb/node-api` | JSON file auto-registration, write-back strategies |
|
|
273
|
+
| JSON | `'json'` | DuckDB in-memory | Queries JSON files as tables, connection caching |
|
|
274
|
+
|
|
275
|
+
## API Overview
|
|
276
|
+
|
|
277
|
+
**Factory**: `getDatabase(options)` — creates or returns a cached database connection.
|
|
278
|
+
|
|
279
|
+
**Interface** (`DatabaseInterface`): `many`, `single`, `pluck`, `execute`, `query`, `insert`, `get`, `list`, `update`, `upsert`, `getOrInsert`, `delete`, `count`, `table`, `tableExists`, `syncSchema`, `transaction`, `beginTransaction`, `vector`, `notifications`, `close`.
|
|
280
|
+
|
|
281
|
+
**Utilities**: `buildWhere`, `syncSchema`, `tableExists`, `escapeSqlValue`, `validateColumnName`, `formatDbError`, `convertUniqueIndexesToInlineConstraints`.
|
|
282
|
+
|
|
283
|
+
**Schema**: `DatabaseSchemaManager` for JSON manifest-based schema initialization with dependency resolution.
|
|
284
|
+
|
|
285
|
+
## License
|
|
286
|
+
|
|
287
|
+
ISC
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { BackupManifest } from './manifest';
|
|
2
|
+
export { BACKUP_KIND, BACKUP_VERSION, type BackupManifest, type BackupManifestDatabase, type BackupManifestFiles, DEFAULT_DUMP_FILE, DEFAULT_FILES_DIR, DEFAULT_MANIFEST_FILE, readBackupManifest, timestampForBackup, writeBackupManifest, } from './manifest';
|
|
3
|
+
export interface ExportBackupContext {
|
|
4
|
+
backupPath: string;
|
|
5
|
+
/**
|
|
6
|
+
* The raw, credential-bearing database URL the caller passed to
|
|
7
|
+
* `exportBackup`. The hook needs the raw form to open its own
|
|
8
|
+
* connection, but **do not log this value verbatim** — it contains
|
|
9
|
+
* credentials. Run it through `redactDatabaseUrl` (exported from
|
|
10
|
+
* `@happyvertical/sql`) before including it in any output. The
|
|
11
|
+
* manifest's `database.url` field is redacted; only `databaseUrl`
|
|
12
|
+
* here is raw.
|
|
13
|
+
*/
|
|
14
|
+
databaseUrl: string;
|
|
15
|
+
/**
|
|
16
|
+
* Local directory the caller should copy file storage into.
|
|
17
|
+
* Already exists by the time `onBackup` runs.
|
|
18
|
+
*/
|
|
19
|
+
filesDir: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ExportBackupResult<Extra = unknown> {
|
|
22
|
+
/** Absolute path to the backup directory. */
|
|
23
|
+
backupPath: string;
|
|
24
|
+
/** The manifest that was written. */
|
|
25
|
+
manifest: BackupManifest & {
|
|
26
|
+
extra?: Extra;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export interface ExportBackupOptions<Extra = unknown> {
|
|
30
|
+
databaseUrl: string;
|
|
31
|
+
/** Override the parent dir for the timestamped backup. Defaults to ~/.local/share/happyvertical/backups. */
|
|
32
|
+
backupRoot?: string;
|
|
33
|
+
/** Prefix the timestamp with this label (e.g. 'prod', 'pre-migration'). Defaults to 'backup'. */
|
|
34
|
+
label?: string;
|
|
35
|
+
/** Allow exporting from a non-local DB without --allow-production. */
|
|
36
|
+
allowProduction?: boolean;
|
|
37
|
+
/** Allow exporting from a local DB even though `prod: true` is set. */
|
|
38
|
+
allowLocal?: boolean;
|
|
39
|
+
/** Caller is running a prod-flavored export. */
|
|
40
|
+
prod?: boolean;
|
|
41
|
+
/** Skip the file-storage half entirely (no `onBackup` invoked). */
|
|
42
|
+
skipFiles?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Caller-supplied hook for backing up file storage. Receives the
|
|
45
|
+
* already-created backup/files directory; whatever it returns is
|
|
46
|
+
* recorded as `extra` on the manifest.
|
|
47
|
+
*/
|
|
48
|
+
onBackup?: (ctx: ExportBackupContext) => Promise<Extra | void>;
|
|
49
|
+
/**
|
|
50
|
+
* Caller-supplied hook for filling the database half of the manifest
|
|
51
|
+
* with diagnostic metadata (row counts, schema count, etc.) — runs
|
|
52
|
+
* after the dump completes. As with `onBackup`, the `databaseUrl`
|
|
53
|
+
* passed in is the raw credential-bearing form — redact via
|
|
54
|
+
* `redactDatabaseUrl` before logging.
|
|
55
|
+
*/
|
|
56
|
+
onDatabaseMetadata?: (ctx: {
|
|
57
|
+
/** Raw URL — see warning on {@link ExportBackupContext.databaseUrl}. */
|
|
58
|
+
databaseUrl: string;
|
|
59
|
+
}) => Promise<Record<string, unknown>>;
|
|
60
|
+
/** Recorded as `files.storageConfig` on the manifest after redaction by the caller. */
|
|
61
|
+
filesStorageConfig?: unknown;
|
|
62
|
+
/** Working dir used when discovering the git sha. Defaults to process.cwd(). */
|
|
63
|
+
gitCwd?: string;
|
|
64
|
+
}
|
|
65
|
+
export interface RestoreBackupContext {
|
|
66
|
+
backupPath: string;
|
|
67
|
+
/** Raw URL — see warning on {@link ExportBackupContext.databaseUrl}. */
|
|
68
|
+
databaseUrl: string;
|
|
69
|
+
filesDir: string;
|
|
70
|
+
manifest: BackupManifest;
|
|
71
|
+
}
|
|
72
|
+
export interface RestoreBackupOptions {
|
|
73
|
+
databaseUrl: string;
|
|
74
|
+
backupPath: string;
|
|
75
|
+
allowProduction?: boolean;
|
|
76
|
+
/** Skip the file-storage half (no `onRestore` invoked). */
|
|
77
|
+
skipFiles?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Caller-supplied hook for restoring file storage from
|
|
80
|
+
* `backupPath/<files.directory>`. Runs after the pg_restore completes.
|
|
81
|
+
*/
|
|
82
|
+
onRestore?: (ctx: RestoreBackupContext) => Promise<void>;
|
|
83
|
+
}
|
|
84
|
+
export interface ResetLocalOptions extends Omit<RestoreBackupOptions, 'allowProduction'> {
|
|
85
|
+
}
|
|
86
|
+
/** Default root for backups. Override via $HAPPYVERTICAL_BACKUP_DIR or `backupRoot`. */
|
|
87
|
+
export declare function defaultBackupRoot(): string;
|
|
88
|
+
/**
|
|
89
|
+
* Take a postgres dump and (optionally) a snapshot of file storage,
|
|
90
|
+
* writing both plus a manifest into a fresh timestamped directory.
|
|
91
|
+
*
|
|
92
|
+
* Returns the directory path and the manifest. The manifest's `extra`
|
|
93
|
+
* field carries whatever `onBackup` returned.
|
|
94
|
+
*
|
|
95
|
+
* **Failure model:** the function is all-or-nothing. If any step after
|
|
96
|
+
* the initial directory creation throws — `dumpPostgresDatabase`,
|
|
97
|
+
* `onBackup`, `onDatabaseMetadata`, `getGitSha`, or the final manifest
|
|
98
|
+
* write — the partial backup directory is removed (best-effort)
|
|
99
|
+
* before the original error is re-thrown. Callers therefore never see
|
|
100
|
+
* a backup directory on disk without a complete manifest, which would
|
|
101
|
+
* otherwise poison later `restoreBackup` calls with confusing
|
|
102
|
+
* "no manifest" errors and leak a partial dump containing sensitive
|
|
103
|
+
* data. If cleanup itself fails, that error is logged via
|
|
104
|
+
* `console.warn` and swallowed so it doesn't mask the original.
|
|
105
|
+
*/
|
|
106
|
+
export declare function exportBackup<Extra = unknown>(options: ExportBackupOptions<Extra>): Promise<ExportBackupResult<Extra>>;
|
|
107
|
+
/**
|
|
108
|
+
* Restore a backup directory into `databaseUrl` and, if `onRestore` is
|
|
109
|
+
* provided, run the file-storage half. The caller controls everything
|
|
110
|
+
* after the dump completes (file copy, migrations, doctor) via the hook.
|
|
111
|
+
*/
|
|
112
|
+
export declare function restoreBackup(options: RestoreBackupOptions): Promise<BackupManifest>;
|
|
113
|
+
/**
|
|
114
|
+
* Drop-and-recreate the target database, then run `restoreBackup`.
|
|
115
|
+
* Local-only: explicitly refuses non-local URLs without an escape hatch
|
|
116
|
+
* (this is a destructive operation by design).
|
|
117
|
+
*/
|
|
118
|
+
export declare function resetLocalDatabaseFromBackup(options: ResetLocalOptions): Promise<BackupManifest>;
|
|
119
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/backup/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { readFile, writeFile, rename, mkdir, rm, access } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { a as assertCanExportDatabase, f as dumpPostgresDatabase, r as redactDatabaseUrl, b as assertCanImportDatabase, e as dropPostgresDatabase, c as createPostgresDatabase, g as restorePostgresDatabase } from "../chunks/postgres-cli-Cr6bBZ3q.js";
|
|
6
|
+
const BACKUP_KIND = "happyvertical-backup";
|
|
7
|
+
const BACKUP_VERSION = 1;
|
|
8
|
+
const DEFAULT_DUMP_FILE = "database.dump";
|
|
9
|
+
const DEFAULT_FILES_DIR = "files";
|
|
10
|
+
const DEFAULT_MANIFEST_FILE = "manifest.json";
|
|
11
|
+
async function readBackupManifest(backupPath) {
|
|
12
|
+
const manifestPath = join(resolve(backupPath), DEFAULT_MANIFEST_FILE);
|
|
13
|
+
const manifest = JSON.parse(
|
|
14
|
+
await readFile(manifestPath, "utf8")
|
|
15
|
+
);
|
|
16
|
+
if (manifest.kind !== BACKUP_KIND || manifest.version !== BACKUP_VERSION) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`Unsupported backup manifest at ${manifestPath} (kind=${manifest.kind}, version=${manifest.version}).`
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
return manifest;
|
|
22
|
+
}
|
|
23
|
+
async function writeBackupManifest(backupPath, manifest) {
|
|
24
|
+
const targetPath = join(backupPath, DEFAULT_MANIFEST_FILE);
|
|
25
|
+
const tempPath = join(
|
|
26
|
+
backupPath,
|
|
27
|
+
`.manifest.${process.pid}.${Date.now().toString(36)}.tmp`
|
|
28
|
+
);
|
|
29
|
+
await writeFile(tempPath, `${JSON.stringify(manifest, null, 2)}
|
|
30
|
+
`, {
|
|
31
|
+
mode: 384
|
|
32
|
+
});
|
|
33
|
+
await rename(tempPath, targetPath);
|
|
34
|
+
}
|
|
35
|
+
function timestampForBackup(date = /* @__PURE__ */ new Date()) {
|
|
36
|
+
return date.toISOString().replace(/[:.]/g, "-");
|
|
37
|
+
}
|
|
38
|
+
function defaultBackupRoot() {
|
|
39
|
+
return process.env.HAPPYVERTICAL_BACKUP_DIR ?? join(homedir(), ".local", "share", "happyvertical", "backups");
|
|
40
|
+
}
|
|
41
|
+
async function exportBackup(options) {
|
|
42
|
+
assertCanExportDatabase(options.databaseUrl, options);
|
|
43
|
+
const label = validateLabel(options.label ?? "backup");
|
|
44
|
+
const backupPath = await createUniqueBackupDir(
|
|
45
|
+
options.backupRoot ?? defaultBackupRoot(),
|
|
46
|
+
label
|
|
47
|
+
);
|
|
48
|
+
try {
|
|
49
|
+
const dumpPath = join(backupPath, DEFAULT_DUMP_FILE);
|
|
50
|
+
await dumpPostgresDatabase(options.databaseUrl, dumpPath);
|
|
51
|
+
const filesDir = join(backupPath, DEFAULT_FILES_DIR);
|
|
52
|
+
let filesInfo = {
|
|
53
|
+
directory: DEFAULT_FILES_DIR,
|
|
54
|
+
exported: false,
|
|
55
|
+
count: 0,
|
|
56
|
+
bytes: 0,
|
|
57
|
+
reason: "Skipped by skipFiles.",
|
|
58
|
+
storageConfig: options.filesStorageConfig
|
|
59
|
+
};
|
|
60
|
+
let extra;
|
|
61
|
+
if (!options.skipFiles && options.onBackup) {
|
|
62
|
+
await mkdir(filesDir, { recursive: true, mode: 448 });
|
|
63
|
+
const result = await options.onBackup({
|
|
64
|
+
backupPath,
|
|
65
|
+
databaseUrl: options.databaseUrl,
|
|
66
|
+
filesDir
|
|
67
|
+
});
|
|
68
|
+
if (result !== void 0 && result !== null) extra = result;
|
|
69
|
+
filesInfo = {
|
|
70
|
+
directory: DEFAULT_FILES_DIR,
|
|
71
|
+
exported: true,
|
|
72
|
+
count: 0,
|
|
73
|
+
bytes: 0,
|
|
74
|
+
storageConfig: options.filesStorageConfig
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const metadata = options.onDatabaseMetadata ? await options.onDatabaseMetadata({ databaseUrl: options.databaseUrl }) : void 0;
|
|
78
|
+
const manifest = {
|
|
79
|
+
kind: BACKUP_KIND,
|
|
80
|
+
version: BACKUP_VERSION,
|
|
81
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
82
|
+
gitSha: await getGitSha(options.gitCwd),
|
|
83
|
+
source: label,
|
|
84
|
+
database: {
|
|
85
|
+
dumpFile: DEFAULT_DUMP_FILE,
|
|
86
|
+
url: redactDatabaseUrl(options.databaseUrl),
|
|
87
|
+
metadata
|
|
88
|
+
},
|
|
89
|
+
files: filesInfo,
|
|
90
|
+
extra
|
|
91
|
+
};
|
|
92
|
+
await writeBackupManifest(backupPath, manifest);
|
|
93
|
+
return { backupPath, manifest };
|
|
94
|
+
} catch (error) {
|
|
95
|
+
try {
|
|
96
|
+
await rm(backupPath, { recursive: true, force: true });
|
|
97
|
+
} catch (cleanupError) {
|
|
98
|
+
console.warn(
|
|
99
|
+
`exportBackup: failed to clean up partial backup at ${backupPath}:`,
|
|
100
|
+
cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function restoreBackup(options) {
|
|
107
|
+
const backupPath = resolve(options.backupPath);
|
|
108
|
+
const manifest = await readBackupManifest(backupPath);
|
|
109
|
+
assertCanImportDatabase(options.databaseUrl, options);
|
|
110
|
+
const dumpPath = join(
|
|
111
|
+
backupPath,
|
|
112
|
+
validateManifestPathSegment(
|
|
113
|
+
manifest.database.dumpFile,
|
|
114
|
+
"database dump file"
|
|
115
|
+
)
|
|
116
|
+
);
|
|
117
|
+
const filesDir = join(
|
|
118
|
+
backupPath,
|
|
119
|
+
validateManifestPathSegment(manifest.files.directory, "files directory")
|
|
120
|
+
);
|
|
121
|
+
await restorePostgresDatabase(options.databaseUrl, dumpPath);
|
|
122
|
+
if (!options.skipFiles && options.onRestore && manifest.files.exported) {
|
|
123
|
+
await options.onRestore({
|
|
124
|
+
backupPath,
|
|
125
|
+
databaseUrl: options.databaseUrl,
|
|
126
|
+
filesDir,
|
|
127
|
+
manifest
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return manifest;
|
|
131
|
+
}
|
|
132
|
+
async function resetLocalDatabaseFromBackup(options) {
|
|
133
|
+
assertCanImportDatabase(options.databaseUrl, { allowProduction: false });
|
|
134
|
+
const backupPath = resolve(options.backupPath);
|
|
135
|
+
const manifest = await readBackupManifest(backupPath);
|
|
136
|
+
const dumpFile = validateManifestPathSegment(
|
|
137
|
+
manifest.database.dumpFile,
|
|
138
|
+
"database dump file"
|
|
139
|
+
);
|
|
140
|
+
validateManifestPathSegment(manifest.files.directory, "files directory");
|
|
141
|
+
await access(join(backupPath, dumpFile));
|
|
142
|
+
await dropPostgresDatabase(options.databaseUrl);
|
|
143
|
+
await createPostgresDatabase(options.databaseUrl);
|
|
144
|
+
return restoreBackup({ ...options, allowProduction: false });
|
|
145
|
+
}
|
|
146
|
+
async function getGitSha(cwd = process.cwd()) {
|
|
147
|
+
try {
|
|
148
|
+
return await new Promise((resolvePromise, reject) => {
|
|
149
|
+
execFile("git", ["rev-parse", "HEAD"], { cwd }, (error, stdout) => {
|
|
150
|
+
if (error) {
|
|
151
|
+
reject(error);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
resolvePromise(stdout.trim());
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
} catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function validateLabel(label) {
|
|
162
|
+
if (!label || label === "." || label === ".." || /[\\/]/u.test(label)) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`Backup label "${label}" is invalid. Labels must be a single path-safe segment.`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return label;
|
|
168
|
+
}
|
|
169
|
+
function validateManifestPathSegment(value, field) {
|
|
170
|
+
if (!value || value === "." || value === ".." || /[\\/]/u.test(value)) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`Backup manifest ${field} is invalid: "${value}". It must be a single path segment.`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
return value;
|
|
176
|
+
}
|
|
177
|
+
async function createUniqueBackupDir(backupRoot, label) {
|
|
178
|
+
const root = resolve(backupRoot);
|
|
179
|
+
await mkdir(root, { recursive: true, mode: 448 });
|
|
180
|
+
const baseName = `${label}-${timestampForBackup()}`;
|
|
181
|
+
for (let attempt = 0; attempt < 64; attempt += 1) {
|
|
182
|
+
const suffix = attempt === 0 ? "" : `-${attempt.toString(36)}`;
|
|
183
|
+
const candidate = join(root, `${baseName}${suffix}`);
|
|
184
|
+
try {
|
|
185
|
+
await mkdir(candidate, { mode: 448 });
|
|
186
|
+
return candidate;
|
|
187
|
+
} catch (error) {
|
|
188
|
+
if (error.code === "EEXIST") {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
throw new Error(
|
|
195
|
+
`Failed to create a unique backup directory under ${root} for label "${label}".`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
export {
|
|
199
|
+
BACKUP_KIND,
|
|
200
|
+
BACKUP_VERSION,
|
|
201
|
+
DEFAULT_DUMP_FILE,
|
|
202
|
+
DEFAULT_FILES_DIR,
|
|
203
|
+
DEFAULT_MANIFEST_FILE,
|
|
204
|
+
defaultBackupRoot,
|
|
205
|
+
exportBackup,
|
|
206
|
+
readBackupManifest,
|
|
207
|
+
resetLocalDatabaseFromBackup,
|
|
208
|
+
restoreBackup,
|
|
209
|
+
timestampForBackup,
|
|
210
|
+
writeBackupManifest
|
|
211
|
+
};
|
|
212
|
+
//# sourceMappingURL=index.js.map
|