@grupodiariodaregiao/bunstone 0.7.0 → 0.7.2
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/AGENTS.md +124 -0
- package/dist/index.d.ts +22 -21
- package/dist/index.js +104486 -104347
- package/dist/lib/app-startup.d.ts +1 -0
- package/dist/lib/database/sql-module.d.ts +30 -10
- package/dist/lib/errors/index.d.ts +3 -1
- package/docs/database-sql.md +62 -0
- package/docs/pt-BR/database-sql.md +62 -0
- package/lib/app-startup.ts +34 -2
- package/lib/database/sql-module.ts +271 -37
- package/lib/errors/index.ts +14 -1
- package/package.json +1 -1
|
@@ -86,6 +86,7 @@ export declare class AppStartup {
|
|
|
86
86
|
private static buildEffectiveRateLimit;
|
|
87
87
|
private static registerTimeouts;
|
|
88
88
|
private static registerCronJobs;
|
|
89
|
+
private static invokeScheduledHandler;
|
|
89
90
|
private static registerBullMqWorkers;
|
|
90
91
|
/**
|
|
91
92
|
* Sets up RabbitMQ consumers for every provider that has `@RabbitSubscribe`
|
|
@@ -1,19 +1,36 @@
|
|
|
1
1
|
import { SQL } from "bun";
|
|
2
|
-
type
|
|
2
|
+
type Provider = "postgresql" | "mysql" | "sqlite";
|
|
3
|
+
type BunSqlPoolOptions = Pick<SQL.PostgresOrMySQLOptions, "max" | "maxLifetime" | "connectionTimeout" | "idleTimeout" | "connection" | "tls" | "prepare" | "bigint" | "onconnect" | "onclose" | "path">;
|
|
4
|
+
type BunSqliteClientOptions = Pick<SQL.SQLiteOptions, "readonly" | "create" | "safeIntegers" | "strict">;
|
|
5
|
+
/** Options forwarded by Bunstone to the underlying Bun.SQL client. */
|
|
6
|
+
export type BunSqlClientOptions = Partial<BunSqlPoolOptions & BunSqliteClientOptions>;
|
|
7
|
+
/**
|
|
8
|
+
* Options accepted by {@link SqlModule.register}.
|
|
9
|
+
* Only Bunstone-supported settings are allowed.
|
|
10
|
+
*/
|
|
11
|
+
export type SqlModuleOptions = BunSqlClientOptions & {
|
|
12
|
+
/**
|
|
13
|
+
* Timezone used for date/time interpretation on the database connection.
|
|
14
|
+
* Mapped to driver `connection` settings when not provided explicitly.
|
|
15
|
+
* Defaults to `UTC`.
|
|
16
|
+
*/
|
|
17
|
+
timezone?: string;
|
|
18
|
+
};
|
|
19
|
+
export type SqlConnectionDetails = {
|
|
3
20
|
host: string;
|
|
4
21
|
port: number;
|
|
5
22
|
username: string;
|
|
6
23
|
password: string;
|
|
7
24
|
database: string;
|
|
8
|
-
provider:
|
|
9
|
-
/**
|
|
10
|
-
* Timezone used for date/time interpretation on the database connection.
|
|
11
|
-
* Defaults to 'UTC' to ensure consistent, offset-free date handling.
|
|
12
|
-
* Set to 'local' to use the process timezone, or any valid tz identifier.
|
|
13
|
-
*/
|
|
14
|
-
timezone?: string;
|
|
25
|
+
provider: Provider;
|
|
15
26
|
};
|
|
16
|
-
type
|
|
27
|
+
export type ConnectionOptions = SqlConnectionDetails & SqlModuleOptions;
|
|
28
|
+
/** @deprecated Use {@link ConnectionOptions} */
|
|
29
|
+
export type SqlConnectionOptions = ConnectionOptions;
|
|
30
|
+
/** @deprecated Use {@link SqlModuleOptions} */
|
|
31
|
+
export type SqlRegisterOptions = SqlModuleOptions;
|
|
32
|
+
/** Pool-related subset of {@link SqlModuleOptions}. */
|
|
33
|
+
export type SqlPoolOptions = Pick<SqlModuleOptions, "max" | "maxLifetime" | "connectionTimeout" | "idleTimeout">;
|
|
17
34
|
export declare class SqlService {
|
|
18
35
|
query<T = any>(query: string, params?: any[]): Promise<T[]>;
|
|
19
36
|
transaction(queries: Array<{
|
|
@@ -21,13 +38,16 @@ export declare class SqlService {
|
|
|
21
38
|
params?: any[];
|
|
22
39
|
}>): Promise<void>;
|
|
23
40
|
bulkInsert<T = any>(table: string, values: T[]): Promise<void>;
|
|
41
|
+
private executeQuery;
|
|
42
|
+
private executeTransaction;
|
|
43
|
+
private executeBulkInsert;
|
|
24
44
|
private getSqlInstance;
|
|
25
45
|
}
|
|
26
46
|
export declare class SqlModule {
|
|
27
47
|
private static sqlInstance;
|
|
28
48
|
private static provider;
|
|
29
49
|
static register(connection: ConnectionOptions): typeof SqlModule;
|
|
30
|
-
static register(connection: string,
|
|
50
|
+
static register(connection: string, options?: SqlModuleOptions | string): typeof SqlModule;
|
|
31
51
|
static getSqlInstance(): SQL;
|
|
32
52
|
static getProvider(): Provider | undefined;
|
|
33
53
|
}
|
|
@@ -75,10 +75,12 @@ export declare class CqrsError extends BunstoneError {
|
|
|
75
75
|
* Codes:
|
|
76
76
|
* - `BNS-DB-001` – SQL instance not initialised
|
|
77
77
|
* - `BNS-DB-002` – query / transaction execution failed
|
|
78
|
+
* - `BNS-DB-003` – invalid SqlModule configuration option
|
|
78
79
|
*/
|
|
79
80
|
export declare class DatabaseError extends BunstoneError {
|
|
80
|
-
constructor(message: string, code?: "BNS-DB-001" | "BNS-DB-002", suggestion?: string, context?: Record<string, unknown>, cause?: Error);
|
|
81
|
+
constructor(message: string, code?: "BNS-DB-001" | "BNS-DB-002" | "BNS-DB-003", suggestion?: string, context?: Record<string, unknown>, cause?: Error);
|
|
81
82
|
static notInitialized(): DatabaseError;
|
|
83
|
+
static invalidConfig(invalidKeys: string[], allowedKeys: readonly string[]): DatabaseError;
|
|
82
84
|
}
|
|
83
85
|
/**
|
|
84
86
|
* Thrown when BullMQ operations fail.
|
package/docs/database-sql.md
CHANGED
|
@@ -43,6 +43,68 @@ OR using a connection string:
|
|
|
43
43
|
export class AppModule {}
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
### Connection options
|
|
47
|
+
|
|
48
|
+
You can pass a second argument with pool and client settings. Bunstone forwards only the supported options to [Bun.SQL](https://bun.sh/docs/api/sql):
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
@Module({
|
|
52
|
+
imports: [
|
|
53
|
+
SqlModule.register("mysql://user:pass@host:3306/db", {
|
|
54
|
+
maxLifetime: 25200,
|
|
55
|
+
connectionTimeout: 30,
|
|
56
|
+
max: 10,
|
|
57
|
+
timezone: "UTC",
|
|
58
|
+
}),
|
|
59
|
+
],
|
|
60
|
+
})
|
|
61
|
+
export class AppModule {}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The same options can be combined with the object-style registration:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
SqlModule.register({
|
|
68
|
+
provider: "postgresql",
|
|
69
|
+
host: "localhost",
|
|
70
|
+
port: 5432,
|
|
71
|
+
username: "user",
|
|
72
|
+
password: "password",
|
|
73
|
+
database: "my_db",
|
|
74
|
+
max: 20,
|
|
75
|
+
idleTimeout: 30,
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
For backward compatibility, the second argument may still be a timezone string:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
SqlModule.register("mysql://user:pass@host/db", "UTC");
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
#### Supported options (`SqlModuleOptions`)
|
|
86
|
+
|
|
87
|
+
| Option | Description |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `timezone` | Bunstone helper for date/time handling. Defaults to `UTC`. Mapped to driver `connection` settings. |
|
|
90
|
+
| `max` | Maximum connections in the pool |
|
|
91
|
+
| `maxLifetime` | Maximum connection lifetime in seconds |
|
|
92
|
+
| `connectionTimeout` | Timeout when establishing a connection (seconds) |
|
|
93
|
+
| `idleTimeout` | Close idle connections after N seconds |
|
|
94
|
+
| `connection` | Driver-specific runtime settings (e.g. PostgreSQL `TimeZone`) |
|
|
95
|
+
| `tls` | TLS/SSL configuration |
|
|
96
|
+
| `prepare` | Enable automatic prepared statements |
|
|
97
|
+
| `bigint` | Return out-of-range integers as `BigInt` |
|
|
98
|
+
| `onconnect` | Callback when a connection attempt completes |
|
|
99
|
+
| `onclose` | Callback when a connection closes |
|
|
100
|
+
| `path` | Unix domain socket path |
|
|
101
|
+
| `readonly` | SQLite read-only mode |
|
|
102
|
+
| `create` | SQLite create-if-missing behavior |
|
|
103
|
+
| `safeIntegers` | SQLite safe integer handling |
|
|
104
|
+
| `strict` | SQLite strict mode |
|
|
105
|
+
|
|
106
|
+
Only the options listed above are accepted. Invalid keys are rejected by TypeScript and also throw `DatabaseError` (`BNS-DB-003`) at runtime.
|
|
107
|
+
|
|
46
108
|
## Usage
|
|
47
109
|
|
|
48
110
|
Once registered, the `SqlService` is globally available. You can inject it into any controller or provider without needing to import `SqlModule` into subsequent modules.
|
|
@@ -43,6 +43,68 @@ OU usando uma string de conexão:
|
|
|
43
43
|
export class AppModule {}
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
### Opções de conexão
|
|
47
|
+
|
|
48
|
+
Você pode passar um segundo argumento com configurações de pool e do client. O Bunstone repassa apenas as opções suportadas para o [Bun.SQL](https://bun.sh/docs/api/sql):
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
@Module({
|
|
52
|
+
imports: [
|
|
53
|
+
SqlModule.register("mysql://user:pass@host:3306/db", {
|
|
54
|
+
maxLifetime: 25200,
|
|
55
|
+
connectionTimeout: 30,
|
|
56
|
+
max: 10,
|
|
57
|
+
timezone: "UTC",
|
|
58
|
+
}),
|
|
59
|
+
],
|
|
60
|
+
})
|
|
61
|
+
export class AppModule {}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
As mesmas opções podem ser usadas no registro via objeto:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
SqlModule.register({
|
|
68
|
+
provider: "postgresql",
|
|
69
|
+
host: "localhost",
|
|
70
|
+
port: 5432,
|
|
71
|
+
username: "user",
|
|
72
|
+
password: "password",
|
|
73
|
+
database: "my_db",
|
|
74
|
+
max: 20,
|
|
75
|
+
idleTimeout: 30,
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Por compatibilidade, o segundo argumento ainda pode ser uma string de timezone:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
SqlModule.register("mysql://user:pass@host/db", "UTC");
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
#### Opções suportadas (`SqlModuleOptions`)
|
|
86
|
+
|
|
87
|
+
| Opção | Descrição |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `timezone` | Helper do Bunstone para datas/horas. Padrão: `UTC`. Mapeado para `connection` do driver. |
|
|
90
|
+
| `max` | Máximo de conexões no pool |
|
|
91
|
+
| `maxLifetime` | Tempo máximo de vida da conexão em segundos |
|
|
92
|
+
| `connectionTimeout` | Timeout ao estabelecer conexão (segundos) |
|
|
93
|
+
| `idleTimeout` | Fecha conexões ociosas após N segundos |
|
|
94
|
+
| `connection` | Configurações runtime do driver (ex.: `TimeZone` no PostgreSQL) |
|
|
95
|
+
| `tls` | Configuração TLS/SSL |
|
|
96
|
+
| `prepare` | Habilita prepared statements automáticos |
|
|
97
|
+
| `bigint` | Retorna inteiros fora do range como `BigInt` |
|
|
98
|
+
| `onconnect` | Callback ao concluir tentativa de conexão |
|
|
99
|
+
| `onclose` | Callback ao fechar conexão |
|
|
100
|
+
| `path` | Caminho do Unix domain socket |
|
|
101
|
+
| `readonly` | Modo somente leitura (SQLite) |
|
|
102
|
+
| `create` | Comportamento de criação do arquivo (SQLite) |
|
|
103
|
+
| `safeIntegers` | Tratamento seguro de inteiros (SQLite) |
|
|
104
|
+
| `strict` | Modo strict (SQLite) |
|
|
105
|
+
|
|
106
|
+
Somente as opções listadas acima são aceitas. Chaves inválidas são rejeitadas pelo TypeScript e também lançam `DatabaseError` (`BNS-DB-003`) em runtime.
|
|
107
|
+
|
|
46
108
|
## Uso
|
|
47
109
|
|
|
48
110
|
Depois de registrado, o `SqlService` fica disponível globalmente. Você pode injetá-lo em qualquer controller ou provider sem precisar importar o `SqlModule` nos módulos subsequentes.
|
package/lib/app-startup.ts
CHANGED
|
@@ -906,7 +906,11 @@ export class AppStartup {
|
|
|
906
906
|
`Scheduling timeout for method: ${timeout.methodName} with delay: ${timeout.delay}ms`,
|
|
907
907
|
);
|
|
908
908
|
setTimeout(() => {
|
|
909
|
-
|
|
909
|
+
void AppStartup.invokeScheduledHandler(
|
|
910
|
+
provider,
|
|
911
|
+
timeout.methodName,
|
|
912
|
+
"timeout",
|
|
913
|
+
);
|
|
910
914
|
}, timeout.delay);
|
|
911
915
|
}
|
|
912
916
|
}
|
|
@@ -935,12 +939,40 @@ export class AppStartup {
|
|
|
935
939
|
for (const cron of crons) {
|
|
936
940
|
AppStartup.logger.log(`Scheduling cron for method: ${cron.methodName}`);
|
|
937
941
|
scheduler.schedule(cron.expression, () => {
|
|
938
|
-
|
|
942
|
+
void AppStartup.invokeScheduledHandler(
|
|
943
|
+
provider,
|
|
944
|
+
cron.methodName,
|
|
945
|
+
"cron",
|
|
946
|
+
);
|
|
939
947
|
});
|
|
940
948
|
}
|
|
941
949
|
}
|
|
942
950
|
}
|
|
943
951
|
|
|
952
|
+
private static async invokeScheduledHandler(
|
|
953
|
+
provider: Record<string, unknown>,
|
|
954
|
+
methodName: string,
|
|
955
|
+
kind: "cron" | "timeout",
|
|
956
|
+
): Promise<void> {
|
|
957
|
+
const handler = provider[methodName];
|
|
958
|
+
if (typeof handler !== "function") {
|
|
959
|
+
AppStartup.logger.error(
|
|
960
|
+
`${kind === "cron" ? "Cron" : "Timeout"} job "${methodName}" is not a method on the provider`,
|
|
961
|
+
);
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
try {
|
|
966
|
+
const result = handler.call(provider);
|
|
967
|
+
await Promise.resolve(result);
|
|
968
|
+
} catch (err) {
|
|
969
|
+
AppStartup.logger.error(
|
|
970
|
+
`${kind === "cron" ? "Cron" : "Timeout"} job "${methodName}" failed`,
|
|
971
|
+
err,
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
944
976
|
private static registerBullMqWorkers(module: any) {
|
|
945
977
|
const providersBullMq: Map<any, { name?: string; methodName: string }[]> =
|
|
946
978
|
Reflect.getMetadata("dip:bullmq", module);
|
|
@@ -9,22 +9,133 @@ import { DatabaseError } from "../errors";
|
|
|
9
9
|
import { Injectable } from "../injectable";
|
|
10
10
|
import { Module } from "../module";
|
|
11
11
|
|
|
12
|
-
type
|
|
12
|
+
type Provider = "postgresql" | "mysql" | "sqlite";
|
|
13
|
+
|
|
14
|
+
type BunSqlPoolOptions = Pick<
|
|
15
|
+
SQL.PostgresOrMySQLOptions,
|
|
16
|
+
| "max"
|
|
17
|
+
| "maxLifetime"
|
|
18
|
+
| "connectionTimeout"
|
|
19
|
+
| "idleTimeout"
|
|
20
|
+
| "connection"
|
|
21
|
+
| "tls"
|
|
22
|
+
| "prepare"
|
|
23
|
+
| "bigint"
|
|
24
|
+
| "onconnect"
|
|
25
|
+
| "onclose"
|
|
26
|
+
| "path"
|
|
27
|
+
>;
|
|
28
|
+
|
|
29
|
+
type BunSqliteClientOptions = Pick<
|
|
30
|
+
SQL.SQLiteOptions,
|
|
31
|
+
"readonly" | "create" | "safeIntegers" | "strict"
|
|
32
|
+
>;
|
|
33
|
+
|
|
34
|
+
/** Options forwarded by Bunstone to the underlying Bun.SQL client. */
|
|
35
|
+
export type BunSqlClientOptions = Partial<
|
|
36
|
+
BunSqlPoolOptions & BunSqliteClientOptions
|
|
37
|
+
>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Options accepted by {@link SqlModule.register}.
|
|
41
|
+
* Only Bunstone-supported settings are allowed.
|
|
42
|
+
*/
|
|
43
|
+
export type SqlModuleOptions = BunSqlClientOptions & {
|
|
44
|
+
/**
|
|
45
|
+
* Timezone used for date/time interpretation on the database connection.
|
|
46
|
+
* Mapped to driver `connection` settings when not provided explicitly.
|
|
47
|
+
* Defaults to `UTC`.
|
|
48
|
+
*/
|
|
49
|
+
timezone?: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type SqlConnectionDetails = {
|
|
13
53
|
host: string;
|
|
14
54
|
port: number;
|
|
15
55
|
username: string;
|
|
16
56
|
password: string;
|
|
17
57
|
database: string;
|
|
18
|
-
provider:
|
|
19
|
-
/**
|
|
20
|
-
* Timezone used for date/time interpretation on the database connection.
|
|
21
|
-
* Defaults to 'UTC' to ensure consistent, offset-free date handling.
|
|
22
|
-
* Set to 'local' to use the process timezone, or any valid tz identifier.
|
|
23
|
-
*/
|
|
24
|
-
timezone?: string;
|
|
58
|
+
provider: Provider;
|
|
25
59
|
};
|
|
26
60
|
|
|
27
|
-
type
|
|
61
|
+
export type ConnectionOptions = SqlConnectionDetails & SqlModuleOptions;
|
|
62
|
+
|
|
63
|
+
/** @deprecated Use {@link ConnectionOptions} */
|
|
64
|
+
export type SqlConnectionOptions = ConnectionOptions;
|
|
65
|
+
|
|
66
|
+
/** @deprecated Use {@link SqlModuleOptions} */
|
|
67
|
+
export type SqlRegisterOptions = SqlModuleOptions;
|
|
68
|
+
|
|
69
|
+
/** Pool-related subset of {@link SqlModuleOptions}. */
|
|
70
|
+
export type SqlPoolOptions = Pick<
|
|
71
|
+
SqlModuleOptions,
|
|
72
|
+
"max" | "maxLifetime" | "connectionTimeout" | "idleTimeout"
|
|
73
|
+
>;
|
|
74
|
+
|
|
75
|
+
type SqlClientInitOptions = Omit<
|
|
76
|
+
SqlModuleOptions,
|
|
77
|
+
"url" | "filename" | "timezone"
|
|
78
|
+
>;
|
|
79
|
+
|
|
80
|
+
const SQL_MODULE_OPTION_KEYS = [
|
|
81
|
+
"timezone",
|
|
82
|
+
"max",
|
|
83
|
+
"maxLifetime",
|
|
84
|
+
"connectionTimeout",
|
|
85
|
+
"idleTimeout",
|
|
86
|
+
"connection",
|
|
87
|
+
"tls",
|
|
88
|
+
"prepare",
|
|
89
|
+
"bigint",
|
|
90
|
+
"onconnect",
|
|
91
|
+
"onclose",
|
|
92
|
+
"path",
|
|
93
|
+
"readonly",
|
|
94
|
+
"create",
|
|
95
|
+
"safeIntegers",
|
|
96
|
+
"strict",
|
|
97
|
+
] as const satisfies readonly (keyof SqlModuleOptions)[];
|
|
98
|
+
|
|
99
|
+
const SQL_CONNECTION_DETAIL_KEYS = [
|
|
100
|
+
"host",
|
|
101
|
+
"port",
|
|
102
|
+
"username",
|
|
103
|
+
"password",
|
|
104
|
+
"database",
|
|
105
|
+
"provider",
|
|
106
|
+
] as const satisfies readonly (keyof SqlConnectionDetails)[];
|
|
107
|
+
|
|
108
|
+
const CONNECTION_CLOSED_CODES = new Set([
|
|
109
|
+
"ERR_MYSQL_CONNECTION_CLOSED",
|
|
110
|
+
"ERR_POSTGRES_CONNECTION_CLOSED",
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
function assertOnlyAllowedKeys(
|
|
114
|
+
source: Record<string, unknown>,
|
|
115
|
+
allowedKeys: readonly string[],
|
|
116
|
+
): void {
|
|
117
|
+
const invalidKeys = Object.keys(source).filter(
|
|
118
|
+
(key) => !allowedKeys.includes(key),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
if (invalidKeys.length > 0) {
|
|
122
|
+
throw DatabaseError.invalidConfig(invalidKeys, allowedKeys);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function pickSqlModuleOptions(
|
|
127
|
+
source: Record<string, unknown>,
|
|
128
|
+
): SqlModuleOptions {
|
|
129
|
+
assertOnlyAllowedKeys(source, SQL_MODULE_OPTION_KEYS);
|
|
130
|
+
|
|
131
|
+
const picked = {} as SqlModuleOptions;
|
|
132
|
+
for (const key of SQL_MODULE_OPTION_KEYS) {
|
|
133
|
+
if (key in source) {
|
|
134
|
+
(picked as Record<string, unknown>)[key] = source[key];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return picked;
|
|
138
|
+
}
|
|
28
139
|
|
|
29
140
|
function detectProvider(url: string): Provider {
|
|
30
141
|
if (url.startsWith("mysql://") || url.startsWith("mysql2://")) {
|
|
@@ -45,21 +156,146 @@ function detectProvider(url: string): Provider {
|
|
|
45
156
|
function buildConnectionConfig(
|
|
46
157
|
provider: Provider,
|
|
47
158
|
timezone: string,
|
|
48
|
-
):
|
|
159
|
+
): NonNullable<SQL.PostgresOrMySQLOptions["connection"]> | undefined {
|
|
49
160
|
if (provider === "postgresql") {
|
|
50
161
|
return { TimeZone: timezone };
|
|
51
162
|
}
|
|
52
163
|
if (provider === "mysql") {
|
|
53
|
-
// Normalise to MySQL's expected offset format ('+00:00') or named zone
|
|
54
164
|
const tz = timezone.toLowerCase() === "utc" ? "+00:00" : timezone;
|
|
55
165
|
return { time_zone: tz };
|
|
56
166
|
}
|
|
57
167
|
return undefined;
|
|
58
168
|
}
|
|
59
169
|
|
|
170
|
+
function normalizeRegisterOptions(
|
|
171
|
+
options?: SqlModuleOptions | string,
|
|
172
|
+
): SqlModuleOptions {
|
|
173
|
+
if (typeof options === "string") {
|
|
174
|
+
return { timezone: options };
|
|
175
|
+
}
|
|
176
|
+
if (!options) {
|
|
177
|
+
return {};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return pickSqlModuleOptions(options as Record<string, unknown>);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function buildSqlClientOptions(
|
|
184
|
+
provider: Provider,
|
|
185
|
+
timezone: string,
|
|
186
|
+
options: SqlModuleOptions,
|
|
187
|
+
): SqlClientInitOptions {
|
|
188
|
+
const { timezone: _timezone, connection: userConnection, ...rest } = options;
|
|
189
|
+
const driverConnectionConfig = buildConnectionConfig(provider, timezone);
|
|
190
|
+
const connection = {
|
|
191
|
+
...driverConnectionConfig,
|
|
192
|
+
...userConnection,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
...rest,
|
|
197
|
+
...(Object.keys(connection).length > 0 && { connection }),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function resolveConnectionUrl(connection: string | ConnectionOptions): string {
|
|
202
|
+
if (typeof connection === "string") {
|
|
203
|
+
return connection;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return `${connection.provider}://${connection.username}:${connection.password}@${connection.host}:${connection.port}/${connection.database}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function resolveProvider(connection: string | ConnectionOptions): Provider {
|
|
210
|
+
return typeof connection === "string"
|
|
211
|
+
? detectProvider(connection)
|
|
212
|
+
: connection.provider;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function resolveTimezone(
|
|
216
|
+
connection: string | ConnectionOptions,
|
|
217
|
+
options: SqlModuleOptions,
|
|
218
|
+
): string {
|
|
219
|
+
if (options.timezone) {
|
|
220
|
+
return options.timezone;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (typeof connection === "object" && connection.timezone) {
|
|
224
|
+
return connection.timezone;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return "UTC";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function resolveRegisterOptions(
|
|
231
|
+
connection: string | ConnectionOptions,
|
|
232
|
+
options?: SqlModuleOptions | string,
|
|
233
|
+
): SqlModuleOptions {
|
|
234
|
+
const normalized = normalizeRegisterOptions(options);
|
|
235
|
+
|
|
236
|
+
if (typeof connection !== "object") {
|
|
237
|
+
return normalized;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
assertOnlyAllowedKeys(
|
|
241
|
+
connection as Record<string, unknown>,
|
|
242
|
+
[...SQL_CONNECTION_DETAIL_KEYS, ...SQL_MODULE_OPTION_KEYS],
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
const {
|
|
246
|
+
host: _host,
|
|
247
|
+
port: _port,
|
|
248
|
+
username: _username,
|
|
249
|
+
password: _password,
|
|
250
|
+
database: _database,
|
|
251
|
+
provider: _provider,
|
|
252
|
+
...connectionSqlOptions
|
|
253
|
+
} = connection;
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
...pickSqlModuleOptions(connectionSqlOptions as Record<string, unknown>),
|
|
257
|
+
...normalized,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function isConnectionClosedError(err: unknown): boolean {
|
|
262
|
+
return (
|
|
263
|
+
typeof err === "object" &&
|
|
264
|
+
err !== null &&
|
|
265
|
+
"code" in err &&
|
|
266
|
+
CONNECTION_CLOSED_CODES.has(String((err as { code: unknown }).code))
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function withConnectionRetry<T>(fn: () => Promise<T>): Promise<T> {
|
|
271
|
+
try {
|
|
272
|
+
return await fn();
|
|
273
|
+
} catch (err) {
|
|
274
|
+
if (!isConnectionClosedError(err)) {
|
|
275
|
+
throw err;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return await fn();
|
|
280
|
+
}
|
|
281
|
+
|
|
60
282
|
@Injectable()
|
|
61
283
|
export class SqlService {
|
|
62
284
|
async query<T = any>(query: string, params?: any[]): Promise<T[]> {
|
|
285
|
+
return withConnectionRetry(() => this.executeQuery<T>(query, params));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async transaction(
|
|
289
|
+
queries: Array<{ query: string; params?: any[] }>,
|
|
290
|
+
): Promise<void> {
|
|
291
|
+
return withConnectionRetry(() => this.executeTransaction(queries));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async bulkInsert<T = any>(table: string, values: T[]): Promise<void> {
|
|
295
|
+
return withConnectionRetry(() => this.executeBulkInsert(table, values));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private async executeQuery<T>(query: string, params?: any[]): Promise<T[]> {
|
|
63
299
|
const sql = this.getSqlInstance();
|
|
64
300
|
const tracer = trace.getTracer("bunstone.db");
|
|
65
301
|
const operation = detectOperation(query);
|
|
@@ -91,7 +327,7 @@ export class SqlService {
|
|
|
91
327
|
);
|
|
92
328
|
}
|
|
93
329
|
|
|
94
|
-
async
|
|
330
|
+
private async executeTransaction(
|
|
95
331
|
queries: Array<{ query: string; params?: any[] }>,
|
|
96
332
|
): Promise<void> {
|
|
97
333
|
const sql = this.getSqlInstance();
|
|
@@ -127,7 +363,10 @@ export class SqlService {
|
|
|
127
363
|
);
|
|
128
364
|
}
|
|
129
365
|
|
|
130
|
-
async
|
|
366
|
+
private async executeBulkInsert<T>(
|
|
367
|
+
table: string,
|
|
368
|
+
values: T[],
|
|
369
|
+
): Promise<void> {
|
|
131
370
|
const sql = this.getSqlInstance();
|
|
132
371
|
const tracer = trace.getTracer("bunstone.db");
|
|
133
372
|
const span = tracer.startSpan("db.INSERT", {
|
|
@@ -176,31 +415,30 @@ export class SqlModule {
|
|
|
176
415
|
private static provider: Provider | undefined;
|
|
177
416
|
|
|
178
417
|
static register(connection: ConnectionOptions): typeof SqlModule;
|
|
179
|
-
static register(
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
418
|
+
static register(
|
|
419
|
+
connection: string,
|
|
420
|
+
options?: SqlModuleOptions | string,
|
|
421
|
+
): typeof SqlModule;
|
|
422
|
+
static register(
|
|
423
|
+
connection: string | ConnectionOptions,
|
|
424
|
+
options?: SqlModuleOptions | string,
|
|
425
|
+
) {
|
|
426
|
+
const resolvedOptions = resolveRegisterOptions(connection, options);
|
|
427
|
+
const url = resolveConnectionUrl(connection);
|
|
428
|
+
const provider = resolveProvider(connection);
|
|
429
|
+
const timezone = resolveTimezone(connection, resolvedOptions);
|
|
430
|
+
const sqlOptions = buildSqlClientOptions(
|
|
431
|
+
provider,
|
|
432
|
+
timezone,
|
|
433
|
+
resolvedOptions,
|
|
434
|
+
);
|
|
190
435
|
|
|
191
436
|
SqlModule.provider = provider;
|
|
192
437
|
|
|
193
|
-
const tz =
|
|
194
|
-
typeof connection === "string"
|
|
195
|
-
? (timezone ?? "UTC")
|
|
196
|
-
: (connection.timezone ?? "UTC");
|
|
197
|
-
|
|
198
|
-
const connectionConfig = buildConnectionConfig(provider, tz);
|
|
199
|
-
|
|
200
438
|
SqlModule.sqlInstance = new SQL({
|
|
201
439
|
url,
|
|
202
|
-
...
|
|
203
|
-
});
|
|
440
|
+
...sqlOptions,
|
|
441
|
+
} as SQL.Options);
|
|
204
442
|
|
|
205
443
|
return SqlModule;
|
|
206
444
|
}
|
|
@@ -236,10 +474,6 @@ function detectOperation(query: string): string {
|
|
|
236
474
|
return "QUERY";
|
|
237
475
|
}
|
|
238
476
|
|
|
239
|
-
/**
|
|
240
|
-
* Strips parameter values from SQL for safe inclusion in telemetry.
|
|
241
|
-
* Keeps the query structure readable without leaking user data.
|
|
242
|
-
*/
|
|
243
477
|
function sanitizeSql(query: string): string {
|
|
244
478
|
return query
|
|
245
479
|
.replace(/'[^']*'/g, "'?'")
|
package/lib/errors/index.ts
CHANGED
|
@@ -244,11 +244,12 @@ export class CqrsError extends BunstoneError {
|
|
|
244
244
|
* Codes:
|
|
245
245
|
* - `BNS-DB-001` – SQL instance not initialised
|
|
246
246
|
* - `BNS-DB-002` – query / transaction execution failed
|
|
247
|
+
* - `BNS-DB-003` – invalid SqlModule configuration option
|
|
247
248
|
*/
|
|
248
249
|
export class DatabaseError extends BunstoneError {
|
|
249
250
|
constructor(
|
|
250
251
|
message: string,
|
|
251
|
-
code: "BNS-DB-001" | "BNS-DB-002" = "BNS-DB-001",
|
|
252
|
+
code: "BNS-DB-001" | "BNS-DB-002" | "BNS-DB-003" = "BNS-DB-001",
|
|
252
253
|
suggestion?: string,
|
|
253
254
|
context?: Record<string, unknown>,
|
|
254
255
|
cause?: Error,
|
|
@@ -266,6 +267,18 @@ export class DatabaseError extends BunstoneError {
|
|
|
266
267
|
].join("\n "),
|
|
267
268
|
);
|
|
268
269
|
}
|
|
270
|
+
|
|
271
|
+
static invalidConfig(
|
|
272
|
+
invalidKeys: string[],
|
|
273
|
+
allowedKeys: readonly string[],
|
|
274
|
+
): DatabaseError {
|
|
275
|
+
return new DatabaseError(
|
|
276
|
+
`Invalid SqlModule option(s): ${invalidKeys.join(", ")}.`,
|
|
277
|
+
"BNS-DB-003",
|
|
278
|
+
`Use only supported options: ${allowedKeys.join(", ")}.`,
|
|
279
|
+
{ invalidKeys, allowedKeys },
|
|
280
|
+
);
|
|
281
|
+
}
|
|
269
282
|
}
|
|
270
283
|
|
|
271
284
|
// ─── BullMQ ───────────────────────────────────────────────────────────────────
|