@cloud-cli/d0 1.8.1 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/client.mjs +11 -1
- package/dist/index.js +79 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,6 +15,13 @@ console.log(schema.tables, schema.statements);
|
|
|
15
15
|
|
|
16
16
|
**GET /api** returns an OpenAPI 3.1 description of the HTTP API.
|
|
17
17
|
|
|
18
|
+
**POST /clone** creates a copy of the selected database. The target name must contain only letters, numbers, and hyphens. An existing target returns `409` until overwrite is explicitly confirmed.
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
await db.clone('test-copy');
|
|
22
|
+
await db.clone('test-copy', true); // overwrite an existing copy
|
|
23
|
+
```
|
|
24
|
+
|
|
18
25
|
The web console uses an explicit method selector instead of guessing from SQL text. Use `all` or `get` for reads, `run` for one prepared statement, `exec` for DDL or multiple statements, and `transaction` to run the entered SQL atomically.
|
|
19
26
|
|
|
20
27
|
**POST /query**
|
package/client.mjs
CHANGED
|
@@ -30,6 +30,16 @@ export async function schema({ internal = false } = {}) {
|
|
|
30
30
|
throw new Error(await req.text());
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
export async function clone(name, overwrite = false) {
|
|
34
|
+
const req = await fetch(new URL('/clone', baseURL), {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
body: JSON.stringify({ name, overwrite }),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (req.ok) return req.json();
|
|
40
|
+
throw new Error(await req.text());
|
|
41
|
+
}
|
|
42
|
+
|
|
33
43
|
export async function transaction(statements, pragma = pragmas) {
|
|
34
44
|
return query('transaction', undefined, undefined, pragma, statements);
|
|
35
45
|
}
|
|
@@ -45,4 +55,4 @@ export function pragma(p) {
|
|
|
45
55
|
}
|
|
46
56
|
}
|
|
47
57
|
|
|
48
|
-
export default { query, get, run, all, exec, transaction, schema, pragma };
|
|
58
|
+
export default { query, get, run, all, exec, transaction, schema, clone, pragma };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
|
-
import { mkdirSync } from 'node:fs';
|
|
2
|
+
import { existsSync, mkdirSync, rmSync } from 'node:fs';
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
4
|
import SQLite from 'better-sqlite3';
|
|
5
5
|
import { join } from 'node:path';
|
|
@@ -12,6 +12,7 @@ const maxDatabases = Math.max(1, Number.parseInt(process.env.MAX_DATABASES || '3
|
|
|
12
12
|
const maxBodyBytes = Math.max(1, Number.parseInt(process.env.MAX_BODY_BYTES || '1048576', 10) || 1048576);
|
|
13
13
|
const slowQueryMs = Math.max(0, Number.parseInt(process.env.SLOW_QUERY_MS || '1000', 10) || 1000);
|
|
14
14
|
const databases = new Map();
|
|
15
|
+
const cloneLocks = new Set();
|
|
15
16
|
mkdirSync(dataPath, { recursive: true });
|
|
16
17
|
export function getDatabase(file) {
|
|
17
18
|
const fullPath = join(dataPath, file);
|
|
@@ -89,6 +90,8 @@ export async function handleRequest(request, response, db) {
|
|
|
89
90
|
return onSchema(response, db, url.searchParams.get('internal') === '1');
|
|
90
91
|
case 'POST /query':
|
|
91
92
|
return onQuery(request, response, db);
|
|
93
|
+
case 'POST /clone':
|
|
94
|
+
return onClone(request, response, db);
|
|
92
95
|
default:
|
|
93
96
|
response.writeHead(404).end();
|
|
94
97
|
}
|
|
@@ -136,6 +139,21 @@ function onApi(request, response) {
|
|
|
136
139
|
},
|
|
137
140
|
},
|
|
138
141
|
'/index.mjs': { get: { summary: 'Get the consumer ES module', responses: { '200': { description: 'JavaScript module.' } } } },
|
|
142
|
+
'/clone': {
|
|
143
|
+
post: {
|
|
144
|
+
summary: 'Clone the selected database',
|
|
145
|
+
requestBody: {
|
|
146
|
+
required: true,
|
|
147
|
+
content: { 'application/json': { schema: { $ref: '#/components/schemas/CloneRequest' } } },
|
|
148
|
+
},
|
|
149
|
+
responses: {
|
|
150
|
+
'201': { description: 'Database cloned.' },
|
|
151
|
+
'400': { description: 'Invalid database name.' },
|
|
152
|
+
'409': { description: 'Target exists and overwrite was not confirmed.' },
|
|
153
|
+
'503': { description: 'Another clone is already running for the target.' },
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
139
157
|
},
|
|
140
158
|
components: {
|
|
141
159
|
schemas: {
|
|
@@ -159,11 +177,71 @@ function onApi(request, response) {
|
|
|
159
177
|
m: { type: 'string', enum: ['all', 'get', 'run', 'exec'], default: 'run' },
|
|
160
178
|
},
|
|
161
179
|
},
|
|
180
|
+
CloneRequest: {
|
|
181
|
+
type: 'object',
|
|
182
|
+
required: ['name'],
|
|
183
|
+
properties: {
|
|
184
|
+
name: { type: 'string', pattern: '^[a-zA-Z0-9][a-zA-Z0-9-]*$', description: 'Name of the cloned database.' },
|
|
185
|
+
overwrite: { type: 'boolean', default: false, description: 'Replace the target if it already exists.' },
|
|
186
|
+
},
|
|
187
|
+
},
|
|
162
188
|
},
|
|
163
189
|
},
|
|
164
190
|
};
|
|
165
191
|
sendJson(response, 200, document);
|
|
166
192
|
}
|
|
193
|
+
async function onClone(request, response, source) {
|
|
194
|
+
const body = await readBody(request);
|
|
195
|
+
if (!body) {
|
|
196
|
+
sendError(response, 413, new Error('Request body too large.'));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
const { name: requestedName, overwrite = false } = JSON.parse(body.toString('utf-8'));
|
|
201
|
+
const name = String(requestedName || '').replace(/\.sqlite3$/i, '');
|
|
202
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9-]*$/.test(name)) {
|
|
203
|
+
sendError(response, 400, new Error('Invalid database name.'));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const target = join(dataPath, `${name}.sqlite3`);
|
|
207
|
+
const sourcePath = join(dataPath, source);
|
|
208
|
+
if (target === sourcePath) {
|
|
209
|
+
sendError(response, 400, new Error('The clone must have a different name.'));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (cloneLocks.has(target)) {
|
|
213
|
+
sendError(response, 503, new Error('A clone is already running for this database.'));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const exists = existsSync(target);
|
|
217
|
+
if (exists && overwrite !== true) {
|
|
218
|
+
sendJson(response, 409, { exists: true, name, requiresOverwrite: true });
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
cloneLocks.add(target);
|
|
222
|
+
try {
|
|
223
|
+
const targetDatabase = databases.get(target);
|
|
224
|
+
if (targetDatabase) {
|
|
225
|
+
targetDatabase.close();
|
|
226
|
+
databases.delete(target);
|
|
227
|
+
}
|
|
228
|
+
if (exists) {
|
|
229
|
+
rmSync(target, { force: true });
|
|
230
|
+
rmSync(`${target}-wal`, { force: true });
|
|
231
|
+
rmSync(`${target}-shm`, { force: true });
|
|
232
|
+
}
|
|
233
|
+
await getDatabase(source).backup(target);
|
|
234
|
+
sendJson(response, 201, { name, overwritten: exists });
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
cloneLocks.delete(target);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
DEBUG && console.error(error);
|
|
242
|
+
sendError(response, 400, error);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
167
245
|
async function onSchema(response, db, includeInternal) {
|
|
168
246
|
try {
|
|
169
247
|
const sqlite = getDatabase(db);
|