spooder 6.0.0 → 6.1.1

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/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.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="],
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.7.2", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA=="],
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
- "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="],
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
- "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
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.0.0",
5
+ "version": "6.1.1",
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 (message: string, ...params: any[]) => {
523
- console.log(prefix + message.replace(/\{([^}]+)\}/g, `${ansi}$1\x1b[0m`), ...params);
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,117 @@ 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
+ };
2120
+
2121
+ type TableRevision = {
2122
+ revision_number: number;
2123
+ file_path: string;
2124
+ filename: string;
2125
+ };
2126
+
2127
+ const db_log = log_create_logger('db', 'spooder');
2128
+
2129
+ export function db_set_cast<T extends string>(set: string | null): Set<T> {
2130
+ return new Set(set?.split(',') as T[] ?? []);
2131
+ }
2132
+
2133
+ export function db_set_serialize<T extends string>(set: Iterable<T> | null): string {
2134
+ return set ? Array.from(set).join(',') : '';
2135
+ }
2136
+
2137
+ export async function db_get_schema_revision(db: SQL): Promise<number|null> {
2138
+ try {
2139
+ const [result] = await db`SELECT MAX(revision_number) as latest_revision FROM db_schema`;
2140
+ return result.latest_revision ?? 0;
2141
+ } catch (e) {
2142
+ return null;
2143
+ }
2144
+ }
2145
+
2146
+ export async function db_schema(db: SQL, schema_path: string, options?: SchemaOptions): Promise<boolean> {
2147
+ const schema_table = options?.schema_table ?? 'db_schema';
2148
+ const recursive = options?.recursive ?? true;
2149
+
2150
+ db_log`applying schema revisions from ${schema_path}`;
2151
+ let current_revision = await db_get_schema_revision(db);
2152
+
2153
+ if (current_revision === null) {
2154
+ db_log`initiating schema database table ${schema_table}`;
2155
+ await db`CREATE TABLE ${db(schema_table)} (
2156
+ revision_number INTEGER PRIMARY KEY,
2157
+ filename VARCHAR(255) NOT NULL,
2158
+ applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
2159
+ );`;
2160
+ }
2161
+
2162
+ current_revision ??= 0;
2163
+
2164
+ const revisions = Array<TableRevision>();
2165
+ const files = await fs.readdir(schema_path, { recursive, encoding: 'utf8' });
2166
+ for (const file of files) {
2167
+ const filename = path.basename(file);
2168
+ if (!filename.toLowerCase().endsWith('.sql'))
2169
+ continue;
2170
+
2171
+ const match = filename.match(/^(\d+)/);
2172
+ const revision_number = match ? Number(match[1]) : null;
2173
+ if (revision_number === null || revision_number < 1) {
2174
+ log_error`skipping sql file ${file}, invalid revision number`;
2175
+ continue;
2176
+ }
2177
+
2178
+ const file_path = path.join(schema_path, file);
2179
+ if (revision_number > current_revision) {
2180
+ revisions.push({
2181
+ revision_number,
2182
+ file_path,
2183
+ filename
2184
+ });
2185
+ }
2186
+ }
2187
+
2188
+ // sort revisions in ascending order before applying
2189
+ // for recursive trees or unreliable OS sort ordering
2190
+ revisions.sort((a, b) => a.revision_number - b.revision_number);
2191
+
2192
+ const revisions_applied = Array<string>();
2193
+ for (const rev of revisions) {
2194
+ db_log`applying revision ${rev.revision_number} from ${rev.filename}`;
2195
+
2196
+ try {
2197
+ await db.begin(async tx => {
2198
+ await tx.file(rev.file_path);
2199
+ await tx`INSERT INTO ${db(schema_table)} ${db(rev, 'revision_number', 'filename')}`;
2200
+ revisions_applied.push(rev.filename);
2201
+ });
2202
+ } catch (err) {
2203
+
2204
+ log_error`failed to apply revisions from ${rev.filename}: ${err}`;
2205
+ log_error`${'warning'}: if ${rev.filename} contained DDL statements, they will ${'not'} be rolled back automatically`;
2206
+ log_error`verify the current database state ${'before'} running ammended revisions`;
2207
+
2208
+ const last_revision = await db_get_schema_revision(db);
2209
+ db_log`database schema revision is now ${last_revision ?? 0}`;
2210
+
2211
+ caution('db_schema failed', { rev, err, last_revision, revisions_applied });
2212
+
2213
+ return false;
2214
+ }
2215
+ }
2216
+
2217
+ if (revisions_applied.length > 0) {
2218
+ const new_revision = await db_get_schema_revision(db);
2219
+ db_log`applied ${revisions_applied.length} database schema revisions (${current_revision} >> ${new_revision})`;
2220
+ } else {
2221
+ db_log`no database schema revisions to apply (current: ${current_revision})`;
2222
+ }
2223
+
2224
+ return true;
2225
+ }
2105
2226
  // endregion
package/test.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { SQL } from 'bun';
2
+ import * as spooder from 'spooder';
3
+
4
+ type TestRow = {
5
+ ID: number;
6
+ test: string;
7
+ };
8
+
9
+ const db = new SQL('mysql://test:1141483652@localhost:3306/test');
10
+ await spooder.db_schema(db, './db/revisions');