squilo 0.1.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/README.md +88 -0
- package/biome.json +34 -0
- package/package.json +44 -0
- package/src/index.ts +1 -0
- package/src/pipes/auth/index.ts +24 -0
- package/src/pipes/auth/strategies/index.ts +2 -0
- package/src/pipes/auth/strategies/msal.ts +133 -0
- package/src/pipes/auth/strategies/userAndPassword.ts +10 -0
- package/src/pipes/connect/index.ts +78 -0
- package/src/pipes/execute/index.ts +29 -0
- package/src/pipes/index.ts +1 -0
- package/src/pipes/input/index.ts +19 -0
- package/src/pipes/output/index.ts +4 -0
- package/src/pipes/output/strategies/console.ts +7 -0
- package/src/pipes/output/strategies/index.ts +4 -0
- package/src/pipes/output/strategies/json.spec.ts +38 -0
- package/src/pipes/output/strategies/json.ts +9 -0
- package/src/pipes/output/strategies/merge.spec.ts +51 -0
- package/src/pipes/output/strategies/merge.ts +11 -0
- package/src/pipes/output/strategies/xls.spec.ts +64 -0
- package/src/pipes/output/strategies/xls.ts +79 -0
- package/src/pipes/retrieve/index.ts +47 -0
- package/src/pipes/server/index.ts +10 -0
- package/src/pipes/server/types.ts +3 -0
- package/src/pool/index.ts +43 -0
- package/test/connect.spec.ts +119 -0
- package/test/container/container.spec.ts +33 -0
- package/test/container/container.ts +22 -0
- package/test/container/setup/databases.spec.ts +24 -0
- package/test/container/setup/databases.ts +77 -0
- package/test/container/setup/users.spec.ts +25 -0
- package/test/container/setup/users.ts +54 -0
- package/test/index.spec.ts +68 -0
- package/test/input.spec.ts +64 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as XLSX from 'xlsx';
|
|
2
|
+
import type { OutputStrategy } from '../index';
|
|
3
|
+
|
|
4
|
+
async function processSeparateSheets<TData>(
|
|
5
|
+
result: ReadableStream<Record<string, TData>>,
|
|
6
|
+
workbook: XLSX.WorkBook
|
|
7
|
+
): Promise<boolean> {
|
|
8
|
+
let hasData = false;
|
|
9
|
+
|
|
10
|
+
for await (const dbResult of result) {
|
|
11
|
+
for (const [database, data] of Object.entries(dbResult)) {
|
|
12
|
+
let sheetData: any[] = [];
|
|
13
|
+
if (Array.isArray(data)) {
|
|
14
|
+
sheetData = data;
|
|
15
|
+
} else if (data && typeof data === 'object') {
|
|
16
|
+
sheetData = [data];
|
|
17
|
+
} else {
|
|
18
|
+
sheetData = [{ value: data }];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (sheetData.length > 0) {
|
|
22
|
+
const worksheet = XLSX.utils.json_to_sheet(sheetData);
|
|
23
|
+
XLSX.utils.book_append_sheet(workbook, worksheet, database);
|
|
24
|
+
hasData = true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return hasData;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function processCombinedSheet<TData>(
|
|
33
|
+
result: ReadableStream<Record<string, TData>>,
|
|
34
|
+
workbook: XLSX.WorkBook
|
|
35
|
+
): Promise<boolean> {
|
|
36
|
+
let allData: any[] = [];
|
|
37
|
+
|
|
38
|
+
for await (const dbResult of result) {
|
|
39
|
+
for (const [database, data] of Object.entries(dbResult)) {
|
|
40
|
+
let sheetData: any[] = [];
|
|
41
|
+
if (Array.isArray(data)) {
|
|
42
|
+
sheetData = data.map(item => ({ ...item, database }));
|
|
43
|
+
} else if (data && typeof data === 'object') {
|
|
44
|
+
sheetData = [{ ...data, database }];
|
|
45
|
+
} else {
|
|
46
|
+
sheetData = [{ value: data, database }];
|
|
47
|
+
}
|
|
48
|
+
allData.push(...sheetData);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (allData.length === 0) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const worksheet = XLSX.utils.json_to_sheet(allData);
|
|
57
|
+
XLSX.utils.book_append_sheet(workbook, worksheet, "Combined");
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const XlsOutputStrategy = <TData>(unique: boolean = false, filename: string): OutputStrategy<TData, void> => async (result) => {
|
|
62
|
+
const workbook = XLSX.utils.book_new();
|
|
63
|
+
|
|
64
|
+
if (unique) {
|
|
65
|
+
const hasData = await processCombinedSheet(result, workbook);
|
|
66
|
+
if (!hasData) {
|
|
67
|
+
const emptyWorksheet = XLSX.utils.json_to_sheet([{ message: "No data available" }]);
|
|
68
|
+
XLSX.utils.book_append_sheet(workbook, emptyWorksheet, "Empty");
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
const hasData = await processSeparateSheets(result, workbook);
|
|
72
|
+
if (!hasData) {
|
|
73
|
+
const emptyWorksheet = XLSX.utils.json_to_sheet([{ message: "No data available" }]);
|
|
74
|
+
XLSX.utils.book_append_sheet(workbook, emptyWorksheet, "Empty");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
XLSX.writeFile(workbook, filename);
|
|
79
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Transaction } from "mssql";
|
|
2
|
+
import { type OutputStrategy, Output } from "../output";
|
|
3
|
+
import type { DatabaseConnection } from "../connect";
|
|
4
|
+
|
|
5
|
+
export type RetrieveChain<TReturn> = {
|
|
6
|
+
Output<TOutput>(strategy: OutputStrategy<TReturn, TOutput>): Promise<TOutput>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const Retrieve = <TParam>(connections$: AsyncGenerator<DatabaseConnection[]>, input: TParam) => {
|
|
10
|
+
return <TReturn>(fn: (transaction: Transaction, database: string, params: TParam) => Promise<TReturn>): RetrieveChain<TReturn> => {
|
|
11
|
+
const { readable, writable } = new TransformStream<Record<string, TReturn>, Record<string, TReturn>>();
|
|
12
|
+
const writer = writable.getWriter();
|
|
13
|
+
|
|
14
|
+
const executeFn = async (dc: DatabaseConnection) => {
|
|
15
|
+
const opened = await dc.connection;
|
|
16
|
+
const transaction = opened.transaction();
|
|
17
|
+
try {
|
|
18
|
+
await transaction.begin();
|
|
19
|
+
const result = await fn(transaction, dc.database, input);
|
|
20
|
+
await writer.write({ [dc.database]: result });
|
|
21
|
+
transaction.commit();
|
|
22
|
+
} catch (error) {
|
|
23
|
+
// TODO: Append client name and error in a structured json file
|
|
24
|
+
transaction.rollback();
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Process all connections and close the stream when done
|
|
29
|
+
(async () => {
|
|
30
|
+
try {
|
|
31
|
+
for await (const connectionBatch of connections$) {
|
|
32
|
+
const executions = connectionBatch.map(executeFn);
|
|
33
|
+
await Promise.all(executions);
|
|
34
|
+
}
|
|
35
|
+
// Close the writer when all connections are processed
|
|
36
|
+
await writer.close();
|
|
37
|
+
} catch (error) {
|
|
38
|
+
// If there's an error in processing connections, abort the writer
|
|
39
|
+
writer.abort(error);
|
|
40
|
+
}
|
|
41
|
+
})();
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
Output: Output(readable)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type AuthStrategy, type AuthenticationChain, Auth } from "../auth";
|
|
2
|
+
import type { ServerConfig } from "./types";
|
|
3
|
+
|
|
4
|
+
type ServerChain = {
|
|
5
|
+
Auth(strategy: AuthStrategy): AuthenticationChain;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const Server = (config: ServerConfig): ServerChain => ({
|
|
9
|
+
Auth: Auth(config),
|
|
10
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type config, ConnectionPool } from 'mssql';
|
|
2
|
+
export type Pool = {
|
|
3
|
+
connect: (partialConfig: Partial<config>) => Promise<ConnectionPool>;
|
|
4
|
+
closeAll: () => Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function Pool(poolConfig: config): Pool {
|
|
8
|
+
const POOL: Record<string, Promise<ConnectionPool>> = {};
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
connect: (partialConfig: Partial<config>) => {
|
|
12
|
+
const config = { ...poolConfig, ...partialConfig };
|
|
13
|
+
const database = config.database;
|
|
14
|
+
|
|
15
|
+
if (!database) {
|
|
16
|
+
throw new Error('Database name is required');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if(!(database in POOL)) {
|
|
20
|
+
const pool = new ConnectionPool(config);
|
|
21
|
+
const close = pool.close.bind(pool);
|
|
22
|
+
|
|
23
|
+
pool.close = async () => {
|
|
24
|
+
delete POOL[database];
|
|
25
|
+
return await close();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
pool.on('error', err => {
|
|
29
|
+
throw new Error(err.message);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
POOL[database] = pool.connect();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return POOL[database]!;
|
|
36
|
+
},
|
|
37
|
+
closeAll: async () => {
|
|
38
|
+
const closes = Object.values(POOL).map(pool => pool.then(p => p.close()));
|
|
39
|
+
await Promise.all(closes);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it, test } from 'bun:test'
|
|
2
|
+
import { connect, ConnectionPool } from 'mssql'
|
|
3
|
+
import type { StartedTestContainer } from 'testcontainers'
|
|
4
|
+
import { AzureSqlEdge, SQL_PASSWORD } from './container';
|
|
5
|
+
|
|
6
|
+
describe('SQL Server', async () => {
|
|
7
|
+
let container: StartedTestContainer
|
|
8
|
+
let sqlClient: ConnectionPool
|
|
9
|
+
|
|
10
|
+
beforeAll(async () => {
|
|
11
|
+
container = await AzureSqlEdge()
|
|
12
|
+
sqlClient = await connect({
|
|
13
|
+
server: container.getHost(),
|
|
14
|
+
port: container.getMappedPort(1433),
|
|
15
|
+
database: 'master',
|
|
16
|
+
user: 'sa',
|
|
17
|
+
password: SQL_PASSWORD,
|
|
18
|
+
options: {
|
|
19
|
+
encrypt: false
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
afterAll(async () => {
|
|
25
|
+
await sqlClient.close();
|
|
26
|
+
await container.stop();
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('should connect to the database', async () => {
|
|
30
|
+
const result = await sqlClient.query`SELECT 'Hello'`;
|
|
31
|
+
expect(result.recordset[0]['']).toBe('Hello');
|
|
32
|
+
})
|
|
33
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { GenericContainer, type StartedTestContainer, Wait } from "testcontainers";
|
|
2
|
+
|
|
3
|
+
export const SQL_PASSWORD = "YourStrong@Passw0rd";
|
|
4
|
+
|
|
5
|
+
export const AzureSqlEdge = () => new GenericContainer('mcr.microsoft.com/azure-sql-edge')
|
|
6
|
+
.withEnvironment({
|
|
7
|
+
'ACCEPT_EULA': 'Y',
|
|
8
|
+
'MSSQL_SA_PASSWORD': SQL_PASSWORD
|
|
9
|
+
})
|
|
10
|
+
.withExposedPorts(1433)
|
|
11
|
+
.withWaitStrategy(Wait.forLogMessage('Recovery is complete'))
|
|
12
|
+
.start();
|
|
13
|
+
|
|
14
|
+
export const CONFIG = (container: StartedTestContainer) => ({
|
|
15
|
+
server: container.getHost(),
|
|
16
|
+
port: container.getMappedPort(1433),
|
|
17
|
+
user: "sa",
|
|
18
|
+
password: SQL_PASSWORD,
|
|
19
|
+
options: {
|
|
20
|
+
encrypt: false
|
|
21
|
+
}
|
|
22
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, it } from "bun:test"
|
|
2
|
+
import { SetupDatabases } from "./databases"
|
|
3
|
+
import { AzureSqlEdge, CONFIG } from "../container"
|
|
4
|
+
import { connect } from "mssql"
|
|
5
|
+
|
|
6
|
+
describe('Database creation', async () => {
|
|
7
|
+
const container = await AzureSqlEdge();
|
|
8
|
+
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
await SetupDatabases(container);
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('Should create 5 databases', async () => {
|
|
14
|
+
const conn = await connect({
|
|
15
|
+
...CONFIG(container),
|
|
16
|
+
database: "master"
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const result = await conn.query`SELECT * FROM sys.databases WHERE name like 'TestDB%'`
|
|
20
|
+
expect(result.recordset).toHaveLength(5);
|
|
21
|
+
|
|
22
|
+
await conn.close();
|
|
23
|
+
})
|
|
24
|
+
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Bit, connect, NVarChar, Table } from "mssql";
|
|
2
|
+
import { CONFIG } from "../container";
|
|
3
|
+
import type { StartedTestContainer } from "testcontainers";
|
|
4
|
+
|
|
5
|
+
export const CLIENTS_MANAGER_DATABASE = 'ClientsManager';
|
|
6
|
+
|
|
7
|
+
export const DATABASES = [
|
|
8
|
+
"TestDB1",
|
|
9
|
+
"TestDB2",
|
|
10
|
+
"TestDB3",
|
|
11
|
+
"TestDB4",
|
|
12
|
+
"TestDB5",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export const SetupClientManager = async (container: StartedTestContainer) => {
|
|
16
|
+
const masterConn = await connect({
|
|
17
|
+
...CONFIG(container),
|
|
18
|
+
database: "master"
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
await masterConn.query(`
|
|
22
|
+
IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = '${CLIENTS_MANAGER_DATABASE}')
|
|
23
|
+
BEGIN
|
|
24
|
+
CREATE DATABASE ${CLIENTS_MANAGER_DATABASE};
|
|
25
|
+
END
|
|
26
|
+
`);
|
|
27
|
+
|
|
28
|
+
await masterConn.close();
|
|
29
|
+
|
|
30
|
+
const clientManagerConn = await connect({
|
|
31
|
+
...CONFIG(container),
|
|
32
|
+
database: CLIENTS_MANAGER_DATABASE
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
await clientManagerConn.query(`
|
|
36
|
+
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Clients')
|
|
37
|
+
BEGIN
|
|
38
|
+
CREATE TABLE Clients (
|
|
39
|
+
Id INT PRIMARY KEY IDENTITY(1,1),
|
|
40
|
+
Name NVARCHAR(255) UNIQUE NOT NULL,
|
|
41
|
+
DatabaseName NVARCHAR(255) UNIQUE NOT NULL,
|
|
42
|
+
Active BIT NOT NULL
|
|
43
|
+
);
|
|
44
|
+
END
|
|
45
|
+
`);
|
|
46
|
+
|
|
47
|
+
const table = new Table("Clients");
|
|
48
|
+
table.columns.add("Name", NVarChar(255), { nullable: false});
|
|
49
|
+
table.columns.add("DatabaseName", NVarChar(255), { nullable: false});
|
|
50
|
+
table.columns.add("Active", Bit(), { nullable: false});
|
|
51
|
+
|
|
52
|
+
for (const database of DATABASES) {
|
|
53
|
+
table.rows.add(database, database, 1);
|
|
54
|
+
}
|
|
55
|
+
table.rows.add("TestDB6", "TestDB6", 0);
|
|
56
|
+
|
|
57
|
+
await clientManagerConn.request().bulk(table);
|
|
58
|
+
await clientManagerConn.close();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const SetupDatabases = async (container: StartedTestContainer) => {
|
|
62
|
+
const conn = await connect({
|
|
63
|
+
...CONFIG(container),
|
|
64
|
+
database: "master"
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
for (const dbName of DATABASES) {
|
|
68
|
+
await conn.query(`
|
|
69
|
+
IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = '${dbName}')
|
|
70
|
+
BEGIN
|
|
71
|
+
CREATE DATABASE ${dbName};
|
|
72
|
+
END
|
|
73
|
+
`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
await conn.close();
|
|
77
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { AzureSqlEdge, CONFIG } from "../container";
|
|
3
|
+
import { DATABASES, SetupDatabases } from "./databases";
|
|
4
|
+
import { SetupUsers } from "./users";
|
|
5
|
+
import { connect } from "mssql";
|
|
6
|
+
|
|
7
|
+
describe('Users table', async () => {
|
|
8
|
+
const container = await AzureSqlEdge();
|
|
9
|
+
|
|
10
|
+
beforeAll(async () => {
|
|
11
|
+
await SetupDatabases(container);
|
|
12
|
+
await SetupUsers(container);
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test.each(DATABASES)('Should create 10 users in %s', async (database) => {
|
|
16
|
+
const conn = await connect({
|
|
17
|
+
...CONFIG(container),
|
|
18
|
+
database
|
|
19
|
+
});
|
|
20
|
+
const result = await conn.query`SELECT * FROM Users`
|
|
21
|
+
expect(result.recordset).toHaveLength(10);
|
|
22
|
+
|
|
23
|
+
await conn.close();
|
|
24
|
+
})
|
|
25
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { DATABASES } from "./databases";
|
|
2
|
+
import { connect, NVarChar, Table } from "mssql";
|
|
3
|
+
import { CONFIG } from "../container";
|
|
4
|
+
import { faker } from "@faker-js/faker";
|
|
5
|
+
import type { StartedTestContainer } from "testcontainers";
|
|
6
|
+
|
|
7
|
+
export type User = {
|
|
8
|
+
Id: number;
|
|
9
|
+
Name: string;
|
|
10
|
+
Email: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const SetupUsers = async (container: StartedTestContainer, options: {
|
|
14
|
+
populate: boolean;
|
|
15
|
+
} = {
|
|
16
|
+
populate: true
|
|
17
|
+
}) => {
|
|
18
|
+
for (const database of DATABASES) {
|
|
19
|
+
const conn = await connect({
|
|
20
|
+
...CONFIG(container),
|
|
21
|
+
database
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
await conn.request().query`
|
|
25
|
+
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Users')
|
|
26
|
+
BEGIN
|
|
27
|
+
CREATE TABLE Users (
|
|
28
|
+
Id INT PRIMARY KEY IDENTITY(1,1),
|
|
29
|
+
Name NVARCHAR(100) NOT NULL,
|
|
30
|
+
Email NVARCHAR(100) NOT NULL
|
|
31
|
+
);
|
|
32
|
+
END
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
if (options.populate) {
|
|
36
|
+
await conn.request().query(`DELETE FROM Users`);
|
|
37
|
+
|
|
38
|
+
const usersTable = new Table("Users");
|
|
39
|
+
usersTable.columns.add("Name", NVarChar(100), { nullable: false });
|
|
40
|
+
usersTable.columns.add("Email", NVarChar(100), { nullable: false });
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < 10; i++) {
|
|
43
|
+
const name = faker.person.fullName({ firstName: "Joe"});
|
|
44
|
+
const email = faker.internet.email({ firstName: "Joe" }) + " ";
|
|
45
|
+
|
|
46
|
+
usersTable.rows.add(name, email);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
await conn.request().bulk(usersTable);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
await conn.close();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { expect, beforeAll, describe, it, afterAll, test } from "bun:test";
|
|
2
|
+
import { Server } from "../src/pipes/server";
|
|
3
|
+
import { UserAndPassword } from "../src/pipes/auth/strategies";
|
|
4
|
+
import { MergeOutputStrategy } from "../src/pipes/output/strategies";
|
|
5
|
+
import { AzureSqlEdge, SQL_PASSWORD } from "./container/container";
|
|
6
|
+
import { DATABASES, SetupDatabases } from "./container/setup/databases";
|
|
7
|
+
import { SetupUsers, type User } from "./container/setup/users";
|
|
8
|
+
|
|
9
|
+
describe('Squilo test', async () => {
|
|
10
|
+
const container = await AzureSqlEdge();
|
|
11
|
+
|
|
12
|
+
const LocalServer = Server({
|
|
13
|
+
server: container.getHost(),
|
|
14
|
+
port: container.getMappedPort(1433),
|
|
15
|
+
options: {
|
|
16
|
+
encrypt: false
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
.Auth(UserAndPassword("sa", SQL_PASSWORD));
|
|
20
|
+
|
|
21
|
+
beforeAll(async () => {
|
|
22
|
+
await SetupDatabases(container);
|
|
23
|
+
await SetupUsers(container);
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
afterAll(async () => {
|
|
27
|
+
await LocalServer.Close();
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it("Get one from each database", async () => {
|
|
31
|
+
|
|
32
|
+
const users = await LocalServer
|
|
33
|
+
.Connect(DATABASES)
|
|
34
|
+
.Retrieve(async (transaction) => {
|
|
35
|
+
const result = await transaction.request().query<User>`
|
|
36
|
+
SELECT TOP 1 * FROM Users
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
return result.recordset;
|
|
40
|
+
})
|
|
41
|
+
.Output(MergeOutputStrategy());
|
|
42
|
+
|
|
43
|
+
expect(users).toHaveLength(5);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('Should fix user\'s email that are ending with extra space', async () => {
|
|
47
|
+
await LocalServer
|
|
48
|
+
.Connect(DATABASES)
|
|
49
|
+
.Execute(async (transaction) => {
|
|
50
|
+
await transaction.request().query`
|
|
51
|
+
UPDATE Users SET Email = RTRIM(Email)
|
|
52
|
+
`;
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const users = await LocalServer
|
|
56
|
+
.Connect(DATABASES)
|
|
57
|
+
.Retrieve(async (transaction) => {
|
|
58
|
+
const result = await transaction.request().query<User>`
|
|
59
|
+
SELECT * FROM Users
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
return result.recordset;
|
|
63
|
+
})
|
|
64
|
+
.Output(MergeOutputStrategy());
|
|
65
|
+
|
|
66
|
+
expect(users.every((user) => user.Email.endsWith(" ") === false)).toBe(true);
|
|
67
|
+
})
|
|
68
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { AzureSqlEdge, SQL_PASSWORD } from "./container/container";
|
|
3
|
+
import { Server } from "../src/pipes/server";
|
|
4
|
+
import { UserAndPassword } from "../src/pipes/auth/strategies";
|
|
5
|
+
import { DATABASES, SetupDatabases } from "./container/setup/databases";
|
|
6
|
+
import { SetupUsers, type User } from "./container/setup/users";
|
|
7
|
+
import { MergeOutputStrategy } from "../src/pipes/output/strategies";
|
|
8
|
+
|
|
9
|
+
describe('Squilo input test', async () => {
|
|
10
|
+
const container = await AzureSqlEdge();
|
|
11
|
+
const localServer = Server({
|
|
12
|
+
server: container.getHost(),
|
|
13
|
+
port: container.getMappedPort(1433),
|
|
14
|
+
options: {
|
|
15
|
+
encrypt: false
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
.Auth(UserAndPassword("sa", SQL_PASSWORD));
|
|
19
|
+
|
|
20
|
+
beforeAll(async () => {
|
|
21
|
+
await SetupDatabases(container);
|
|
22
|
+
await SetupUsers(container, { populate: false })
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
afterAll(async () => {
|
|
26
|
+
await localServer.Close();
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('Should add a user using console input', async () => {
|
|
30
|
+
let name: string | null = "joe";
|
|
31
|
+
let email: string | null = "joe.doe@example.com";
|
|
32
|
+
|
|
33
|
+
await localServer
|
|
34
|
+
.Connect(DATABASES)
|
|
35
|
+
.Input<Omit<User, "Id">>(() => {
|
|
36
|
+
return {
|
|
37
|
+
Name: name!,
|
|
38
|
+
Email: email!,
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
.Execute(async (transaction, _, user) => {
|
|
42
|
+
await transaction.request().query`
|
|
43
|
+
INSERT INTO Users (Name, Email)
|
|
44
|
+
VALUES (${user.Name}, ${user.Email})
|
|
45
|
+
`;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const users = await localServer
|
|
49
|
+
.Connect(DATABASES)
|
|
50
|
+
.Retrieve(async (transaction) => {
|
|
51
|
+
const result = await transaction.request().query<User>`
|
|
52
|
+
SELECT * FROM Users
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
return result.recordset;
|
|
56
|
+
})
|
|
57
|
+
.Output(MergeOutputStrategy());
|
|
58
|
+
|
|
59
|
+
expect(users).toHaveLength(5);
|
|
60
|
+
|
|
61
|
+
const everyUserIsJoe = users.every(user => user.Name === name && user.Email === email);
|
|
62
|
+
expect(everyUserIsJoe).toBe(true);
|
|
63
|
+
})
|
|
64
|
+
})
|