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/README.md +82 -509
- package/bun.lock +4 -33
- package/package.json +2 -4
- package/src/api.ts +128 -7
- package/test.ts +10 -0
- package/src/api_db.ts +0 -732
package/README.md
CHANGED
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
The design goal behind `spooder` is not to provide a full-featured web server, but to expand the Bun runtime with a set of APIs and utilities that make it easy to develop servers with minimal overhead.
|
|
12
12
|
|
|
13
|
+
### spooderverse
|
|
14
|
+
In addition to the core API provided here, you can also find [spooderverse](https://github.com/Kruithne/spooderverse) which is a collection of drop-in modules designed for spooder with minimal overhead and zero dependencies.
|
|
15
|
+
|
|
13
16
|
> [!NOTE]
|
|
14
17
|
> If you think a is missing a feature, consider opening an issue with your use-case. The goal behind `spooder` is to provide APIs that are useful for a wide range of use-cases, not to provide bespoke features better suited for userland.
|
|
15
18
|
|
|
@@ -108,10 +111,8 @@ The `CLI` component of `spooder` is a global command-line tool for running serve
|
|
|
108
111
|
- [API > Cache Busting](#api-cache-busting)
|
|
109
112
|
- [API > Git](#api-git)
|
|
110
113
|
- [API > Database](#api-database)
|
|
114
|
+
- [API > Database > Utilities](#api-database-utilities)
|
|
111
115
|
- [API > Database > Schema](#api-database-schema)
|
|
112
|
-
- [API > Database > Interface](#api-database-interface)
|
|
113
|
-
- [API > Database > Interface > SQLite](#api-database-interface-sqlite)
|
|
114
|
-
- [API > Database > Interface > MySQL](#api-database-interface-mysql)
|
|
115
116
|
- [API > Utilities](#api-utilities)
|
|
116
117
|
|
|
117
118
|
# CLI
|
|
@@ -672,46 +673,20 @@ cache_bust_get_hash_table(): Record<string, string>;
|
|
|
672
673
|
|
|
673
674
|
// git
|
|
674
675
|
git_get_hashes(length: number): Promise<Record<string, string>>;
|
|
675
|
-
git_get_hashes_sync(length: number): Record<string, string
|
|
676
|
-
|
|
677
|
-
// database
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
db_cast_set<T extends string>(set: string | null): Set<T>;
|
|
681
|
-
db_serialize_set<T extends string>(set: Set<T> | null): string;
|
|
682
|
-
|
|
683
|
-
// db_sqlite
|
|
684
|
-
update_schema(db_dir: string, schema_table?: string): Promise<void>
|
|
685
|
-
insert(sql: string, ...values: any): number;
|
|
686
|
-
insert_object(table: string, obj: Record<string, any>): number;
|
|
687
|
-
execute(sql: string, ...values: any): number;
|
|
688
|
-
get_all<T>(sql: string, ...values: any): T[];
|
|
689
|
-
get_single<T>(sql: string, ...values: any): T | null;
|
|
690
|
-
get_column<T>(sql: string, column: string, ...values: any): T[];
|
|
691
|
-
get_paged<T>(sql: string, values?: any[], page_size?: number): AsyncGenerator<T[]>;
|
|
692
|
-
count(sql: string, ...values: any): number;
|
|
693
|
-
count_table(table_name: string): number;
|
|
694
|
-
exists(sql: string, ...values: any): boolean;
|
|
695
|
-
transaction(scope: (transaction: SQLiteDatabaseInterface) => void | Promise<void>): boolean;
|
|
696
|
-
|
|
697
|
-
// db_mysql
|
|
698
|
-
update_schema(db_dir: string, schema_table?: string): Promise<void>
|
|
699
|
-
insert(sql: string, ...values: any): Promise<number>;
|
|
700
|
-
insert_object(table: string, obj: Record<string, any>): Promise<number>;
|
|
701
|
-
execute(sql: string, ...values: any): Promise<number>;
|
|
702
|
-
get_all<T>(sql: string, ...values: any): Promise<T[]>;
|
|
703
|
-
get_single<T>(sql: string, ...values: any): Promise<T | null>;
|
|
704
|
-
get_column<T>(sql: string, column: string, ...values: any): Promise<T[]>;
|
|
705
|
-
call<T>(func_name: string, ...args: any): Promise<T[]>;
|
|
706
|
-
get_paged<T>(sql: string, values?: any[], page_size?: number): AsyncGenerator<T[]>;
|
|
707
|
-
count(sql: string, ...values: any): Promise<number>;
|
|
708
|
-
count_table(table_name: string): Promise<number>;
|
|
709
|
-
exists(sql: string, ...values: any): Promise<boolean>;
|
|
710
|
-
transaction(scope: (transaction: MySQLDatabaseInterface) => void | Promise<void>): Promise<boolean>;
|
|
676
|
+
git_get_hashes_sync(length: number): Record<string, string>;
|
|
677
|
+
|
|
678
|
+
// database utilities
|
|
679
|
+
db_set_cast<T extends string>(set: string | null): Set<T>;
|
|
680
|
+
db_set_serialize<T extends string>(set: Iterable<T> | null): string;
|
|
711
681
|
|
|
712
682
|
// database schema
|
|
713
|
-
|
|
714
|
-
|
|
683
|
+
type SchemaOptions = {
|
|
684
|
+
schema_table?: string;
|
|
685
|
+
recursive?: boolean;
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
db_get_schema_revision(db: SQL): Promise<number|null>;
|
|
689
|
+
db_schema(db: SQL, schema_path: string, options?: SchemaOptions): Promise<boolean>;
|
|
715
690
|
|
|
716
691
|
// caching
|
|
717
692
|
cache_http(options?: CacheOptions);
|
|
@@ -746,6 +721,13 @@ log('Hello, {world}!');
|
|
|
746
721
|
// > [info] Hello, world!
|
|
747
722
|
```
|
|
748
723
|
|
|
724
|
+
Tagged template literals are also supported and automatically highlights values without the brace syntax.
|
|
725
|
+
|
|
726
|
+
```ts
|
|
727
|
+
const user = 'Fred';
|
|
728
|
+
log`Hello ${user}!`;
|
|
729
|
+
```
|
|
730
|
+
|
|
749
731
|
Formatting parameters are supported using standard console logging formatters.
|
|
750
732
|
|
|
751
733
|
```ts
|
|
@@ -2528,525 +2510,116 @@ const full_hashes = await git_get_hashes(40);
|
|
|
2528
2510
|
|
|
2529
2511
|
|
|
2530
2512
|
<a id="api-database"></a>
|
|
2531
|
-
<a id="api-database-interface"></a>
|
|
2532
2513
|
## API > Database
|
|
2533
2514
|
|
|
2534
|
-
|
|
2515
|
+
Before `v6.0.0`, spooder provided a database API for `sqlite` and `mysql` while they were not available natively in `bun`.
|
|
2535
2516
|
|
|
2536
|
-
|
|
2517
|
+
Now that `bun` provides a native API for these, we've dropped our API in favor of those as it aligns with the mission of minimalism.
|
|
2537
2518
|
|
|
2538
|
-
|
|
2539
|
-
enum ExampleRow {
|
|
2540
|
-
OPT_A = 'OPT_A',
|
|
2541
|
-
OPT_B = 'OPT_B',
|
|
2542
|
-
OPT_C = 'OPT_C'
|
|
2543
|
-
};
|
|
2519
|
+
You can see the documentation for the [Bun SQL API here.](https://bun.com/docs/runtime/sql)
|
|
2544
2520
|
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
// ...
|
|
2548
|
-
}
|
|
2549
|
-
```
|
|
2521
|
+
<a id="api-database-utilities"></a>
|
|
2522
|
+
## API > Database > Utilities
|
|
2550
2523
|
|
|
2551
|
-
### 🔧 ``
|
|
2524
|
+
### 🔧 ``db_set_cast<T extends string>(set: string | null): Set<T>``
|
|
2552
2525
|
|
|
2553
|
-
Takes a
|
|
2526
|
+
Takes a database SET string and returns a `Set<T>` where `T` is a provided enum.
|
|
2554
2527
|
|
|
2555
2528
|
```ts
|
|
2556
|
-
enum
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2529
|
+
enum Fruits {
|
|
2530
|
+
Apple = 'Apple',
|
|
2531
|
+
Banana = 'Banana',
|
|
2532
|
+
Lemon = 'Lemon'
|
|
2560
2533
|
};
|
|
2561
2534
|
|
|
2562
|
-
const
|
|
2563
|
-
|
|
2564
|
-
const serialized = db_serialize_set(set);
|
|
2565
|
-
// > 'OPT_A,OPT_B'
|
|
2566
|
-
```
|
|
2567
|
-
|
|
2568
|
-
<a id="api-database-interface-sqlite"></a>
|
|
2569
|
-
## API > Database > Interface > SQLite
|
|
2570
|
-
|
|
2571
|
-
`spooder` provides a simple **SQLite** interface that acts as a wrapper around the Bun SQLite API. The construction parameters match the underlying API.
|
|
2572
|
-
|
|
2573
|
-
```ts
|
|
2574
|
-
// see: https://bun.sh/docs/api/sqlite
|
|
2575
|
-
const db = db_sqlite(':memory:', { create: true });
|
|
2576
|
-
db.instance; // raw access to underlying sqlite instance.
|
|
2577
|
-
```
|
|
2578
|
-
|
|
2579
|
-
### Error Reporting
|
|
2580
|
-
|
|
2581
|
-
In the event of an error from SQLite, an applicable value will be returned from interface functions, rather than the error being thrown.
|
|
2582
|
-
|
|
2583
|
-
```ts
|
|
2584
|
-
const result = await db.get_single('BROKEN QUERY');
|
|
2585
|
-
if (result !== null) {
|
|
2586
|
-
// do more stuff with result
|
|
2587
|
-
}
|
|
2588
|
-
```
|
|
2589
|
-
|
|
2590
|
-
If you have configured the canary reporting feature in spooder, you can instruct the database interface to report errors using this feature with the `use_canary_reporting` parameter.
|
|
2591
|
-
|
|
2592
|
-
```ts
|
|
2593
|
-
const db = db_sqlite(':memory', { ... }, true);
|
|
2594
|
-
```
|
|
2595
|
-
|
|
2596
|
-
### 🔧 ``db_sqlite.update_schema(schema_dir: string, schema_table: string): Promise<void>``
|
|
2597
|
-
|
|
2598
|
-
`spooder` offers a database schema management system. The `update_schema()` function is a shortcut to call this on the underlying database.
|
|
2599
|
-
|
|
2600
|
-
See [API > Database > Schema](#api-database-schema) for information on how schema updating works.
|
|
2601
|
-
|
|
2602
|
-
```ts
|
|
2603
|
-
// without interface
|
|
2604
|
-
import { db_sqlite, db_update_schema_sqlite } from 'spooder';
|
|
2605
|
-
const db = db_sqlite('./my_database.sqlite');
|
|
2606
|
-
await db_update_schema_sqlite(db.instance, './schema');
|
|
2607
|
-
|
|
2608
|
-
// with interface
|
|
2609
|
-
import { db_sqlite } from 'spooder';
|
|
2610
|
-
const db = db_sqlite('./my_database.sqlite');
|
|
2611
|
-
await db.update_schema('./schema');
|
|
2612
|
-
```
|
|
2613
|
-
|
|
2614
|
-
### 🔧 ``db_sqlite.insert(sql: string, ...values: any): number``
|
|
2615
|
-
|
|
2616
|
-
Executes a query and returns the `lastInsertRowid`. Returns `-1` in the event of an error or if `lastInsertRowid` is not provided.
|
|
2617
|
-
|
|
2618
|
-
```ts
|
|
2619
|
-
const id = db.insert('INSERT INTO users (name) VALUES(?)', 'test');
|
|
2620
|
-
```
|
|
2621
|
-
|
|
2622
|
-
### 🔧 ``db_sqlite.insert_object(table: string, obj: Record<string, any>): number``
|
|
2623
|
-
|
|
2624
|
-
Executes an insert query using object key/value mapping and returns the `lastInsertRowid`. Returns `-1` in the event of an error.
|
|
2625
|
-
|
|
2626
|
-
```ts
|
|
2627
|
-
const id = db.insert_object('users', { name: 'John', email: 'john@example.com' });
|
|
2628
|
-
```
|
|
2629
|
-
|
|
2630
|
-
### 🔧 ``db_sqlite.execute(sql: string, ...values: any): number``
|
|
2631
|
-
|
|
2632
|
-
Executes a query and returns the number of affected rows. Returns `-1` in the event of an error.
|
|
2633
|
-
|
|
2634
|
-
```ts
|
|
2635
|
-
const affected = db.execute('UPDATE users SET name = ? WHERE id = ?', 'Jane', 1);
|
|
2636
|
-
```
|
|
2637
|
-
|
|
2638
|
-
### 🔧 ``db_sqlite.get_all<T>(sql: string, ...values: any): T[]``
|
|
2639
|
-
|
|
2640
|
-
Returns the complete query result set as an array. Returns empty array if no rows found or if query fails.
|
|
2641
|
-
|
|
2642
|
-
```ts
|
|
2643
|
-
const users = db.get_all<User>('SELECT * FROM users WHERE active = ?', true);
|
|
2644
|
-
```
|
|
2645
|
-
|
|
2646
|
-
### 🔧 ``db_sqlite.get_single<T>(sql: string, ...values: any): T | null``
|
|
2647
|
-
|
|
2648
|
-
Returns the first row from a query result set. Returns `null` if no rows found or if query fails.
|
|
2649
|
-
|
|
2650
|
-
```ts
|
|
2651
|
-
const user = db.get_single<User>('SELECT * FROM users WHERE id = ?', 1);
|
|
2652
|
-
```
|
|
2653
|
-
|
|
2654
|
-
### 🔧 ``db_sqlite.get_column<T>(sql: string, column: string, ...values: any): T[]``
|
|
2655
|
-
|
|
2656
|
-
Returns the query result as a single column array. Returns empty array if no rows found or if query fails.
|
|
2657
|
-
|
|
2658
|
-
```ts
|
|
2659
|
-
const names = db.get_column<string>('SELECT name FROM users', 'name');
|
|
2660
|
-
```
|
|
2661
|
-
|
|
2662
|
-
### 🔧 ``db_sqlite.get_paged<T>(sql: string, values?: any[], page_size?: number): AsyncGenerator<T[]>``
|
|
2663
|
-
|
|
2664
|
-
Returns an async iterator that yields pages of database rows. Each page contains at most `page_size` rows (default 1000).
|
|
2665
|
-
|
|
2666
|
-
```ts
|
|
2667
|
-
for await (const page of db.get_paged<User>('SELECT * FROM users', [], 100)) {
|
|
2668
|
-
console.log(`Processing ${page.length} users`);
|
|
2669
|
-
}
|
|
2670
|
-
```
|
|
2671
|
-
|
|
2672
|
-
### 🔧 ``db_sqlite.count(sql: string, ...values: any): number``
|
|
2673
|
-
|
|
2674
|
-
Returns the value of `count` from a query. Returns `0` if query fails.
|
|
2675
|
-
|
|
2676
|
-
```ts
|
|
2677
|
-
const user_count = db.count('SELECT COUNT(*) AS count FROM users WHERE active = ?', true);
|
|
2678
|
-
```
|
|
2679
|
-
|
|
2680
|
-
### 🔧 ``db_sqlite.count_table(table_name: string): number``
|
|
2681
|
-
|
|
2682
|
-
Returns the total count of rows from a table. Returns `0` if query fails.
|
|
2683
|
-
|
|
2684
|
-
```ts
|
|
2685
|
-
const total_users = db.count_table('users');
|
|
2686
|
-
```
|
|
2687
|
-
|
|
2688
|
-
### 🔧 ``db_sqlite.exists(sql: string, ...values: any): boolean``
|
|
2689
|
-
|
|
2690
|
-
Returns `true` if the query returns any results. Returns `false` if no results found or if query fails.
|
|
2691
|
-
|
|
2692
|
-
```ts
|
|
2693
|
-
const has_active_users = db.exists('SELECT 1 FROM users WHERE active = ? LIMIT 1', true);
|
|
2694
|
-
```
|
|
2695
|
-
|
|
2696
|
-
### 🔧 ``db_sqlite.transaction(scope: (transaction: SQLiteDatabaseInterface) => void | Promise<void>): boolean``
|
|
2697
|
-
|
|
2698
|
-
Executes a callback function within a database transaction. The callback receives a transaction object with all the same database methods available. Returns `true` if the transaction was committed successfully, `false` if it was rolled back due to an error.
|
|
2699
|
-
|
|
2700
|
-
```ts
|
|
2701
|
-
const success = db.transaction(async (tx) => {
|
|
2702
|
-
const user_id = tx.insert('INSERT INTO users (name) VALUES (?)', 'John');
|
|
2703
|
-
tx.insert('INSERT INTO user_profiles (user_id, bio) VALUES (?, ?)', user_id, 'Hello world');
|
|
2704
|
-
});
|
|
2705
|
-
|
|
2706
|
-
if (success) {
|
|
2707
|
-
console.log('Transaction completed successfully');
|
|
2708
|
-
} else {
|
|
2709
|
-
console.log('Transaction was rolled back');
|
|
2710
|
-
}
|
|
2711
|
-
```
|
|
2712
|
-
|
|
2713
|
-
<a id="api-database-interface-mysql"></a>
|
|
2714
|
-
## API > Database > Interface > MySQL
|
|
2715
|
-
|
|
2716
|
-
`spooder` provides a simple **MySQL** interface that acts as a wrapper around the `mysql2` API. The connection options match the underlying API.
|
|
2717
|
-
|
|
2718
|
-
> [!IMPORTANT]
|
|
2719
|
-
> 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.
|
|
2720
|
-
|
|
2721
|
-
```ts
|
|
2722
|
-
// see: https://github.com/mysqljs/mysql#connection-options
|
|
2723
|
-
const db = await db_mysql({
|
|
2724
|
-
// ...
|
|
2725
|
-
});
|
|
2726
|
-
db.instance; // raw access to underlying mysql2 instance.
|
|
2727
|
-
```
|
|
2728
|
-
|
|
2729
|
-
### Error Reporting
|
|
2730
|
-
|
|
2731
|
-
In the event of an error from MySQL, an applicable value will be returned from interface functions, rather than the error being thrown.
|
|
2732
|
-
|
|
2733
|
-
```ts
|
|
2734
|
-
const result = await db.get_single('BROKEN QUERY');
|
|
2735
|
-
if (result !== null) {
|
|
2736
|
-
// do more stuff with result
|
|
2737
|
-
}
|
|
2738
|
-
```
|
|
2739
|
-
|
|
2740
|
-
If you have configured the canary reporting feature in spooder, you can instruct the database interface to report errors using this feature with the `use_canary_reporting` parameter.
|
|
2741
|
-
|
|
2742
|
-
```ts
|
|
2743
|
-
const db = await db_mysql({ ... }, false, true);
|
|
2744
|
-
```
|
|
2745
|
-
|
|
2746
|
-
### Pooling
|
|
2747
|
-
|
|
2748
|
-
MySQL supports connection pooling. This can be configured by providing `true` to the `pool` parameter.
|
|
2749
|
-
|
|
2750
|
-
```ts
|
|
2751
|
-
const pool = await db_mysql({ ... }, true);
|
|
2752
|
-
```
|
|
2753
|
-
|
|
2754
|
-
### 🔧 ``db_mysql.update_schema(schema_dir: string, schema_table: string): Promise<void>``
|
|
2755
|
-
|
|
2756
|
-
`spooder` offers a database schema management system. The `update_schema()` function is a shortcut to call this on the underlying database.
|
|
2757
|
-
|
|
2758
|
-
See [API > Database > Schema](#api-database-schema) for information on how schema updating works.
|
|
2759
|
-
|
|
2760
|
-
```ts
|
|
2761
|
-
// without interface
|
|
2762
|
-
import { db_mysql, db_update_schema_mysql } from 'spooder';
|
|
2763
|
-
const db = await db_mysql({ ... });
|
|
2764
|
-
await db_update_schema_mysql(db.instance, './schema');
|
|
2765
|
-
|
|
2766
|
-
// with interface
|
|
2767
|
-
import { db_mysql } from 'spooder';
|
|
2768
|
-
const db = await db_mysql({ ... });
|
|
2769
|
-
await db.update_schema('./schema');
|
|
2770
|
-
```
|
|
2771
|
-
|
|
2772
|
-
### 🔧 ``db_mysql.insert(sql: string, ...values: any): Promise<number>``
|
|
2773
|
-
|
|
2774
|
-
Executes a query and returns the `LAST_INSERT_ID`. Returns `-1` in the event of an error or if `LAST_INSERT_ID` is not provided.
|
|
2775
|
-
|
|
2776
|
-
```ts
|
|
2777
|
-
const id = await db.insert('INSERT INTO tbl (name) VALUES(?)', 'test');
|
|
2778
|
-
```
|
|
2779
|
-
|
|
2780
|
-
### 🔧 ``db_mysql.insert_object(table: string, obj: Record<string, any>): Promise<number>``
|
|
2781
|
-
|
|
2782
|
-
Executes an insert query using object key/value mapping and returns the `LAST_INSERT_ID`. Returns `-1` in the event of an error.
|
|
2783
|
-
|
|
2784
|
-
```ts
|
|
2785
|
-
const id = await db.insert_object('users', { name: 'John', email: 'john@example.com' });
|
|
2786
|
-
```
|
|
2787
|
-
|
|
2788
|
-
### 🔧 ``db_mysql.execute(sql: string, ...values: any): Promise<number>``
|
|
2789
|
-
|
|
2790
|
-
Executes a query and returns the number of affected rows. Returns `-1` in the event of an error.
|
|
2791
|
-
|
|
2792
|
-
```ts
|
|
2793
|
-
const affected = await db.execute('UPDATE users SET name = ? WHERE id = ?', 'Jane', 1);
|
|
2794
|
-
```
|
|
2795
|
-
|
|
2796
|
-
### 🔧 ``db_mysql.get_all<T>(sql: string, ...values: any): Promise<T[]>``
|
|
2535
|
+
const [row] = await sql`SELECT * FROM some_table`;
|
|
2536
|
+
const set = db_set_cast<Fruits>(row.fruits);
|
|
2797
2537
|
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
```ts
|
|
2801
|
-
const users = await db.get_all<User>('SELECT * FROM users WHERE active = ?', true);
|
|
2802
|
-
```
|
|
2803
|
-
|
|
2804
|
-
### 🔧 ``db_mysql.get_single<T>(sql: string, ...values: any): Promise<T | null>``
|
|
2805
|
-
|
|
2806
|
-
Returns the first row from a query result set. Returns `null` if no rows found or if query fails.
|
|
2807
|
-
|
|
2808
|
-
```ts
|
|
2809
|
-
const user = await db.get_single<User>('SELECT * FROM users WHERE id = ?', 1);
|
|
2810
|
-
```
|
|
2811
|
-
|
|
2812
|
-
### 🔧 ``db_mysql.get_column<T>(sql: string, column: string, ...values: any): Promise<T[]>``
|
|
2813
|
-
|
|
2814
|
-
Returns the query result as a single column array. Returns empty array if no rows found or if query fails.
|
|
2815
|
-
|
|
2816
|
-
```ts
|
|
2817
|
-
const names = await db.get_column<string>('SELECT name FROM users', 'name');
|
|
2818
|
-
```
|
|
2819
|
-
|
|
2820
|
-
### 🔧 ``db_mysql.call<T>(func_name: string, ...args: any): Promise<T[]>``
|
|
2821
|
-
|
|
2822
|
-
Calls a stored procedure and returns the result set as an array. Returns empty array if no rows found or if query fails.
|
|
2823
|
-
|
|
2824
|
-
```ts
|
|
2825
|
-
const results = await db.call<User>('get_active_users', true, 10);
|
|
2826
|
-
```
|
|
2827
|
-
|
|
2828
|
-
### 🔧 ``db_mysql.get_paged<T>(sql: string, values?: any[], page_size?: number): AsyncGenerator<T[]>``
|
|
2829
|
-
|
|
2830
|
-
Returns an async iterator that yields pages of database rows. Each page contains at most `page_size` rows (default 1000).
|
|
2831
|
-
|
|
2832
|
-
```ts
|
|
2833
|
-
for await (const page of db.get_paged<User>('SELECT * FROM users', [], 100)) {
|
|
2834
|
-
console.log(`Processing ${page.length} users`);
|
|
2538
|
+
if (set.has(Fruits.Apple)) {
|
|
2539
|
+
// we have an apple in the set
|
|
2835
2540
|
}
|
|
2836
2541
|
```
|
|
2837
2542
|
|
|
2838
|
-
### 🔧 ``
|
|
2839
|
-
|
|
2840
|
-
Returns the value of `count` from a query. Returns `0` if query fails.
|
|
2841
|
-
|
|
2842
|
-
```ts
|
|
2843
|
-
const user_count = await db.count('SELECT COUNT(*) AS count FROM users WHERE active = ?', true);
|
|
2844
|
-
```
|
|
2845
|
-
|
|
2846
|
-
### 🔧 ``db_mysql.count_table(table_name: string): Promise<number>``
|
|
2847
|
-
|
|
2848
|
-
Returns the total count of rows from a table. Returns `0` if query fails.
|
|
2849
|
-
|
|
2850
|
-
```ts
|
|
2851
|
-
const total_users = await db.count_table('users');
|
|
2852
|
-
```
|
|
2853
|
-
|
|
2854
|
-
### 🔧 ``db_mysql.exists(sql: string, ...values: any): Promise<boolean>``
|
|
2543
|
+
### 🔧 ``db_set_serialize<T extends string>(set: Iterable<T> | null): string``
|
|
2855
2544
|
|
|
2856
|
-
|
|
2545
|
+
Takes an `Iterable<T>` and returns a database SET string. If the set is empty or `null`, it returns an empty string.
|
|
2857
2546
|
|
|
2858
2547
|
```ts
|
|
2859
|
-
|
|
2860
|
-
|
|
2548
|
+
enum Fruits {
|
|
2549
|
+
Apple = 'Apple',
|
|
2550
|
+
Banana = 'Banana',
|
|
2551
|
+
Lemon = 'Lemon'
|
|
2552
|
+
};
|
|
2861
2553
|
|
|
2862
|
-
|
|
2554
|
+
// edit existing set
|
|
2555
|
+
const [row] = await sql`SELECT * FROM some_table`;
|
|
2556
|
+
const fruits = db_set_cast<Fruits>(row.fruits);
|
|
2863
2557
|
|
|
2864
|
-
|
|
2558
|
+
if (!fruits.has(Fruits.Lemon))
|
|
2559
|
+
fruits.add(Fruits.Lemon);
|
|
2865
2560
|
|
|
2866
|
-
|
|
2867
|
-
const success = await db.transaction(async (tx) => {
|
|
2868
|
-
const user_id = await tx.insert('INSERT INTO users (name) VALUES (?)', 'John');
|
|
2869
|
-
await tx.insert('INSERT INTO user_profiles (user_id, bio) VALUES (?, ?)', user_id, 'Hello world');
|
|
2870
|
-
});
|
|
2561
|
+
await sql`UPDATE some_table SET fruits = ${sql(db_set_serialize(fruits))} WHERE id = ${row.id}`;
|
|
2871
2562
|
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
} else {
|
|
2875
|
-
console.log('Transaction was rolled back');
|
|
2876
|
-
}
|
|
2563
|
+
// new set from iterable
|
|
2564
|
+
await sql`UPDATE some_table SET fruits = ${sql(db_set_serialize([Fruits.Apple, Fruits.Lemon]))}`;
|
|
2877
2565
|
```
|
|
2878
2566
|
|
|
2879
2567
|
<a id="api-database-schema"></a>
|
|
2880
2568
|
## API > Database > Schema
|
|
2881
2569
|
|
|
2882
|
-
|
|
2570
|
+
### 🔧 ``db_schema(db: SQL, schema_path: string, options?: SchemaOptions): Promise<boolean>``
|
|
2883
2571
|
|
|
2884
|
-
|
|
2885
|
-
// sqlite
|
|
2886
|
-
db_update_schema_sqlite(db: Database, schema_dir: string, schema_table?: string): Promise<void>;
|
|
2887
|
-
|
|
2888
|
-
// mysql
|
|
2889
|
-
db_update_schema_mysql(db: Connection, schema_dir: string, schema_table?: string): Promise<void>;
|
|
2890
|
-
```
|
|
2572
|
+
`db_schema` executes all revisioned `.sql` files in a given directory, applying them to the database incrementally.
|
|
2891
2573
|
|
|
2892
2574
|
```ts
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
import { Database } from 'bun:sqlite';
|
|
2896
|
-
|
|
2897
|
-
const db = new Database('./database.sqlite');
|
|
2898
|
-
await db_update_schema_sqlite(db, './schema');
|
|
2575
|
+
const db = new SQL('db:pw@localhost:3306/test');
|
|
2576
|
+
await db_schema(db, './db/revisions');
|
|
2899
2577
|
```
|
|
2900
2578
|
|
|
2901
|
-
|
|
2902
|
-
// mysql example
|
|
2903
|
-
import { db_update_schema_mysql } from 'spooder';
|
|
2904
|
-
import mysql from 'mysql2';
|
|
2905
|
-
|
|
2906
|
-
const db = await mysql.createConnection({
|
|
2907
|
-
// connection options
|
|
2908
|
-
// see https://github.com/mysqljs/mysql#connection-options
|
|
2909
|
-
});
|
|
2910
|
-
await db_update_schema_mysql(db, './schema');
|
|
2911
|
-
```
|
|
2912
|
-
|
|
2913
|
-
> [!IMPORTANT]
|
|
2914
|
-
> 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.
|
|
2915
|
-
|
|
2916
|
-
### Interface API
|
|
2917
|
-
|
|
2918
|
-
If you are already using the [database interface API](#api-database-interface) provided by `spooder`, you can call `update_schema()` directly on the interface.
|
|
2579
|
+
The above example will **recursively** search the `./db/revisions` directory for all `.sql` files that begin with a positive numeric identifier.
|
|
2919
2580
|
|
|
2920
2581
|
```ts
|
|
2921
|
-
|
|
2922
|
-
|
|
2582
|
+
db/revisions/000_invalid.sql // no: 0 is not valid
|
|
2583
|
+
db/revisions/001_valid.sql // yes: revision 1
|
|
2584
|
+
db/revisions/25-valid.sql // yes: revision 25
|
|
2585
|
+
db/revisions/005_not.txt // no: .sql extension missing
|
|
2586
|
+
db/revisions/invalid_500.sql // no: must begin with rev
|
|
2923
2587
|
```
|
|
2924
2588
|
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
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.
|
|
2928
|
-
|
|
2929
|
-
> [!NOTE]
|
|
2930
|
-
> The schema directory is searched recursively and files without the `.sql` extension (case-insensitive) will be ignored.
|
|
2589
|
+
Revisions are applied in **numerical order**, rather than the file sorting order from the operating system. Invalid files are **skipped** without throwing an error.
|
|
2931
2590
|
|
|
2932
|
-
|
|
2933
|
-
- database.sqlite
|
|
2934
|
-
- schema/
|
|
2935
|
-
- users.sql
|
|
2936
|
-
- posts.sql
|
|
2937
|
-
- comments.sql
|
|
2938
|
-
```
|
|
2591
|
+
By default, schema revision is tracked in a table called `db_schema`. The name of this table can be customized by providing a different `.schema_table` option.
|
|
2939
2592
|
|
|
2940
2593
|
```ts
|
|
2941
|
-
|
|
2942
|
-
import { Database } from 'bun:sqlite';
|
|
2943
|
-
|
|
2944
|
-
const db = new Database('./database.sqlite');
|
|
2945
|
-
await db_update_schema_sqlite(db, './schema');
|
|
2946
|
-
```
|
|
2947
|
-
|
|
2948
|
-
Each of the SQL files should contain all of the revisions for the table, with the first revision being table creation and subsequent revisions being table modifications.
|
|
2949
|
-
|
|
2950
|
-
```sql
|
|
2951
|
-
-- [1] Table creation.
|
|
2952
|
-
CREATE TABLE users (
|
|
2953
|
-
id INTEGER PRIMARY KEY,
|
|
2954
|
-
username TEXT NOT NULL,
|
|
2955
|
-
password TEXT NOT NULL
|
|
2956
|
-
);
|
|
2957
|
-
|
|
2958
|
-
-- [2] Add email column.
|
|
2959
|
-
ALTER TABLE users ADD COLUMN email TEXT;
|
|
2960
|
-
|
|
2961
|
-
-- [3] Cleanup invalid usernames.
|
|
2962
|
-
DELETE FROM users WHERE username = 'admin';
|
|
2963
|
-
DELETE FROM users WHERE username = 'root';
|
|
2594
|
+
await db_schema(db, './db/revisions', { schema_table: 'alt_table_name' });
|
|
2964
2595
|
```
|
|
2965
2596
|
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
>[!NOTE]
|
|
2969
|
-
> The exact revision header syntax is `^--\s*\[(\d+)\]`.
|
|
2970
|
-
|
|
2971
|
-
Everything following a revision header is considered part of that revision until the next revision header or the end of the file, allowing for multiple SQL statements to be included in a single revision.
|
|
2972
|
-
|
|
2973
|
-
When calling `db_update_schema_*`, unapplied revisions will be applied in ascending order (regardless of order within the file) until the schema is up-to-date.
|
|
2974
|
-
|
|
2975
|
-
It is acceptable to omit keys. This can be useful to prevent repitition when managing stored procedures, views or functions.
|
|
2976
|
-
|
|
2977
|
-
```sql
|
|
2978
|
-
-- example of repetitive declaration
|
|
2979
|
-
|
|
2980
|
-
-- [1] create view
|
|
2981
|
-
CREATE VIEW `view_test` AS SELECT * FROM `table_a` WHERE col = 'foo';
|
|
2982
|
-
|
|
2983
|
-
-- [2] change view
|
|
2984
|
-
DROP VIEW IF EXISTS `view_test`;
|
|
2985
|
-
CREATE VIEW `view_test` AS SELECT * FROM `table_b` WHERE col = 'foo';
|
|
2986
|
-
```
|
|
2987
|
-
Instead of unnecessarily including each full revision of a procedure, view or function in the schema file, simply store the most up-to-date one and increment the version.
|
|
2988
|
-
```sql
|
|
2989
|
-
-- [2] create view
|
|
2990
|
-
CREATE OR REPLACE VIEW `view_test` AS SELECT * FROM `table_b` WHERE col = 'foo';
|
|
2991
|
-
```
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
Schema revisions are tracked in a table called `db_schema` which is created automatically if it does not exist with the following schema.
|
|
2995
|
-
|
|
2996
|
-
```sql
|
|
2997
|
-
CREATE TABLE db_schema (
|
|
2998
|
-
db_schema_table_name TEXT PRIMARY KEY,
|
|
2999
|
-
db_schema_version INTEGER
|
|
3000
|
-
);
|
|
3001
|
-
```
|
|
3002
|
-
|
|
3003
|
-
The table used for schema tracking can be changed if necessary by providing an alternative table name as the third paramater.
|
|
2597
|
+
The revision folder is enumerated recursively by default. This can be disabled by passing `false` to `.recursive`, which will only scan the top level of the specified directory.
|
|
3004
2598
|
|
|
3005
2599
|
```ts
|
|
3006
|
-
await
|
|
2600
|
+
await db_schema(db, './db/revisions', { recursive: false });
|
|
3007
2601
|
```
|
|
3008
2602
|
|
|
3009
|
-
|
|
3010
|
-
> The entire process is transactional. If an error occurs during the application of **any** revision for **any** table, the entire process will be rolled back and the database will be left in the state it was before the update was attempted.
|
|
3011
|
-
|
|
3012
|
-
>[!IMPORTANT]
|
|
3013
|
-
> `db_update_schema_*` will throw an error if the revisions cannot be parsed or applied for any reason. It is important you catch and handle appropriately.
|
|
3014
|
-
|
|
3015
|
-
```ts
|
|
3016
|
-
try {
|
|
3017
|
-
const db = new Database('./database.sqlite');
|
|
3018
|
-
await db_update_schema_sqlite(db, './schema');
|
|
3019
|
-
} catch (e) {
|
|
3020
|
-
// panic (crash) or gracefully continue, etc.
|
|
3021
|
-
await panic(e);
|
|
3022
|
-
}
|
|
3023
|
-
```
|
|
2603
|
+
Each revision file is executed within a transaction. In the event of an error, the transaction will be rolled back. Successful revision files executed **before** the error will not be rolled back. Subsequent revision files will **not** be executed after an error.
|
|
3024
2604
|
|
|
3025
|
-
|
|
3026
|
-
|
|
2605
|
+
> [!CAUTION]
|
|
2606
|
+
> Implicit commits, such as those that modify DDL, cannot be rolled back inside a transaction.
|
|
2607
|
+
>
|
|
2608
|
+
> It is recommended to only feature one implicit commit query per revision file. In the event of multiple, an error will not rollback previous implicitly committed queries within the revision, leaving your database in a partial state.
|
|
2609
|
+
>
|
|
2610
|
+
> See [MySQL 8.4 Reference Manual // 15.3.3 Statements That Cause an Implicit Commit](https://dev.mysql.com/doc/refman/8.4/en/implicit-commit.html) for more information.
|
|
3027
2611
|
|
|
3028
|
-
If a specific revision depends on one or more other schema files to be executed before it (for example, when adding foreign keys), you can specify dependencies at the revision level.
|
|
3029
2612
|
|
|
3030
|
-
```
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
);
|
|
2613
|
+
```ts
|
|
2614
|
+
type SchemaOptions = {
|
|
2615
|
+
schema_table: string;
|
|
2616
|
+
recursive: boolean;
|
|
2617
|
+
};
|
|
3036
2618
|
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
ALTER TABLE table_a ADD COLUMN table_b_id INTEGER REFERENCES table_b(id);
|
|
2619
|
+
db_get_schema_revision(db: SQL): Promise<number|null>;
|
|
2620
|
+
db_schema(db: SQL, schema_path: string, options?: SchemaOptions): Promise<boolean>;
|
|
3040
2621
|
```
|
|
3041
2622
|
|
|
3042
|
-
When a revision specifies dependencies, all revisions of the dependent schema files will be executed before that specific revision runs. This allows you to create tables independently and then add dependencies in later revisions.
|
|
3043
|
-
|
|
3044
|
-
>[!IMPORTANT]
|
|
3045
|
-
> Dependencies are specified per-revision, not per-file. A `-- [deps]` line applies only to the revision it appears in.
|
|
3046
|
-
|
|
3047
|
-
>[!IMPORTANT]
|
|
3048
|
-
> Cyclic or missing dependencies will throw an error.
|
|
3049
|
-
|
|
3050
2623
|
<a id="api-utilities"></a>
|
|
3051
2624
|
## API > Utilities
|
|
3052
2625
|
|