squilo 0.1.2 → 0.2.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 +265 -36
- package/package.json +17 -12
- package/src/.DS_Store +0 -0
- package/src/index.ts +3 -1
- package/src/pipes/auth/index.ts +2 -12
- package/src/pipes/auth/strategies/msal.ts +14 -7
- package/src/pipes/auth/strategies/userAndPassword.ts +1 -1
- package/src/pipes/auth/types.ts +12 -0
- package/src/pipes/connect/index.ts +25 -42
- package/src/pipes/connect/types.ts +25 -0
- package/src/pipes/execute/index.ts +36 -4
- package/src/pipes/index.ts +1 -1
- package/src/pipes/input/index.ts +6 -11
- package/src/pipes/input/types.ts +7 -0
- package/src/pipes/output/index.ts +4 -3
- package/src/pipes/output/strategies/console.ts +1 -1
- package/src/pipes/output/strategies/json.spec.ts +16 -6
- package/src/pipes/output/strategies/json.ts +14 -3
- package/src/pipes/output/strategies/merge.ts +1 -1
- package/src/pipes/output/strategies/xls.spec.ts +44 -15
- package/src/pipes/output/strategies/xls.ts +42 -10
- package/src/pipes/output/types.ts +1 -0
- package/src/pipes/retrieve/index.ts +48 -19
- package/src/pipes/retrieve/types.ts +5 -0
- package/src/pipes/server/index.ts +2 -6
- package/src/pipes/server/types.ts +5 -0
- package/src/pipes/types.ts +1 -0
- package/src/utils/append-error.ts +42 -0
- package/src/utils/load-env.ts +18 -0
- package/biome.json +0 -34
- package/test/connect.spec.ts +0 -119
- package/test/container/container.spec.ts +0 -33
- package/test/container/container.ts +0 -22
- package/test/container/setup/databases.spec.ts +0 -24
- package/test/container/setup/databases.ts +0 -77
- package/test/container/setup/users.spec.ts +0 -25
- package/test/container/setup/users.ts +0 -54
- package/test/index.spec.ts +0 -68
- package/test/input.spec.ts +0 -64
- package/tsconfig.json +0 -28
package/src/pipes/input/index.ts
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
|
-
import type { Transaction } from "mssql";
|
|
2
1
|
import { Execute } from "../execute";
|
|
3
|
-
import type { DatabaseConnection } from "../connect";
|
|
4
|
-
import { Retrieve
|
|
2
|
+
import type { DatabaseConnection } from "../connect/types";
|
|
3
|
+
import { Retrieve } from "../retrieve";
|
|
4
|
+
import type { InputChain } from "./types";
|
|
5
5
|
|
|
6
|
-
export
|
|
7
|
-
Execute(fn: (transaction: Transaction, database: string, params: TParam) => Promise<void>): Promise<void>;
|
|
8
|
-
Retrieve<TResult>(fn: (transaction: Transaction, database: string, params: TParam) => Promise<TResult>): RetrieveChain<TResult>;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const Input = (connections: AsyncGenerator<DatabaseConnection[]>) => {
|
|
6
|
+
export const Input = (connections$: (databases: string[]) => Generator<DatabaseConnection[]>, databases$: Promise<string[]>) => {
|
|
12
7
|
return <TParam>(fn: () => TParam): InputChain<TParam> => {
|
|
13
8
|
const params = fn();
|
|
14
9
|
return {
|
|
15
|
-
Execute: Execute(connections
|
|
16
|
-
Retrieve: Retrieve(connections
|
|
10
|
+
Execute: Execute(connections$, databases$, params),
|
|
11
|
+
Retrieve: Retrieve(connections$, databases$, params)
|
|
17
12
|
}
|
|
18
13
|
}
|
|
19
14
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Transaction } from "mssql";
|
|
2
|
+
import type { RetrieveChain } from "../retrieve/types";
|
|
3
|
+
|
|
4
|
+
export type InputChain<TParam> = {
|
|
5
|
+
Execute(fn: (transaction: Transaction, database: string, params: TParam) => Promise<void>): Promise<void>;
|
|
6
|
+
Retrieve<TResult>(fn: (transaction: Transaction, database: string, params: TParam) => Promise<TResult>): RetrieveChain<TResult>;
|
|
7
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export const Output = <TReturn, TOutput = void>(data: ReadableStream<Record<string, TReturn>>) => (strategy: OutputStrategy<TReturn, TOutput>): Promise<TOutput> => strategy(data);
|
|
1
|
+
import type { OutputStrategy } from "./types";
|
|
4
2
|
|
|
3
|
+
export const Output =
|
|
4
|
+
<TReturn, TOutput = void>(data: ReadableStream<Record<string, TReturn>>) =>
|
|
5
|
+
(strategy: OutputStrategy<TReturn, TOutput>): Promise<TOutput> => strategy(data);
|
|
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { JsonOutputStrategy } from "./index";
|
|
3
3
|
|
|
4
4
|
describe("JsonOutputStrategy", () => {
|
|
5
|
-
test("should
|
|
5
|
+
test("should write JSON data to file and return filename", async () => {
|
|
6
6
|
const mockData = new ReadableStream({
|
|
7
7
|
start(controller) {
|
|
8
8
|
controller.enqueue({ "database1": [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }] });
|
|
@@ -14,14 +14,19 @@ describe("JsonOutputStrategy", () => {
|
|
|
14
14
|
const strategy = JsonOutputStrategy();
|
|
15
15
|
const result = await strategy(mockData);
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
// Verify file exists and contains correct data
|
|
18
|
+
const fileContent = await Bun.file(result).text();
|
|
19
|
+
const parsedData = JSON.parse(fileContent);
|
|
20
|
+
expect(parsedData).toEqual({
|
|
19
21
|
"database1": [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }],
|
|
20
22
|
"database2": [{ id: 3, name: "Charlie" }]
|
|
21
23
|
});
|
|
24
|
+
|
|
25
|
+
// Clean up
|
|
26
|
+
await Bun.$`rm ${result}`;
|
|
22
27
|
});
|
|
23
28
|
|
|
24
|
-
test("should handle empty data", async () => {
|
|
29
|
+
test("should handle empty data and create file", async () => {
|
|
25
30
|
const mockData = new ReadableStream({
|
|
26
31
|
start(controller) {
|
|
27
32
|
controller.enqueue({});
|
|
@@ -32,7 +37,12 @@ describe("JsonOutputStrategy", () => {
|
|
|
32
37
|
const strategy = JsonOutputStrategy();
|
|
33
38
|
const result = await strategy(mockData);
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
// Verify file exists and contains empty object
|
|
41
|
+
const fileContent = await Bun.file(result).text();
|
|
42
|
+
const parsedData = JSON.parse(fileContent);
|
|
43
|
+
expect(parsedData).toEqual({});
|
|
44
|
+
|
|
45
|
+
// Clean up
|
|
46
|
+
await Bun.$`rm ${result}`;
|
|
37
47
|
});
|
|
38
48
|
});
|
|
@@ -1,9 +1,20 @@
|
|
|
1
|
-
import type { OutputStrategy } from '../
|
|
1
|
+
import type { OutputStrategy } from '../types';
|
|
2
2
|
|
|
3
|
-
export const JsonOutputStrategy = <TData>(): OutputStrategy<TData,
|
|
3
|
+
export const JsonOutputStrategy = <TData>(): OutputStrategy<TData, string> => async (result) => {
|
|
4
4
|
const data: Record<string, TData[]> = {};
|
|
5
|
+
|
|
5
6
|
for await (const item of result) {
|
|
6
7
|
Object.assign(data, item);
|
|
7
8
|
}
|
|
8
|
-
|
|
9
|
+
|
|
10
|
+
let filename = process.argv[1]?.replace(/\.(?:js|ts)/, '')
|
|
11
|
+
filename = `${filename}-${Date.now()}.json`;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
await Bun.write(filename, JSON.stringify(data, null, 2));
|
|
15
|
+
} catch (error) {
|
|
16
|
+
console.error('Error writing JSON file:', error);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return filename;
|
|
9
20
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OutputStrategy } from '../
|
|
1
|
+
import type { OutputStrategy } from '../types';
|
|
2
2
|
|
|
3
3
|
export const MergeOutputStrategy = <TData extends Array<unknown>, TMerged = TData extends Array<infer TItem> ? TItem : TData>(): OutputStrategy<TData, TMerged[]> => async (result) => {
|
|
4
4
|
const data: TMerged[] = [];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { XlsOutputStrategy } from "./index";
|
|
3
|
+
import * as XLSX from "xlsx";
|
|
3
4
|
|
|
4
5
|
describe("XlsOutputStrategy", () => {
|
|
5
6
|
test("should generate XLS file with separate sheets", async () => {
|
|
@@ -13,14 +14,13 @@ describe("XlsOutputStrategy", () => {
|
|
|
13
14
|
}
|
|
14
15
|
});
|
|
15
16
|
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
await strategy(mockData);
|
|
17
|
+
const strategy = XlsOutputStrategy(false);
|
|
18
|
+
const filename = await strategy(mockData);
|
|
19
19
|
|
|
20
|
-
expect(await Bun.file(
|
|
20
|
+
expect(await Bun.file(filename).exists()).toBe(true);
|
|
21
21
|
|
|
22
22
|
// Clean up
|
|
23
|
-
await Bun.file(
|
|
23
|
+
await Bun.file(filename).delete();
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
test("should generate XLS file with combined sheet when unique=true", async () => {
|
|
@@ -34,14 +34,13 @@ describe("XlsOutputStrategy", () => {
|
|
|
34
34
|
}
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
await strategy(mockData);
|
|
37
|
+
const strategy = XlsOutputStrategy(true);
|
|
38
|
+
const filename = await strategy(mockData);
|
|
40
39
|
|
|
41
|
-
expect(await Bun.file(
|
|
40
|
+
expect(await Bun.file(filename).exists()).toBe(true);
|
|
42
41
|
|
|
43
42
|
// Clean up
|
|
44
|
-
await Bun.file(
|
|
43
|
+
await Bun.file(filename).delete();
|
|
45
44
|
});
|
|
46
45
|
|
|
47
46
|
test("should create empty sheet when no data provided", async () => {
|
|
@@ -52,13 +51,43 @@ describe("XlsOutputStrategy", () => {
|
|
|
52
51
|
}
|
|
53
52
|
});
|
|
54
53
|
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
await strategy(mockData);
|
|
54
|
+
const strategy = XlsOutputStrategy(false);
|
|
55
|
+
const filename = await strategy(mockData);
|
|
58
56
|
|
|
59
|
-
expect(await Bun.file(
|
|
57
|
+
expect(await Bun.file(filename).exists()).toBe(true);
|
|
60
58
|
|
|
61
59
|
// Clean up
|
|
62
|
-
await Bun.file(
|
|
60
|
+
await Bun.file(filename).delete();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("Should group rows by database when unique=true", async () => {
|
|
64
|
+
const mockData = new ReadableStream({
|
|
65
|
+
start(controller) {
|
|
66
|
+
controller.enqueue({
|
|
67
|
+
"database1": [{ id: 1, name: "Alice", age: 30 }, { id: 2, name: "Bob", age: 25 }],
|
|
68
|
+
"database2": [{ id: 1, name: "Charlie", age: 35 }, { id: 2, name: "Dave", age: 40 }]
|
|
69
|
+
});
|
|
70
|
+
controller.close();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const strategy = XlsOutputStrategy(true);
|
|
75
|
+
const filename = await strategy(mockData);
|
|
76
|
+
|
|
77
|
+
const file = Bun.file(filename);
|
|
78
|
+
|
|
79
|
+
expect(await file.exists()).toBe(true);
|
|
80
|
+
|
|
81
|
+
const workbook = XLSX.read(await file.arrayBuffer(), { cellStyles: true });
|
|
82
|
+
const worksheet = workbook.Sheets["Combined"];
|
|
83
|
+
expect(worksheet).toBeDefined();
|
|
84
|
+
|
|
85
|
+
const rows = worksheet!['!rows']!;
|
|
86
|
+
// Check if row grouping is set correctly
|
|
87
|
+
expect(rows[1]!.level).toEqual(1); // Summary row for database1
|
|
88
|
+
expect(rows[3]!.level).toEqual(1); // Summary row for database2
|
|
89
|
+
|
|
90
|
+
// Clean up
|
|
91
|
+
await Bun.file(filename).delete();
|
|
63
92
|
});
|
|
64
93
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as XLSX from 'xlsx';
|
|
2
|
-
import type { OutputStrategy } from '../
|
|
2
|
+
import type { OutputStrategy } from '../types';
|
|
3
3
|
|
|
4
4
|
async function processSeparateSheets<TData>(
|
|
5
5
|
result: ReadableStream<Record<string, TData>>,
|
|
@@ -9,7 +9,7 @@ async function processSeparateSheets<TData>(
|
|
|
9
9
|
|
|
10
10
|
for await (const dbResult of result) {
|
|
11
11
|
for (const [database, data] of Object.entries(dbResult)) {
|
|
12
|
-
let sheetData:
|
|
12
|
+
let sheetData: unknown[] = [];
|
|
13
13
|
if (Array.isArray(data)) {
|
|
14
14
|
sheetData = data;
|
|
15
15
|
} else if (data && typeof data === 'object') {
|
|
@@ -33,18 +33,28 @@ async function processCombinedSheet<TData>(
|
|
|
33
33
|
result: ReadableStream<Record<string, TData>>,
|
|
34
34
|
workbook: XLSX.WorkBook
|
|
35
35
|
): Promise<boolean> {
|
|
36
|
-
|
|
36
|
+
const allData: unknown[] = [];
|
|
37
|
+
const databaseGroups: { [database: string]: { startRow: number, endRow: number } } = {};
|
|
38
|
+
let currentRow = 1; // Start from row 1 (header is row 0)
|
|
37
39
|
|
|
38
40
|
for await (const dbResult of result) {
|
|
39
41
|
for (const [database, data] of Object.entries(dbResult)) {
|
|
40
|
-
let sheetData:
|
|
42
|
+
let sheetData: unknown[] = [];
|
|
41
43
|
if (Array.isArray(data)) {
|
|
42
|
-
sheetData = data.map(item => ({ ...item
|
|
44
|
+
sheetData = data.map(item => ({ database, ...item }));
|
|
43
45
|
} else if (data && typeof data === 'object') {
|
|
44
|
-
sheetData = [{ ...data
|
|
46
|
+
sheetData = [{ database, ...data }];
|
|
45
47
|
} else {
|
|
46
|
-
sheetData = [{ value: data
|
|
48
|
+
sheetData = [{ database, value: data }];
|
|
47
49
|
}
|
|
50
|
+
|
|
51
|
+
// Track the start row for this database group
|
|
52
|
+
databaseGroups[database] ??= { startRow: currentRow, endRow: currentRow };
|
|
53
|
+
|
|
54
|
+
// Update the end row for this database group
|
|
55
|
+
databaseGroups[database].endRow = currentRow + sheetData.length - 1;
|
|
56
|
+
currentRow += sheetData.length;
|
|
57
|
+
|
|
48
58
|
allData.push(...sheetData);
|
|
49
59
|
}
|
|
50
60
|
}
|
|
@@ -54,11 +64,23 @@ async function processCombinedSheet<TData>(
|
|
|
54
64
|
}
|
|
55
65
|
|
|
56
66
|
const worksheet = XLSX.utils.json_to_sheet(allData);
|
|
67
|
+
|
|
68
|
+
worksheet['!rows'] ??= [];
|
|
69
|
+
|
|
70
|
+
// Set row grouping for each database
|
|
71
|
+
for (const [_, { startRow, endRow }] of Object.entries(databaseGroups)) {
|
|
72
|
+
// Set level 1 for all rows in this database group
|
|
73
|
+
for (let i = startRow; i <= endRow; i++) {
|
|
74
|
+
worksheet['!rows'][i] ??= {hpx: 20};
|
|
75
|
+
worksheet['!rows'][i]!.level = i === endRow ? 0 : 1;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
57
79
|
XLSX.utils.book_append_sheet(workbook, worksheet, "Combined");
|
|
58
80
|
return true;
|
|
59
81
|
}
|
|
60
82
|
|
|
61
|
-
export const XlsOutputStrategy = <TData>(unique: boolean = false
|
|
83
|
+
export const XlsOutputStrategy = <TData>(unique: boolean = false): OutputStrategy<TData, string> => async (result) => {
|
|
62
84
|
const workbook = XLSX.utils.book_new();
|
|
63
85
|
|
|
64
86
|
if (unique) {
|
|
@@ -74,6 +96,16 @@ export const XlsOutputStrategy = <TData>(unique: boolean = false, filename: stri
|
|
|
74
96
|
XLSX.utils.book_append_sheet(workbook, emptyWorksheet, "Empty");
|
|
75
97
|
}
|
|
76
98
|
}
|
|
77
|
-
|
|
78
|
-
|
|
99
|
+
|
|
100
|
+
let filename = process.argv[1]?.replace(/\.(?:js|ts)/, '');
|
|
101
|
+
filename = `${filename}-${Date.now()}.xlsx`;
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx', cellStyles: true });
|
|
105
|
+
await Bun.write(filename, buffer);
|
|
106
|
+
return filename;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error('Error writing Excel file');
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
79
111
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type OutputStrategy<TReturn, TOutput = void> = (data: ReadableStream<Record<string, TReturn>>) => Promise<TOutput>;
|
|
@@ -1,43 +1,72 @@
|
|
|
1
1
|
import type { Transaction } from "mssql";
|
|
2
|
-
import {
|
|
3
|
-
import type { DatabaseConnection } from "../connect";
|
|
2
|
+
import { Output } from "../output"
|
|
3
|
+
import type { DatabaseConnection } from "../connect/types";
|
|
4
|
+
import type { RetrieveChain } from "./types";
|
|
5
|
+
import { Presets, SingleBar } from "cli-progress";
|
|
6
|
+
import { LoadEnv } from "../../utils/load-env";
|
|
7
|
+
import { AppendError, CleanErrors, type ErrorType } from "../../utils/append-error";
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
9
|
+
const ENV = LoadEnv();
|
|
10
|
+
let ERRORS_COUNT = 0;
|
|
8
11
|
|
|
9
|
-
export const Retrieve = <TParam>(
|
|
12
|
+
export const Retrieve = <TParam>(
|
|
13
|
+
connections$: (databases: string[]) => Generator<DatabaseConnection[]>,
|
|
14
|
+
databases$: Promise<string[]>,
|
|
15
|
+
input: TParam
|
|
16
|
+
) => {
|
|
10
17
|
return <TReturn>(fn: (transaction: Transaction, database: string, params: TParam) => Promise<TReturn>): RetrieveChain<TReturn> => {
|
|
11
18
|
const { readable, writable } = new TransformStream<Record<string, TReturn>, Record<string, TReturn>>();
|
|
12
19
|
const writer = writable.getWriter();
|
|
13
20
|
|
|
21
|
+
const singlerBar = new SingleBar({
|
|
22
|
+
format: `{bar} {percentage}% | {value}/{total} | {database}`
|
|
23
|
+
}, Presets.shades_classic);
|
|
24
|
+
|
|
14
25
|
const executeFn = async (dc: DatabaseConnection) => {
|
|
15
26
|
const opened = await dc.connection;
|
|
16
27
|
const transaction = opened.transaction();
|
|
17
28
|
try {
|
|
18
29
|
await transaction.begin();
|
|
19
30
|
const result = await fn(transaction, dc.database, input);
|
|
20
|
-
await writer.write({ [dc.database]: result });
|
|
21
31
|
transaction.commit();
|
|
32
|
+
|
|
33
|
+
await writer.write({ [dc.database]: result });
|
|
34
|
+
|
|
35
|
+
if (Bun.env.NODE_ENV !== 'test') {
|
|
36
|
+
singlerBar.increment(1, { database: dc.database });
|
|
37
|
+
}
|
|
22
38
|
} catch (error) {
|
|
23
|
-
|
|
24
|
-
|
|
39
|
+
await transaction.rollback();
|
|
40
|
+
await AppendError(dc.database, error as ErrorType);
|
|
41
|
+
|
|
42
|
+
if (++ERRORS_COUNT > ENV.MAX_ERRORS) {
|
|
43
|
+
await writable.abort(error as ErrorType);
|
|
44
|
+
console.error('Max errors reached, exiting...');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
25
47
|
}
|
|
26
48
|
};
|
|
27
49
|
|
|
28
50
|
// Process all connections and close the stream when done
|
|
29
51
|
(async () => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
52
|
+
await CleanErrors();
|
|
53
|
+
const databases = await databases$;
|
|
54
|
+
|
|
55
|
+
if (Bun.env.NODE_ENV !== 'test') {
|
|
56
|
+
singlerBar.start(databases.length, 0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for await (const connectionBatch of connections$(databases)) {
|
|
60
|
+
const executions = connectionBatch.map(executeFn);
|
|
61
|
+
await Promise.allSettled(executions);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (Bun.env.NODE_ENV !== 'test') {
|
|
65
|
+
singlerBar.stop();
|
|
40
66
|
}
|
|
67
|
+
|
|
68
|
+
// Close the writer when all connections are processed
|
|
69
|
+
await writer.close();
|
|
41
70
|
})();
|
|
42
71
|
|
|
43
72
|
return {
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { ServerConfig } from "./types";
|
|
3
|
-
|
|
4
|
-
type ServerChain = {
|
|
5
|
-
Auth(strategy: AuthStrategy): AuthenticationChain;
|
|
6
|
-
}
|
|
1
|
+
import { Auth } from "../auth";
|
|
2
|
+
import type { ServerChain, ServerConfig } from "./types";
|
|
7
3
|
|
|
8
4
|
export const Server = (config: ServerConfig): ServerChain => ({
|
|
9
5
|
Auth: Auth(config),
|
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
import type { config } from "mssql";
|
|
2
|
+
import type { AuthStrategy, AuthenticationChain } from "../auth/types";
|
|
2
3
|
|
|
3
4
|
export type ServerConfig = Omit<config, "authentication" | "user" | "password">;
|
|
5
|
+
|
|
6
|
+
export type ServerChain = {
|
|
7
|
+
Auth(strategy: AuthStrategy): AuthenticationChain;
|
|
8
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./auth/types"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ConnectionError, PreparedStatementError, RequestError, TransactionError } from "mssql";
|
|
2
|
+
|
|
3
|
+
export type ErrorType = Error | ConnectionError | TransactionError | RequestError | PreparedStatementError;
|
|
4
|
+
|
|
5
|
+
const BASE_FILENAME = process.argv[1]?.replace(/\.(?:js|ts)/, '');
|
|
6
|
+
const ERROR_LOG_PATH = `${BASE_FILENAME}-last-errors.json`;
|
|
7
|
+
|
|
8
|
+
export const CleanErrors = async () => {
|
|
9
|
+
if (await Bun.file(ERROR_LOG_PATH).exists()) {
|
|
10
|
+
await Bun.file(ERROR_LOG_PATH).delete()
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const AppendError = async (database: string, error: ErrorType) => {
|
|
15
|
+
const errorFile = Bun.file(ERROR_LOG_PATH);
|
|
16
|
+
const errorContent = await errorFile.exists()
|
|
17
|
+
? await errorFile.json()
|
|
18
|
+
: {};
|
|
19
|
+
|
|
20
|
+
// Create a serializable error object
|
|
21
|
+
const serializableError = {
|
|
22
|
+
name: error.name,
|
|
23
|
+
message: error.message,
|
|
24
|
+
stack: error.stack,
|
|
25
|
+
code: (error as any).code || undefined,
|
|
26
|
+
number: (error as any).number || undefined,
|
|
27
|
+
state: (error as any).state || undefined,
|
|
28
|
+
class: (error as any).class || undefined,
|
|
29
|
+
serverName: (error as any).serverName || undefined,
|
|
30
|
+
procName: (error as any).procName || undefined,
|
|
31
|
+
lineNumber: (error as any).lineNumber || undefined
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Update error content with new error for database
|
|
35
|
+
const updatedContent = {
|
|
36
|
+
...errorContent,
|
|
37
|
+
[database]: serializableError
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Write updated error content to file
|
|
41
|
+
await Bun.write(errorFile, JSON.stringify(updatedContent, null, 2));
|
|
42
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type Env = {
|
|
2
|
+
MAX_ERRORS: number;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
type StringEnv = {
|
|
6
|
+
[P in keyof Env]?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
declare module "bun" {
|
|
10
|
+
interface Env extends StringEnv {}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const LoadEnv = (): Env => {
|
|
14
|
+
const MAX_ERRORS = Number.parseInt(Bun.env.MAX_ERRORS || '1', 10);
|
|
15
|
+
return {
|
|
16
|
+
MAX_ERRORS
|
|
17
|
+
}
|
|
18
|
+
}
|
package/biome.json
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
|
|
3
|
-
"vcs": {
|
|
4
|
-
"enabled": false,
|
|
5
|
-
"clientKind": "git",
|
|
6
|
-
"useIgnoreFile": false
|
|
7
|
-
},
|
|
8
|
-
"files": {
|
|
9
|
-
"ignoreUnknown": false
|
|
10
|
-
},
|
|
11
|
-
"formatter": {
|
|
12
|
-
"enabled": true,
|
|
13
|
-
"indentStyle": "tab"
|
|
14
|
-
},
|
|
15
|
-
"linter": {
|
|
16
|
-
"enabled": true,
|
|
17
|
-
"rules": {
|
|
18
|
-
"recommended": true
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
|
-
"javascript": {
|
|
22
|
-
"formatter": {
|
|
23
|
-
"quoteStyle": "double"
|
|
24
|
-
}
|
|
25
|
-
},
|
|
26
|
-
"assist": {
|
|
27
|
-
"enabled": true,
|
|
28
|
-
"actions": {
|
|
29
|
-
"source": {
|
|
30
|
-
"organizeImports": "on"
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
package/test/connect.spec.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { beforeAll, describe, expect, test, mock, afterEach} from 'bun:test'
|
|
2
|
-
import { AzureSqlEdge, SQL_PASSWORD } from './container/container'
|
|
3
|
-
import { Server } from '../src';
|
|
4
|
-
import { UserAndPassword } from '../src/pipes/auth/strategies';
|
|
5
|
-
import { CLIENTS_MANAGER_DATABASE, DATABASES, SetupClientManager, SetupDatabases } from './container/setup/databases';
|
|
6
|
-
import { type DatabaseConnection } from '../src/pipes/connect';
|
|
7
|
-
import type { Transaction } from 'mssql';
|
|
8
|
-
|
|
9
|
-
const mockExecute = mock(<TParam>(connections$: AsyncGenerator<DatabaseConnection[]>) => (fn: (transaction: Transaction, database: string, params: TParam) => Promise<void>) => Promise<void>);
|
|
10
|
-
mock.module('../src/pipes/execute', () => ({
|
|
11
|
-
Execute: mockExecute,
|
|
12
|
-
}))
|
|
13
|
-
|
|
14
|
-
describe('Connection overloads', async () => {
|
|
15
|
-
const container = await AzureSqlEdge();
|
|
16
|
-
const LocalServer = Server({
|
|
17
|
-
server: container.getHost(),
|
|
18
|
-
port: container.getMappedPort(1433),
|
|
19
|
-
options: {
|
|
20
|
-
encrypt: false
|
|
21
|
-
}
|
|
22
|
-
}).Auth(UserAndPassword("sa", SQL_PASSWORD));
|
|
23
|
-
|
|
24
|
-
beforeAll(async () => {
|
|
25
|
-
await SetupDatabases(container);
|
|
26
|
-
await SetupClientManager(container);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
afterEach(() => {
|
|
30
|
-
mockExecute.mockClear();
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
test('Connect to unique database', async () => {
|
|
34
|
-
const database = DATABASES[0]!;
|
|
35
|
-
const Connection = LocalServer.Connect(database);
|
|
36
|
-
|
|
37
|
-
expect(Connection).toBeDefined();
|
|
38
|
-
expect(mockExecute).toHaveBeenCalledTimes(1);
|
|
39
|
-
|
|
40
|
-
const connectionsGenerator = mockExecute.mock.calls[0]![0];
|
|
41
|
-
const uniqueConnection = await connectionsGenerator.next();
|
|
42
|
-
|
|
43
|
-
expect(uniqueConnection.done).toBe(false);
|
|
44
|
-
expect(uniqueConnection.value).toHaveLength(1);
|
|
45
|
-
expect(uniqueConnection.value).toEqual([{
|
|
46
|
-
database,
|
|
47
|
-
connection: expect.any(Promise),
|
|
48
|
-
}]);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test('Connect with database list', async () => {
|
|
52
|
-
const databases = [DATABASES[0]!, DATABASES[1]!];
|
|
53
|
-
LocalServer.Connect(databases);
|
|
54
|
-
|
|
55
|
-
expect(mockExecute).toHaveBeenCalledTimes(1);
|
|
56
|
-
const connectionsGenerator = mockExecute.mock.calls[0]![0];
|
|
57
|
-
const connections = await connectionsGenerator.next();
|
|
58
|
-
|
|
59
|
-
expect(connections.done).toBe(false);
|
|
60
|
-
expect(connections.value).toHaveLength(2);
|
|
61
|
-
expect(connections.value).toEqual([{
|
|
62
|
-
database: DATABASES[0]!,
|
|
63
|
-
connection: expect.any(Promise),
|
|
64
|
-
}, {
|
|
65
|
-
database: DATABASES[1]!,
|
|
66
|
-
connection: expect.any(Promise),
|
|
67
|
-
}]);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test('Connect with limited concurrent database list', async () => {
|
|
71
|
-
const databases = [DATABASES[0]!, DATABASES[1]!, DATABASES[2]!, DATABASES[3]!];
|
|
72
|
-
LocalServer.Connect(databases, 2);
|
|
73
|
-
|
|
74
|
-
expect(mockExecute).toHaveBeenCalledTimes(1);
|
|
75
|
-
const connectionsGenerator = mockExecute.mock.calls[0]![0];
|
|
76
|
-
const firstConnections = await connectionsGenerator.next();
|
|
77
|
-
|
|
78
|
-
expect(firstConnections.done).toBe(false);
|
|
79
|
-
expect(firstConnections.value).toHaveLength(2);
|
|
80
|
-
expect(firstConnections.value).toEqual([{
|
|
81
|
-
database: DATABASES[0]!,
|
|
82
|
-
connection: expect.any(Promise),
|
|
83
|
-
}, {
|
|
84
|
-
database: DATABASES[1]!,
|
|
85
|
-
connection: expect.any(Promise),
|
|
86
|
-
}]);
|
|
87
|
-
|
|
88
|
-
const secondConnections = await connectionsGenerator.next();
|
|
89
|
-
expect(secondConnections.done).toBe(false);
|
|
90
|
-
expect(secondConnections.value).toHaveLength(2);
|
|
91
|
-
expect(secondConnections.value).toEqual([{
|
|
92
|
-
database: DATABASES[2]!,
|
|
93
|
-
connection: expect.any(Promise),
|
|
94
|
-
}, {
|
|
95
|
-
database: DATABASES[3]!,
|
|
96
|
-
connection: expect.any(Promise),
|
|
97
|
-
}]);
|
|
98
|
-
|
|
99
|
-
const thirdConnections = await connectionsGenerator.next();
|
|
100
|
-
expect(thirdConnections.done).toBe(true);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test('Connect with query', async () => {
|
|
104
|
-
LocalServer.Connect({
|
|
105
|
-
database: CLIENTS_MANAGER_DATABASE,
|
|
106
|
-
query: 'SELECT DatabaseName FROM Clients WHERE Active = 1'
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
expect(mockExecute).toHaveBeenCalledTimes(1);
|
|
110
|
-
const connectionsGenerator = mockExecute.mock.calls[0]![0];
|
|
111
|
-
const connections = await connectionsGenerator.next();
|
|
112
|
-
expect(connections.done).toBe(false);
|
|
113
|
-
expect(connections.value).toHaveLength(5);
|
|
114
|
-
expect(connections.value).toEqual(DATABASES.map(database => ({
|
|
115
|
-
database,
|
|
116
|
-
connection: expect.any(Promise),
|
|
117
|
-
})));
|
|
118
|
-
})
|
|
119
|
-
})
|