Skip to content

Commit 9477b33

Browse files
committed
Added Dumbo - utilities for PostgreSQL handling
That will help to keep the shared code between Pongo and Emmett in one place. Dumbo will be versioned separately, which should give possibility to use different versions in Pongo and Emmett. And have this peer dependency more stable. Dumbo is treated for now as internal Pongo and Emmett dependency, so there are no plans (for now) to provide docs for it.
1 parent b40edc8 commit 9477b33

23 files changed

+363
-37
lines changed

src/package-lock.json

+24-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@event-driven-io/pongo-core",
3-
"version": "0.2.4",
3+
"version": "0.3.0",
44
"description": "Pongo - Mongo with strong consistency on top of Postgres",
55
"type": "module",
66
"engines": {
@@ -98,6 +98,7 @@
9898
"pg-format": "^1.0.4"
9999
},
100100
"workspaces": [
101+
"packages/dumbo",
101102
"packages/pongo"
102103
]
103104
}

src/packages/dumbo/package.json

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
{
2+
"name": "@event-driven-io/dumbo",
3+
"version": "0.1.0",
4+
"description": "Dumbo - tools for dealing with PostgreSQL",
5+
"type": "module",
6+
"scripts": {
7+
"build": "tsup",
8+
"build:ts": "tsc",
9+
"build:ts:watch": "tsc --watch",
10+
"test": "run-s test:unit test:int test:e2e",
11+
"test:unit": "glob -c \"node --import tsx --test\" **/*.unit.spec.ts",
12+
"test:int": "glob -c \"node --import tsx --test\" **/*.int.spec.ts",
13+
"test:e2e": "glob -c \"node --import tsx --test\" **/*.e2e.spec.ts",
14+
"test:watch": "node --import tsx --test --watch",
15+
"test:unit:watch": "glob -c \"node --import tsx --test --watch\" **/*.unit.spec.ts",
16+
"test:int:watch": "glob -c \"node --import tsx --test --watch\" **/*.int.spec.ts",
17+
"test:e2e:watch": "glob -c \"node --import tsx --test --watch\" **/*.e2e.spec.ts"
18+
},
19+
"repository": {
20+
"type": "git",
21+
"url": "git+https://github.com/event-driven-io/Pongo.git"
22+
},
23+
"keywords": [
24+
"Event Sourcing"
25+
],
26+
"author": "Oskar Dudycz",
27+
"bugs": {
28+
"url": "https://github.com/event-driven-io/Pongo/issues"
29+
},
30+
"homepage": "https://event-driven-io.github.io/Pongo/",
31+
"exports": {
32+
".": {
33+
"import": {
34+
"types": "./dist/index.d.ts",
35+
"default": "./dist/index.js"
36+
},
37+
"require": {
38+
"types": "./dist/index.d.cts",
39+
"default": "./dist/index.cjs"
40+
}
41+
}
42+
},
43+
"main": "./dist/index.cjs",
44+
"module": "./dist/index.js",
45+
"types": "./dist/index.d.ts",
46+
"files": [
47+
"dist"
48+
],
49+
"peerDependencies": {
50+
"@types/uuid": "^9.0.8",
51+
"@types/pg": "^8.11.6",
52+
"@types/pg-format": "^1.0.5",
53+
"pg": "^8.12.0",
54+
"pg-format": "^1.0.4",
55+
"uuid": "^9.0.1"
56+
},
57+
"devDependencies": {
58+
"@types/node": "20.11.30"
59+
}
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import pg from 'pg';
2+
import { endPool, getPool } from './pool';
3+
4+
export interface PostgresClient {
5+
connect(): Promise<pg.PoolClient>;
6+
close(): Promise<void>;
7+
}
8+
9+
export const postgresClient = (
10+
connectionString: string,
11+
database?: string,
12+
): PostgresClient => {
13+
const pool = getPool({ connectionString, database });
14+
15+
return {
16+
connect: () => pool.connect(),
17+
close: () => endPool(connectionString),
18+
};
19+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './client';
2+
export * from './pool';
+158
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import type pg from 'pg';
2+
import type { SQL } from '../sql';
3+
4+
export const execute = async <Result = void>(
5+
pool: pg.Pool,
6+
handle: (client: pg.PoolClient) => Promise<Result>,
7+
) => {
8+
const client = await pool.connect();
9+
try {
10+
return await handle(client);
11+
} finally {
12+
client.release();
13+
}
14+
};
15+
16+
export const executeInTransaction = async <Result = void>(
17+
pool: pg.Pool,
18+
handle: (
19+
client: pg.PoolClient,
20+
) => Promise<{ success: boolean; result: Result }>,
21+
): Promise<Result> =>
22+
execute(pool, async (client) => {
23+
try {
24+
await client.query('BEGIN');
25+
26+
const { success, result } = await handle(client);
27+
28+
if (success) await client.query('COMMIT');
29+
else await client.query('ROLLBACK');
30+
31+
return result;
32+
} catch (e) {
33+
await client.query('ROLLBACK');
34+
throw e;
35+
}
36+
});
37+
38+
export const executeSQL = async <
39+
Result extends pg.QueryResultRow = pg.QueryResultRow,
40+
>(
41+
poolOrClient: pg.Pool | pg.PoolClient,
42+
sql: SQL,
43+
): Promise<pg.QueryResult<Result>> =>
44+
'totalCount' in poolOrClient
45+
? execute(poolOrClient, (client) => client.query<Result>(sql))
46+
: poolOrClient.query<Result>(sql);
47+
48+
export const executeSQLInTransaction = async <
49+
Result extends pg.QueryResultRow = pg.QueryResultRow,
50+
>(
51+
pool: pg.Pool,
52+
sql: SQL,
53+
) => {
54+
console.log(sql);
55+
return executeInTransaction(pool, async (client) => ({
56+
success: true,
57+
result: await client.query<Result>(sql),
58+
}));
59+
};
60+
61+
export const executeSQLBatchInTransaction = async <
62+
Result extends pg.QueryResultRow = pg.QueryResultRow,
63+
>(
64+
pool: pg.Pool,
65+
...sqls: SQL[]
66+
) =>
67+
executeInTransaction(pool, async (client) => {
68+
for (const sql of sqls) {
69+
await client.query<Result>(sql);
70+
}
71+
72+
return { success: true, result: undefined };
73+
});
74+
75+
export const firstOrNull = async <
76+
Result extends pg.QueryResultRow = pg.QueryResultRow,
77+
>(
78+
getResult: Promise<pg.QueryResult<Result>>,
79+
): Promise<Result | null> => {
80+
const result = await getResult;
81+
82+
return result.rows.length > 0 ? result.rows[0] ?? null : null;
83+
};
84+
85+
export const first = async <
86+
Result extends pg.QueryResultRow = pg.QueryResultRow,
87+
>(
88+
getResult: Promise<pg.QueryResult<Result>>,
89+
): Promise<Result> => {
90+
const result = await getResult;
91+
92+
if (result.rows.length === 0)
93+
throw new Error("Query didn't return any result");
94+
95+
return result.rows[0]!;
96+
};
97+
98+
export const singleOrNull = async <
99+
Result extends pg.QueryResultRow = pg.QueryResultRow,
100+
>(
101+
getResult: Promise<pg.QueryResult<Result>>,
102+
): Promise<Result | null> => {
103+
const result = await getResult;
104+
105+
if (result.rows.length > 1) throw new Error('Query had more than one result');
106+
107+
return result.rows.length > 0 ? result.rows[0] ?? null : null;
108+
};
109+
110+
export const single = async <
111+
Result extends pg.QueryResultRow = pg.QueryResultRow,
112+
>(
113+
getResult: Promise<pg.QueryResult<Result>>,
114+
): Promise<Result> => {
115+
const result = await getResult;
116+
117+
if (result.rows.length === 0)
118+
throw new Error("Query didn't return any result");
119+
120+
if (result.rows.length > 1) throw new Error('Query had more than one result');
121+
122+
return result.rows[0]!;
123+
};
124+
125+
export const mapRows = async <
126+
Result extends pg.QueryResultRow = pg.QueryResultRow,
127+
Mapped = unknown,
128+
>(
129+
getResult: Promise<pg.QueryResult<Result>>,
130+
map: (row: Result) => Mapped,
131+
): Promise<Mapped[]> => {
132+
const result = await getResult;
133+
134+
return result.rows.map(map);
135+
};
136+
137+
export const toCamelCase = (snakeStr: string): string =>
138+
snakeStr.replace(/_([a-z])/g, (g) => g[1]?.toUpperCase() ?? '');
139+
140+
export const mapToCamelCase = <T extends Record<string, unknown>>(
141+
obj: T,
142+
): T => {
143+
const newObj: Record<string, unknown> = {};
144+
for (const key in obj) {
145+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
146+
newObj[toCamelCase(key)] = obj[key];
147+
}
148+
}
149+
return newObj as T;
150+
};
151+
152+
export type ExistsSQLQueryResult = { exists: boolean };
153+
154+
export const exists = async (pool: pg.Pool, sql: SQL): Promise<boolean> => {
155+
const result = await single(executeSQL<ExistsSQLQueryResult>(pool, sql));
156+
157+
return result.exists === true;
158+
};

src/packages/dumbo/src/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './connections';
2+
export * from './execute';
3+
export * from './sql';

src/packages/dumbo/src/sql/schema.ts

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import pg from 'pg';
2+
import { sql, type SQL } from '.';
3+
import { exists } from '../execute';
4+
export * from './schema';
5+
6+
export const tableExistsSQL = (tableName: string): SQL =>
7+
sql(
8+
`
9+
SELECT EXISTS (
10+
SELECT FROM pg_tables
11+
WHERE tablename = %L
12+
) AS exists;`,
13+
tableName,
14+
);
15+
16+
export const tableExists = async (
17+
pool: pg.Pool,
18+
tableName: string,
19+
): Promise<boolean> => exists(pool, tableExistsSQL(tableName));
20+
21+
export const functionExistsSQL = (functionName: string): SQL =>
22+
sql(
23+
`
24+
SELECT EXISTS (
25+
SELECT FROM pg_proc
26+
WHERE
27+
proname = %L
28+
) AS exists;
29+
`,
30+
functionName,
31+
);
32+
33+
export const functionExists = async (
34+
pool: pg.Pool,
35+
functionName: string,
36+
): Promise<boolean> => exists(pool, functionExistsSQL(functionName));
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"composite": false
5+
}
6+
}
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "./tsconfig.json"
3+
}

src/packages/dumbo/tsconfig.json

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"extends": "../../tsconfig.shared.json",
3+
"include": ["./src/**/*"],
4+
"compilerOptions": {
5+
"composite": true,
6+
"outDir": "./dist" /* Redirect output structure to the directory. */,
7+
"rootDir": "./src",
8+
"paths": {}
9+
},
10+
"references": []
11+
}

0 commit comments

Comments
 (0)