spooder 6.0.0 → 6.1.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 +82 -509
- package/bun.lock +4 -33
- package/package.json +2 -4
- package/src/api.ts +129 -7
- package/test.ts +10 -0
- package/src/api_db.ts +0 -732
package/bun.lock
CHANGED
|
@@ -6,48 +6,19 @@
|
|
|
6
6
|
"devDependencies": {
|
|
7
7
|
"@types/bun": "^1.2.20",
|
|
8
8
|
},
|
|
9
|
-
"optionalDependencies": {
|
|
10
|
-
"mysql2": "^3.11.0",
|
|
11
|
-
},
|
|
12
9
|
},
|
|
13
10
|
},
|
|
14
11
|
"packages": {
|
|
15
|
-
"@types/bun": ["@types/bun@1.3.
|
|
12
|
+
"@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="],
|
|
16
13
|
|
|
17
|
-
"@types/node": ["@types/node@24.
|
|
14
|
+
"@types/node": ["@types/node@24.9.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg=="],
|
|
18
15
|
|
|
19
16
|
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
|
|
20
17
|
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
"bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="],
|
|
18
|
+
"bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],
|
|
24
19
|
|
|
25
20
|
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
|
26
21
|
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
"generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="],
|
|
30
|
-
|
|
31
|
-
"iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="],
|
|
32
|
-
|
|
33
|
-
"is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
|
|
34
|
-
|
|
35
|
-
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
|
36
|
-
|
|
37
|
-
"lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
|
|
38
|
-
|
|
39
|
-
"lru.min": ["lru.min@1.1.2", "", {}, "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg=="],
|
|
40
|
-
|
|
41
|
-
"mysql2": ["mysql2@3.15.2", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-kFm5+jbwR5mC+lo+3Cy46eHiykWSpUtTLOH3GE+AR7GeLq8PgfJcvpMiyVWk9/O53DjQsqm6a3VOOfq7gYWFRg=="],
|
|
42
|
-
|
|
43
|
-
"named-placeholders": ["named-placeholders@1.1.3", "", { "dependencies": { "lru-cache": "^7.14.1" } }, "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w=="],
|
|
44
|
-
|
|
45
|
-
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
|
46
|
-
|
|
47
|
-
"seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="],
|
|
48
|
-
|
|
49
|
-
"sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="],
|
|
50
|
-
|
|
51
|
-
"undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="],
|
|
22
|
+
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
|
52
23
|
}
|
|
53
24
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spooder",
|
|
3
|
+
"author": "Kruithne <kruithne@gmail.com>",
|
|
3
4
|
"type": "module",
|
|
4
|
-
"version": "6.
|
|
5
|
+
"version": "6.1.0",
|
|
5
6
|
"module": "./src/api.ts",
|
|
6
7
|
"bin": {
|
|
7
8
|
"spooder": "./src/cli.ts"
|
|
@@ -16,8 +17,5 @@
|
|
|
16
17
|
},
|
|
17
18
|
"devDependencies": {
|
|
18
19
|
"@types/bun": "^1.2.20"
|
|
19
|
-
},
|
|
20
|
-
"optionalDependencies": {
|
|
21
|
-
"mysql2": "^3.11.0"
|
|
22
20
|
}
|
|
23
21
|
}
|
package/src/api.ts
CHANGED
|
@@ -4,7 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import fs from 'node:fs/promises';
|
|
5
5
|
import crypto from 'crypto';
|
|
6
6
|
import { Blob } from 'node:buffer';
|
|
7
|
-
import { ColorInput } from 'bun';
|
|
7
|
+
import { ColorInput, SQL } from 'bun';
|
|
8
8
|
import packageJson from '../package.json' with { type: 'json' };
|
|
9
9
|
|
|
10
10
|
// region exit codes
|
|
@@ -21,10 +21,6 @@ export const EXIT_CODE_NAMES = Object.fromEntries(
|
|
|
21
21
|
);
|
|
22
22
|
// endregion
|
|
23
23
|
|
|
24
|
-
// region api forwarding
|
|
25
|
-
export * from './api_db';
|
|
26
|
-
// endregion
|
|
27
|
-
|
|
28
24
|
// region workers
|
|
29
25
|
type WorkerMessageData = Record<string, any>;
|
|
30
26
|
type WorkerMessage = {
|
|
@@ -519,8 +515,20 @@ export function log_create_logger(label: string, color: ColorInput = 'blue') {
|
|
|
519
515
|
const ansi = Bun.color(color, 'ansi-256') ?? '\x1b[38;5;6m';
|
|
520
516
|
const prefix = `[${ansi}${label}\x1b[0m] `;
|
|
521
517
|
|
|
522
|
-
return (
|
|
523
|
-
|
|
518
|
+
return (strings: TemplateStringsArray | string, ...values: any[]) => {
|
|
519
|
+
if (typeof strings === 'string') {
|
|
520
|
+
// regular string with { } syntax
|
|
521
|
+
console.log(prefix + strings.replace(/\{([^}]+)\}/g, `${ansi}$1\x1b[0m`), ...values);
|
|
522
|
+
} else {
|
|
523
|
+
// tagged template literal
|
|
524
|
+
let message = '';
|
|
525
|
+
for (let i = 0; i < strings.length; i++) {
|
|
526
|
+
message += strings[i];
|
|
527
|
+
if (i < values.length)
|
|
528
|
+
message += `${ansi}${values[i]}\x1b[0m`;
|
|
529
|
+
}
|
|
530
|
+
console.log(prefix + message);
|
|
531
|
+
}
|
|
524
532
|
};
|
|
525
533
|
}
|
|
526
534
|
|
|
@@ -2102,4 +2110,118 @@ export function http_serve(port: number, hostname?: string) {
|
|
|
2102
2110
|
}
|
|
2103
2111
|
};
|
|
2104
2112
|
}
|
|
2113
|
+
// endregion
|
|
2114
|
+
|
|
2115
|
+
// region db
|
|
2116
|
+
type SchemaOptions = {
|
|
2117
|
+
schema_table: string;
|
|
2118
|
+
recursive: boolean;
|
|
2119
|
+
throw_on_skip: boolean;
|
|
2120
|
+
};
|
|
2121
|
+
|
|
2122
|
+
type TableRevision = {
|
|
2123
|
+
revision_number: number;
|
|
2124
|
+
file_path: string;
|
|
2125
|
+
filename: string;
|
|
2126
|
+
};
|
|
2127
|
+
|
|
2128
|
+
const db_log = log_create_logger('db', 'spooder');
|
|
2129
|
+
|
|
2130
|
+
export function db_set_cast<T extends string>(set: string | null): Set<T> {
|
|
2131
|
+
return new Set(set?.split(',') as T[] ?? []);
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
export function db_set_serialize<T extends string>(set: Iterable<T> | null): string {
|
|
2135
|
+
return set ? Array.from(set).join(',') : '';
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
export async function db_get_schema_revision(db: SQL): Promise<number|null> {
|
|
2139
|
+
try {
|
|
2140
|
+
const [result] = await db`SELECT MAX(revision_number) as latest_revision FROM db_schema`;
|
|
2141
|
+
return result.latest_revision ?? 0;
|
|
2142
|
+
} catch (e) {
|
|
2143
|
+
return null;
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
export async function db_schema(db: SQL, schema_path: string, options?: SchemaOptions): Promise<boolean> {
|
|
2148
|
+
const schema_table = options?.schema_table ?? 'db_schema';
|
|
2149
|
+
const recursive = options?.recursive ?? true;
|
|
2150
|
+
|
|
2151
|
+
db_log`applying schema revisions from ${schema_path}`;
|
|
2152
|
+
let current_revision = await db_get_schema_revision(db);
|
|
2153
|
+
|
|
2154
|
+
if (current_revision === null) {
|
|
2155
|
+
db_log`initiating schema database table ${schema_table}`;
|
|
2156
|
+
await db`CREATE TABLE ${db(schema_table)} (
|
|
2157
|
+
revision_number INTEGER PRIMARY KEY,
|
|
2158
|
+
filename VARCHAR(255) NOT NULL,
|
|
2159
|
+
applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
2160
|
+
);`;
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
current_revision ??= 0;
|
|
2164
|
+
|
|
2165
|
+
const revisions = Array<TableRevision>();
|
|
2166
|
+
const files = await fs.readdir(schema_path, { recursive, encoding: 'utf8' });
|
|
2167
|
+
for (const file of files) {
|
|
2168
|
+
const filename = path.basename(file);
|
|
2169
|
+
if (!filename.toLowerCase().endsWith('.sql'))
|
|
2170
|
+
continue;
|
|
2171
|
+
|
|
2172
|
+
const match = filename.match(/^(\d+)/);
|
|
2173
|
+
const revision_number = match ? Number(match[1]) : null;
|
|
2174
|
+
if (revision_number === null || revision_number < 1) {
|
|
2175
|
+
log_error`skipping sql file ${file}, invalid revision number`;
|
|
2176
|
+
continue;
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
const file_path = path.join(schema_path, file);
|
|
2180
|
+
if (revision_number > current_revision) {
|
|
2181
|
+
revisions.push({
|
|
2182
|
+
revision_number,
|
|
2183
|
+
file_path,
|
|
2184
|
+
filename
|
|
2185
|
+
});
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
// sort revisions in ascending order before applying
|
|
2190
|
+
// for recursive trees or unreliable OS sort ordering
|
|
2191
|
+
revisions.sort((a, b) => a.revision_number - b.revision_number);
|
|
2192
|
+
|
|
2193
|
+
const revisions_applied = Array<string>();
|
|
2194
|
+
for (const rev of revisions) {
|
|
2195
|
+
db_log`applying revision ${rev.revision_number} from ${rev.filename}`;
|
|
2196
|
+
|
|
2197
|
+
try {
|
|
2198
|
+
await db.begin(async tx => {
|
|
2199
|
+
await tx.file(rev.file_path);
|
|
2200
|
+
await tx`INSERT INTO ${db(schema_table)} ${db(rev, 'revision_number', 'filename')}`;
|
|
2201
|
+
revisions_applied.push(rev.filename);
|
|
2202
|
+
});
|
|
2203
|
+
} catch (err) {
|
|
2204
|
+
|
|
2205
|
+
log_error`failed to apply revisions from ${rev.filename}: ${err}`;
|
|
2206
|
+
log_error`${'warning'}: if ${rev.filename} contained DDL statements, they will ${'not'} be rolled back automatically`;
|
|
2207
|
+
log_error`verify the current database state ${'before'} running ammended revisions`;
|
|
2208
|
+
|
|
2209
|
+
const last_revision = await db_get_schema_revision(db);
|
|
2210
|
+
db_log`database schema revision is now ${last_revision ?? 0}`;
|
|
2211
|
+
|
|
2212
|
+
caution('db_schema failed', { rev, err, last_revision, revisions_applied });
|
|
2213
|
+
|
|
2214
|
+
return false;
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
if (revisions_applied.length > 0) {
|
|
2219
|
+
const new_revision = await db_get_schema_revision(db);
|
|
2220
|
+
db_log`applied ${revisions_applied.length} database schema revisions (${current_revision} >> ${new_revision})`;
|
|
2221
|
+
} else {
|
|
2222
|
+
db_log`no database schema revisions to apply (current: ${current_revision})`;
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
return true;
|
|
2226
|
+
}
|
|
2105
2227
|
// endregion
|
package/test.ts
ADDED