spooder 4.3.2 → 4.4.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 +43 -26
- package/package.json +4 -1
- package/src/api.ts +137 -53
package/README.md
CHANGED
|
@@ -71,8 +71,6 @@ The `CLI` component of `spooder` is a global command-line tool for running serve
|
|
|
71
71
|
- [`set_cookie(res: Response, name: string, value: string, options?: CookieOptions)`](#api-state-management-set-cookie)
|
|
72
72
|
- [`get_cookies(source: Request | Response): Record<string, string>`](#api-state-management-get-cookies)
|
|
73
73
|
- [API > Database Schema](#api-database-schema)
|
|
74
|
-
- [`db_update_schema_sqlite(db: Database, schema: string): Promise<void>`](#api-database-schema-db-update-schema-sqlite)
|
|
75
|
-
- [`db_init_schema_sqlite(db_path: string, schema: string): Promise<Database>`](#api-database-schema-db-init-schema-sqlite)
|
|
76
74
|
|
|
77
75
|
# Installation
|
|
78
76
|
|
|
@@ -1329,13 +1327,52 @@ const cookies = get_cookies(req, true);
|
|
|
1329
1327
|
|
|
1330
1328
|
`spooder` provides a straightforward API to manage database schema in revisions through source control.
|
|
1331
1329
|
|
|
1330
|
+
Database schema is updated with `db_update_schema_DRIVER` where `DRIVER` corresponds to the database driver being used.
|
|
1331
|
+
|
|
1332
1332
|
> [!NOTE]
|
|
1333
|
-
> Currently, only SQLite
|
|
1333
|
+
> Currently, only SQLite and MySQL are supported. This may be expanded once Bun supports more database drivers.
|
|
1334
|
+
|
|
1335
|
+
```ts
|
|
1336
|
+
// sqlite example
|
|
1337
|
+
import { db_update_schema_sqlite } from 'spooder';
|
|
1338
|
+
import { Database } from 'bun:sqlite';
|
|
1339
|
+
|
|
1340
|
+
const db = new Database('./database.sqlite');
|
|
1341
|
+
await db_update_schema_sqlite(db, './schema');
|
|
1342
|
+
```
|
|
1343
|
+
|
|
1344
|
+
```ts
|
|
1345
|
+
// mysql example
|
|
1346
|
+
import { db_update_schema_mysql } from 'spooder';
|
|
1347
|
+
import mysql from 'mysql2';
|
|
1348
|
+
|
|
1349
|
+
const db = await mysql.createConnection({
|
|
1350
|
+
// connection options
|
|
1351
|
+
// see https://github.com/mysqljs/mysql#connection-options
|
|
1352
|
+
})
|
|
1353
|
+
```
|
|
1354
|
+
|
|
1355
|
+
> [!IMPORTANT]
|
|
1356
|
+
> MySQL requires the optional dependency `mysql2` to be installed - this is not automatically installed with spooder. This will be replaced when bun:sql supports MySQL natively.
|
|
1334
1357
|
|
|
1335
|
-
|
|
1336
|
-
### 🔧 `db_update_schema_sqlite(db: Database, schema: string): Promise<void>`
|
|
1358
|
+
Database initiation and schema updating can be streamlined with the `db_init_schema_DRIVER` functions. The following examples are equivalent to the above ones.
|
|
1337
1359
|
|
|
1338
|
-
|
|
1360
|
+
```ts
|
|
1361
|
+
// sqlite example
|
|
1362
|
+
import { db_init_schema_sqlite } from 'spooder';
|
|
1363
|
+
const db = await db_init_schema_sqlite('./database.sqlite', './schema');
|
|
1364
|
+
```
|
|
1365
|
+
|
|
1366
|
+
```ts
|
|
1367
|
+
// mysql example
|
|
1368
|
+
import { db_init_schema_mysql } from 'spooder';
|
|
1369
|
+
const db = await db_init_schema_mysql({
|
|
1370
|
+
// connection options
|
|
1371
|
+
// see https://github.com/mysqljs/mysql#connection-options
|
|
1372
|
+
}, './schema');
|
|
1373
|
+
```
|
|
1374
|
+
|
|
1375
|
+
### Schema Files
|
|
1339
1376
|
|
|
1340
1377
|
The schema directory is expected to contain an SQL file for each table in the database with the file name matching the name of the table.
|
|
1341
1378
|
|
|
@@ -1408,26 +1445,6 @@ try {
|
|
|
1408
1445
|
}
|
|
1409
1446
|
```
|
|
1410
1447
|
|
|
1411
|
-
<a id="api-database-schema-db-init-schema-sqlite"></a>
|
|
1412
|
-
### 🔧 `db_init_schema_sqlite(db_path: string, schema: string): Promise<Database>`
|
|
1413
|
-
|
|
1414
|
-
`db_init_schema_sqlite` exists as a convenience function to create a new database and apply the schema in one step.
|
|
1415
|
-
|
|
1416
|
-
```ts
|
|
1417
|
-
import { db_init_schema_sqlite } from 'spooder';
|
|
1418
|
-
const db = await db_init_schema_sqlite('./database.sqlite', './schema');
|
|
1419
|
-
```
|
|
1420
|
-
|
|
1421
|
-
The above is equivalent to the following.
|
|
1422
|
-
|
|
1423
|
-
```ts
|
|
1424
|
-
import { db_update_schema_sqlite } from 'spooder';
|
|
1425
|
-
import { Database } from 'bun:sqlite';
|
|
1426
|
-
|
|
1427
|
-
const db = new Database('./database.sqlite', { create: true });
|
|
1428
|
-
await db_update_schema_sqlite(db, './schema');
|
|
1429
|
-
```
|
|
1430
|
-
|
|
1431
1448
|
## Legal
|
|
1432
1449
|
This software is provided as-is with no warranty or guarantee. The authors of this project are not responsible or liable for any problems caused by using this software or any part thereof. Use of this software does not entitle you to any support or assistance from the authors of this project.
|
|
1433
1450
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spooder",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.4.0",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
7
7
|
"bun": "./src/api.ts",
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
"devDependencies": {
|
|
13
13
|
"@types/bun": "^1.0.5"
|
|
14
14
|
},
|
|
15
|
+
"optionalDependencies": {
|
|
16
|
+
"mysql2": "^3.11.0"
|
|
17
|
+
},
|
|
15
18
|
"bin": {
|
|
16
19
|
"spooder": "./src/cli.ts"
|
|
17
20
|
}
|
package/src/api.ts
CHANGED
|
@@ -6,6 +6,16 @@ import { log } from './utils';
|
|
|
6
6
|
import crypto from 'crypto';
|
|
7
7
|
import { Blob } from 'node:buffer';
|
|
8
8
|
import { Database } from 'bun:sqlite';
|
|
9
|
+
import type * as mysql_types from 'mysql2/promise';
|
|
10
|
+
|
|
11
|
+
let mysql: typeof mysql_types | undefined;
|
|
12
|
+
try {
|
|
13
|
+
mysql = await import('mysql2/promise') as typeof mysql_types;
|
|
14
|
+
} catch (e) {
|
|
15
|
+
// mysql2 optional dependency not installed.
|
|
16
|
+
// this dependency will be replaced once bun:sql supports mysql.
|
|
17
|
+
// db_update_schema_mysql and db_init_schema_mysql will throw.
|
|
18
|
+
}
|
|
9
19
|
|
|
10
20
|
export const HTTP_STATUS_CODE = http.STATUS_CODES;
|
|
11
21
|
|
|
@@ -207,6 +217,66 @@ export async function generate_hash_subs(length = 7, prefix = 'hash=', hashes?:
|
|
|
207
217
|
}
|
|
208
218
|
|
|
209
219
|
type Row_DBSchema = { db_schema_table_name: string, db_schema_version: number };
|
|
220
|
+
type SchemaVersionMap = Map<string, number>;
|
|
221
|
+
|
|
222
|
+
async function db_load_schema(schema_dir: string, schema_versions: SchemaVersionMap) {
|
|
223
|
+
const schema_out = [];
|
|
224
|
+
const schema_files = await fs.readdir(schema_dir);
|
|
225
|
+
|
|
226
|
+
for (const schema_file of schema_files) {
|
|
227
|
+
const schema_file_lower = schema_file.toLowerCase();
|
|
228
|
+
if (!schema_file_lower.endsWith('.sql'))
|
|
229
|
+
continue;
|
|
230
|
+
|
|
231
|
+
log('[{db}] parsing schema file {%s}', schema_file_lower);
|
|
232
|
+
|
|
233
|
+
const schema_name = path.basename(schema_file_lower, '.sql');
|
|
234
|
+
const schema_path = path.join(schema_dir, schema_file);
|
|
235
|
+
const schema = await fs.readFile(schema_path, 'utf8');
|
|
236
|
+
|
|
237
|
+
const revisions = new Map();
|
|
238
|
+
let current_rev_id = 0;
|
|
239
|
+
let current_rev = '';
|
|
240
|
+
|
|
241
|
+
for (const line of schema.split(/\r?\n/)) {
|
|
242
|
+
const rev_start = line.match(/^--\s*\[(\d+)\]/);
|
|
243
|
+
if (rev_start !== null) {
|
|
244
|
+
// New chunk definition detected, store the current chunk and start a new one.
|
|
245
|
+
if (current_rev_id > 0) {
|
|
246
|
+
revisions.set(current_rev_id, current_rev);
|
|
247
|
+
current_rev = '';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const rev_number = parseInt(rev_start[1]);
|
|
251
|
+
if (isNaN(rev_number) || rev_number < 1)
|
|
252
|
+
throw new Error(rev_number + ' is not a valid revision number in ' + schema_file_lower);
|
|
253
|
+
current_rev_id = rev_number;
|
|
254
|
+
} else {
|
|
255
|
+
// Append to existing revision.
|
|
256
|
+
current_rev += line + '\n';
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// There may be something left in current_chunk once we reach end of the file.
|
|
261
|
+
if (current_rev_id > 0)
|
|
262
|
+
revisions.set(current_rev_id, current_rev);
|
|
263
|
+
|
|
264
|
+
if (revisions.size === 0) {
|
|
265
|
+
log('[{db}] {%s} contains no valid revisions', schema_file);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const current_schema_version = schema_versions.get(schema_name) ?? 0;
|
|
270
|
+
schema_out.push({
|
|
271
|
+
revisions,
|
|
272
|
+
name: schema_name,
|
|
273
|
+
current_version: current_schema_version,
|
|
274
|
+
chunk_keys: Array.from(revisions.keys()).filter(chunk_id => chunk_id > current_schema_version).sort((a, b) => a - b)
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return schema_out;
|
|
279
|
+
}
|
|
210
280
|
|
|
211
281
|
export async function db_update_schema_sqlite(db: Database, schema_dir: string) {
|
|
212
282
|
log('[{db}] updating database schema for {%s}', db.filename);
|
|
@@ -228,67 +298,68 @@ export async function db_update_schema_sqlite(db: Database, schema_dir: string)
|
|
|
228
298
|
ON CONFLICT(db_schema_table_name) DO UPDATE SET db_schema_version = EXCLUDED.db_schema_version
|
|
229
299
|
`);
|
|
230
300
|
|
|
231
|
-
const
|
|
232
|
-
for (const schema_file of schema_files) {
|
|
233
|
-
const schema_file_lower = schema_file.toLowerCase();
|
|
234
|
-
if (!schema_file_lower.endsWith('.sql'))
|
|
235
|
-
continue;
|
|
236
|
-
|
|
237
|
-
log('[{db}] parsing schema file {%s}', schema_file_lower);
|
|
238
|
-
|
|
239
|
-
const schema_name = path.basename(schema_file_lower, '.sql');
|
|
240
|
-
const schema_path = path.join(schema_dir, schema_file);
|
|
241
|
-
const schema = await fs.readFile(schema_path, 'utf8');
|
|
242
|
-
|
|
243
|
-
const revisions = new Map();
|
|
244
|
-
let current_rev_id = 0;
|
|
245
|
-
let current_rev = '';
|
|
246
|
-
|
|
247
|
-
for (const line of schema.split(/\r?\n/)) {
|
|
248
|
-
const rev_start = line.match(/^--\s*\[(\d+)\]/);
|
|
249
|
-
if (rev_start !== null) {
|
|
250
|
-
// New chunk definition detected, store the current chunk and start a new one.
|
|
251
|
-
if (current_rev_id > 0) {
|
|
252
|
-
revisions.set(current_rev_id, current_rev);
|
|
253
|
-
current_rev = '';
|
|
254
|
-
}
|
|
301
|
+
const schemas = await db_load_schema(schema_dir, schema_versions);
|
|
255
302
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
303
|
+
for (const schema of schemas) {
|
|
304
|
+
let newest_schema_version = schema.current_version;
|
|
305
|
+
for (const rev_id of schema.chunk_keys) {
|
|
306
|
+
const revision = schema.revisions.get(rev_id);
|
|
307
|
+
log('[{db}] applying revision {%d} to {%s}', rev_id, schema.name);
|
|
308
|
+
db.transaction(() => db.run(revision))();
|
|
309
|
+
newest_schema_version = rev_id;
|
|
264
310
|
}
|
|
311
|
+
|
|
312
|
+
if (newest_schema_version > schema.current_version) {
|
|
313
|
+
log('[{db}] updated table {%s} to revision {%d}', schema.name, newest_schema_version);
|
|
314
|
+
update_schema_query.run(newest_schema_version, schema.name);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
})();
|
|
318
|
+
}
|
|
265
319
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
320
|
+
export async function db_update_schema_mysql(db: mysql_types.Connection, schema_dir: string) {
|
|
321
|
+
if (mysql === undefined)
|
|
322
|
+
throw new Error('{db_update_schema_mysql} cannot be called without optional dependency {mysql2} installed');
|
|
269
323
|
|
|
270
|
-
|
|
271
|
-
log('[{db}] {%s} contains no valid revisions', schema_file);
|
|
272
|
-
continue;
|
|
273
|
-
}
|
|
324
|
+
log('[{db}] updating database schema for {%s}', db.config.database);
|
|
274
325
|
|
|
275
|
-
|
|
276
|
-
const chunk_keys = Array.from(revisions.keys()).filter(chunk_id => chunk_id > current_schema_version).sort((a, b) => a - b);
|
|
326
|
+
const schema_versions = new Map();
|
|
277
327
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
328
|
+
try {
|
|
329
|
+
const [rows] = await db.query('SELECT db_schema_table_name, db_schema_version FROM db_schema');
|
|
330
|
+
for (const row of rows as Array<Row_DBSchema>)
|
|
331
|
+
schema_versions.set(row.db_schema_table_name, row.db_schema_version);
|
|
332
|
+
} catch (e) {
|
|
333
|
+
log('[{db}] creating {db_schema} table');
|
|
334
|
+
await db.query('CREATE TABLE db_schema (db_schema_table_name VARCHAR(255) PRIMARY KEY, db_schema_version INT)');
|
|
335
|
+
}
|
|
285
336
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
337
|
+
await db.beginTransaction();
|
|
338
|
+
|
|
339
|
+
const update_schema_query = await db.prepare(`
|
|
340
|
+
INSERT INTO db_schema (db_schema_version, db_schema_table_name) VALUES (?, ?)
|
|
341
|
+
ON DUPLICATE KEY UPDATE db_schema_version = VALUES(db_schema_version);
|
|
342
|
+
`);
|
|
343
|
+
|
|
344
|
+
const schemas = await db_load_schema(schema_dir, schema_versions);
|
|
345
|
+
for (const schema of schemas) {
|
|
346
|
+
let newest_schema_version = schema.current_version;
|
|
347
|
+
for (const rev_id of schema.chunk_keys) {
|
|
348
|
+
const revision = schema.revisions.get(rev_id);
|
|
349
|
+
log('[{db}] applying revision {%d} to {%s}', rev_id, schema.name);
|
|
350
|
+
|
|
351
|
+
await db.query(revision);
|
|
352
|
+
newest_schema_version = rev_id;
|
|
290
353
|
}
|
|
291
|
-
|
|
354
|
+
|
|
355
|
+
if (newest_schema_version > schema.current_version) {
|
|
356
|
+
log('[{db}] updated table {%s} to revision {%d}', schema.name, newest_schema_version);
|
|
357
|
+
|
|
358
|
+
await update_schema_query.execute([newest_schema_version, schema.name]);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
await db.commit();
|
|
292
363
|
}
|
|
293
364
|
|
|
294
365
|
export async function db_init_schema_sqlite(db_path: string, schema_dir: string): Promise<Database> {
|
|
@@ -297,6 +368,19 @@ export async function db_init_schema_sqlite(db_path: string, schema_dir: string)
|
|
|
297
368
|
return db;
|
|
298
369
|
}
|
|
299
370
|
|
|
371
|
+
export async function db_init_schema_mysql(db_info: mysql_types.ConnectionConfig, schema_dir: string): Promise<mysql_types.Connection> {
|
|
372
|
+
if (mysql === undefined)
|
|
373
|
+
throw new Error('{db_init_schema_mysql} cannot be called without optional dependency {mysql2} installed');
|
|
374
|
+
|
|
375
|
+
// required for parsing multiple statements from schema files
|
|
376
|
+
db_info.multipleStatements = true;
|
|
377
|
+
|
|
378
|
+
const db = await mysql.createConnection(db_info);
|
|
379
|
+
await db_update_schema_mysql(db, schema_dir);
|
|
380
|
+
|
|
381
|
+
return db;
|
|
382
|
+
}
|
|
383
|
+
|
|
300
384
|
type CookieOptions = {
|
|
301
385
|
same_site?: 'Strict' | 'Lax' | 'None',
|
|
302
386
|
secure?: boolean,
|