Skip to content

Commit d8ab9de

Browse files
committed
Added the first implementation of the Pongo implementation
1 parent 3d8b2e0 commit d8ab9de

File tree

14 files changed

+350
-23
lines changed

14 files changed

+350
-23
lines changed

src/package-lock.json

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

src/package.json

+4-3
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,11 @@
6262
"dist"
6363
],
6464
"devDependencies": {
65+
"@faker-js/faker": "8.4.1",
66+
"@types/mongodb": "^4.0.7",
6567
"@types/node": "20.11.30",
6668
"@types/pg": "^8.11.6",
6769
"@types/pg-format": "^1.0.5",
68-
"@faker-js/faker": "8.4.1",
69-
"@types/mongodb": "^4.0.7",
7070
"@types/uuid": "9.0.8",
7171
"@typescript-eslint/eslint-plugin": "7.9.0",
7272
"@typescript-eslint/parser": "7.9.0",
@@ -86,7 +86,8 @@
8686
},
8787
"peerDependencies": {
8888
"pg": "^8.12.0",
89-
"pg-format": "^1.0.4"
89+
"pg-format": "^1.0.4",
90+
"close-with-grace": "^1.3.0"
9091
},
9192
"workspaces": [
9293
"packages/pongo"

src/packages/pongo/package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,10 @@
4848
],
4949
"peerDependencies": {
5050
"@types/uuid": "^9.0.8",
51-
"uuid": "^9.0.1",
51+
"close-with-grace": "^1.3.0",
5252
"pg": "^8.12.0",
53-
"pg-format": "^1.0.4"
53+
"pg-format": "^1.0.4",
54+
"uuid": "^9.0.1"
5455
},
5556
"devDependencies": {
5657
"@types/node": "20.11.30",

src/packages/pongo/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './main';
2+
export * from './postgres';

src/packages/pongo/src/main/client.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { getDbClient } from './dbClient';
2+
import type { PongoClient, PongoDb } from './typing';
3+
4+
export const pongoClient = (connectionString: string): PongoClient => {
5+
const dbClient = getDbClient(connectionString);
6+
7+
return {
8+
connect: () => dbClient.connect(),
9+
close: () => dbClient.close(),
10+
db: (dbName?: string): PongoDb =>
11+
dbName ? getDbClient(connectionString, dbName) : dbClient,
12+
};
13+
};
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { postgresClient } from '../postgres';
2+
import type { PongoCollection } from './typing';
3+
4+
export interface DbClient {
5+
connect(): Promise<void>;
6+
close(): Promise<void>;
7+
collection: <T>(name: string) => PongoCollection<T>;
8+
}
9+
10+
export const getDbClient = (
11+
connectionString: string,
12+
database?: string,
13+
): DbClient => {
14+
// This is the place where in the future could come resolution of other database types
15+
return postgresClient(connectionString, database);
16+
};

src/packages/pongo/src/main/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './client';
2+
export * from './dbClient';
3+
export * from './typing';
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,27 @@
1-
// src/pongoTypes.ts
1+
export interface PongoClient {
2+
connect(): Promise<void>;
3+
4+
close(): Promise<void>;
5+
6+
db(dbName?: string): PongoDb;
7+
}
8+
9+
export interface PongoDb {
10+
collection<T>(name: string): PongoCollection<T>;
11+
}
12+
13+
export interface PongoCollection<T> {
14+
createCollection(): Promise<void>;
15+
insertOne(document: T): Promise<PongoInsertResult>;
16+
updateOne(
17+
filter: PongoFilter<T>,
18+
update: PongoUpdate<T>,
19+
): Promise<PongoUpdateResult>;
20+
deleteOne(filter: PongoFilter<T>): Promise<PongoDeleteResult>;
21+
findOne(filter: PongoFilter<T>): Promise<T | null>;
22+
find(filter: PongoFilter<T>): Promise<T[]>;
23+
}
24+
225
export type PongoFilter<T> = {
326
[P in keyof T]?: T[P] | PongoFilterOperator<T[P]>;
427
};
@@ -12,37 +35,24 @@ export type PongoFilterOperator<T> = {
1235
$ne?: T;
1336
$in?: T[];
1437
$nin?: T[];
15-
// Add more operators as needed
1638
};
1739

1840
export type PongoUpdate<T> = {
1941
$set?: Partial<T>;
2042
$unset?: { [P in keyof T]?: '' };
2143
$inc?: { [P in keyof T]?: number };
2244
$push?: { [P in keyof T]?: T[P] };
23-
// Add more update operators as needed
2445
};
2546

2647
export interface PongoInsertResult {
27-
insertedId: string;
48+
insertedId: string | null;
49+
insertedCount: number | null;
2850
}
2951

3052
export interface PongoUpdateResult {
31-
modifiedCount: number;
53+
modifiedCount: number | null;
3254
}
3355

3456
export interface PongoDeleteResult {
35-
deletedCount: number;
36-
}
37-
38-
export interface PongoCollection<T> {
39-
createCollection(): Promise<void>;
40-
insertOne(document: T): Promise<PongoInsertResult>;
41-
updateOne(
42-
filter: PongoFilter<T>,
43-
update: PongoUpdate<T>,
44-
): Promise<PongoUpdateResult>;
45-
deleteOne(filter: PongoFilter<T>): Promise<PongoDeleteResult>;
46-
findOne(filter: PongoFilter<T>): Promise<T | null>;
47-
find(filter: PongoFilter<T>): Promise<T[]>;
57+
deletedCount: number | null;
4858
}
+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import type { Pool } from 'pg';
2+
import { v4 as uuid } from 'uuid';
3+
import {
4+
type DbClient,
5+
type PongoCollection,
6+
type PongoDeleteResult,
7+
type PongoFilter,
8+
type PongoInsertResult,
9+
type PongoUpdate,
10+
type PongoUpdateResult,
11+
} from '../main';
12+
import { constructFilterQuery } from './filter';
13+
import { getPool } from './pool';
14+
import { constructUpdateQuery } from './update';
15+
import { sql } from './execute';
16+
17+
export const postgresClient = (
18+
connectionString: string,
19+
database?: string,
20+
): DbClient => {
21+
const pool = getPool({ connectionString, database });
22+
23+
return {
24+
connect: () => Promise.resolve(),
25+
close: () => Promise.resolve(),
26+
collection: <T>(name: string) => postgresCollection<T>(name, pool),
27+
};
28+
};
29+
30+
export const postgresCollection = <T>(
31+
collectionName: string,
32+
pool: Pool,
33+
): PongoCollection<T> => {
34+
const createCollection = async (): Promise<void> => {
35+
await sql(
36+
pool,
37+
'CREATE TABLE IF NOT EXISTS %I (id UUID PRIMARY KEY, data JSONB)',
38+
collectionName,
39+
);
40+
};
41+
42+
return {
43+
createCollection,
44+
insertOne: async (document: T): Promise<PongoInsertResult> => {
45+
await createCollection();
46+
47+
const id = uuid();
48+
49+
const result = await sql(
50+
pool,
51+
'INSERT INTO %I (id, data) VALUES (%L, %L)',
52+
collectionName,
53+
id,
54+
JSON.stringify({ ...document, _id: id }),
55+
);
56+
57+
return result.rowCount
58+
? { insertedId: id, insertedCount: result.rowCount }
59+
: { insertedId: null, insertedCount: null };
60+
},
61+
updateOne: async (
62+
filter: PongoFilter<T>,
63+
update: PongoUpdate<T>,
64+
): Promise<PongoUpdateResult> => {
65+
const filterQuery = constructFilterQuery(filter);
66+
const updateQuery = constructUpdateQuery(update);
67+
68+
const result = await sql(
69+
pool,
70+
'UPDATE %I SET data = %s WHERE %s',
71+
collectionName,
72+
updateQuery,
73+
filterQuery,
74+
);
75+
return { modifiedCount: result.rowCount };
76+
},
77+
deleteOne: async (filter: PongoFilter<T>): Promise<PongoDeleteResult> => {
78+
const filterQuery = constructFilterQuery(filter);
79+
const result = await sql(
80+
pool,
81+
'DELETE FROM %I WHERE %s',
82+
collectionName,
83+
filterQuery,
84+
);
85+
return { deletedCount: result.rowCount };
86+
},
87+
findOne: async (filter: PongoFilter<T>): Promise<T | null> => {
88+
const filterQuery = constructFilterQuery(filter);
89+
const result = await sql(
90+
pool,
91+
'SELECT data FROM %I WHERE %s LIMIT 1',
92+
collectionName,
93+
filterQuery,
94+
);
95+
return (result.rows[0]?.data ?? null) as T | null;
96+
},
97+
find: async (filter: PongoFilter<T>): Promise<T[]> => {
98+
const filterQuery = constructFilterQuery(filter);
99+
const result = await sql(
100+
pool,
101+
'SELECT data FROM %I WHERE %s LIMIT 1',
102+
collectionName,
103+
filterQuery,
104+
);
105+
106+
return result.rows.map((row) => row.data as T);
107+
},
108+
};
109+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { QueryResultRow, Pool, QueryResult, PoolClient } from 'pg';
2+
import format from 'pg-format';
3+
4+
export const sql = async <Result extends QueryResultRow = QueryResultRow>(
5+
pool: Pool,
6+
sqlText: string,
7+
...params: unknown[]
8+
): Promise<QueryResult<Result>> => {
9+
const client = await pool.connect();
10+
try {
11+
const query = format(sqlText, ...params);
12+
return await client.query<Result>(query);
13+
} finally {
14+
client.release();
15+
}
16+
};
17+
18+
export const execute = async <Result = void>(
19+
pool: Pool,
20+
handle: (client: PoolClient) => Promise<Result>,
21+
) => {
22+
const client = await pool.connect();
23+
try {
24+
return await handle(client);
25+
} finally {
26+
client.release();
27+
}
28+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import format from 'pg-format';
2+
import type { PongoFilter } from '../../main';
3+
4+
export const constructFilterQuery = <T>(filter: PongoFilter<T>): string => {
5+
const filters = Object.entries(filter).map(([key, value]) => {
6+
if (typeof value === 'object' && !Array.isArray(value)) {
7+
return constructComplexFilterQuery(key, value as Record<string, unknown>);
8+
} else {
9+
return format('data->>%I = %L', key, value);
10+
}
11+
});
12+
return filters.join(' AND ');
13+
};
14+
15+
export const constructComplexFilterQuery = (
16+
key: string,
17+
value: Record<string, unknown>,
18+
): string => {
19+
const subFilters = Object.entries(value).map(([operator, val]) => {
20+
switch (operator) {
21+
case '$eq':
22+
return format('data->>%I = %L', key, val);
23+
case '$gt':
24+
return format('data->>%I > %L', key, val);
25+
case '$gte':
26+
return format('data->>%I >= %L', key, val);
27+
case '$lt':
28+
return format('data->>%I < %L', key, val);
29+
case '$lte':
30+
return format('data->>%I <= %L', key, val);
31+
case '$ne':
32+
return format('data->>%I != %L', key, val);
33+
case '$in':
34+
return format(
35+
'data->>%I IN (%s)',
36+
key,
37+
(val as unknown[]).map((v) => format('%L', v)).join(', '),
38+
);
39+
case '$nin':
40+
return format(
41+
'data->>%I NOT IN (%s)',
42+
key,
43+
(val as unknown[]).map((v) => format('%L', v)).join(', '),
44+
);
45+
default:
46+
throw new Error(`Unsupported operator: ${operator}`);
47+
}
48+
});
49+
return subFilters.join(' AND ');
50+
};
+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './client';
2+
export * from './pool';

0 commit comments

Comments
 (0)