@deterministic-code/datasources-postgres 0.0.0-stage → 0.1.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/package.json +21 -4
- package/src/PostgresBulkUpdateStatementTextRenderer.ts +27 -0
- package/src/PostgresCreateTableStatementRenderer.ts +33 -0
- package/src/PostgresDriver.ts +55 -0
- package/src/PostgresStatementRenderer.ts +34 -0
- package/src/bind.ts +50 -0
- package/src/columnTypes.ts +10 -0
- package/src/command.ts +129 -0
- package/src/connection-options.ts +7 -0
- package/src/connection.ts +95 -0
- package/src/converters/BinaryConverter.ts +20 -0
- package/src/converters/BoolConverter.ts +6 -0
- package/src/converters/DateTimeConverter.ts +11 -0
- package/src/converters/DecimalConverter.ts +7 -0
- package/src/converters/PostgresRowTypeConverters.ts +24 -0
- package/src/converters/UuidConverter.ts +6 -0
- package/src/i-postgres-driver.ts +61 -0
- package/src/index.ts +1 -0
- package/src/scalar.ts +3 -0
- package/src/transaction.ts +21 -0
- package/README.md +0 -3
package/package.json
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deterministic-code/datasources-postgres",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"types": "./src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@deterministic-code/datasources": "^0.1.0",
|
|
19
|
+
"@types/pg": "^8.15.5",
|
|
20
|
+
"pg": "^8.16.0",
|
|
21
|
+
"pg-query-stream": "^4.17.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { BulkUpdateStatementTextRenderer, type Cell } from "@deterministic-code/datasources";
|
|
2
|
+
import { columnTypes } from "./columnTypes.ts";
|
|
3
|
+
import { isBase64 } from "./converters/BinaryConverter.ts";
|
|
4
|
+
import { isIsoDate } from "./converters/DateTimeConverter.ts";
|
|
5
|
+
|
|
6
|
+
const logical = (value: Cell): string | undefined =>
|
|
7
|
+
value === null
|
|
8
|
+
? undefined
|
|
9
|
+
: typeof value === "boolean"
|
|
10
|
+
? "bool"
|
|
11
|
+
: typeof value === "bigint" || (typeof value === "number" && Number.isInteger(value))
|
|
12
|
+
? "int"
|
|
13
|
+
: typeof value === "number"
|
|
14
|
+
? "float"
|
|
15
|
+
: value instanceof Date || (typeof value === "string" && isIsoDate(value))
|
|
16
|
+
? "datetime"
|
|
17
|
+
: value instanceof Uint8Array || (typeof value === "string" && isBase64(value))
|
|
18
|
+
? "binary"
|
|
19
|
+
: "string";
|
|
20
|
+
|
|
21
|
+
export class PostgresBulkUpdateStatementTextRenderer extends BulkUpdateStatementTextRenderer {
|
|
22
|
+
protected override cell = (column: string, index: number, value: Cell): string => {
|
|
23
|
+
const type = logical(value);
|
|
24
|
+
const sql = type ? columnTypes[type] : undefined;
|
|
25
|
+
return sql ? `$${column}__${index}::${sql} AS ${column}` : `$${column}__${index} AS ${column}`;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ColumnDefault,
|
|
3
|
+
CreateTableStatementTextRenderer,
|
|
4
|
+
type Column,
|
|
5
|
+
} from "@deterministic-code/datasources";
|
|
6
|
+
import { PostgresRowTypeConverters } from "./converters/PostgresRowTypeConverters.ts";
|
|
7
|
+
import { columnTypes } from "./columnTypes.ts";
|
|
8
|
+
|
|
9
|
+
export class PostgresCreateTableStatementRenderer extends CreateTableStatementTextRenderer {
|
|
10
|
+
protected typeName(column: Column): string {
|
|
11
|
+
return columnTypes[column.type] ?? column.type;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
protected columnDefault(column: Column): string {
|
|
15
|
+
const literal = sqlLiteral(column);
|
|
16
|
+
const value = column.default;
|
|
17
|
+
return value == null || literal == null
|
|
18
|
+
? super.columnDefault(column)
|
|
19
|
+
: super.columnDefault({ ...column, default: new ColumnDefault(literal, value.name) });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
protected identity(column: Column): string {
|
|
23
|
+
return column.identity ? " GENERATED BY DEFAULT AS IDENTITY" : "";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function sqlLiteral(column: Column): string | undefined {
|
|
28
|
+
const raw = column.default?.value;
|
|
29
|
+
const converter = PostgresRowTypeConverters[column.type];
|
|
30
|
+
if (raw == null || !converter || !/^-?\d+$/.test(raw)) return undefined;
|
|
31
|
+
const converted = converter.convert(Number(raw));
|
|
32
|
+
return converted === true ? "TRUE" : converted === false ? "FALSE" : undefined;
|
|
33
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DatasourceError,
|
|
3
|
+
StatementTextRenderer,
|
|
4
|
+
type Cell,
|
|
5
|
+
type DatasourceOptions,
|
|
6
|
+
type ICommand,
|
|
7
|
+
type IConnection,
|
|
8
|
+
type IDriver,
|
|
9
|
+
type Information,
|
|
10
|
+
type ITransaction,
|
|
11
|
+
type IsolationLevel,
|
|
12
|
+
type TypeConverter,
|
|
13
|
+
} from "@deterministic-code/datasources";
|
|
14
|
+
import { PostgresConnection } from "./connection.ts";
|
|
15
|
+
import { PostgresCommand } from "./command.ts";
|
|
16
|
+
import { PostgresTransaction } from "./transaction.ts";
|
|
17
|
+
import { PostgresRowTypeConverters } from "./converters/PostgresRowTypeConverters.ts";
|
|
18
|
+
import { PostgresStatementRenderer } from "./PostgresStatementRenderer.ts";
|
|
19
|
+
import { columnTypes } from "./columnTypes.ts";
|
|
20
|
+
|
|
21
|
+
export { columnTypes };
|
|
22
|
+
|
|
23
|
+
const statements = new StatementTextRenderer(t => new PostgresStatementRenderer(t));
|
|
24
|
+
|
|
25
|
+
export class PostgresDriver implements IDriver {
|
|
26
|
+
readonly statementRenderer = statements;
|
|
27
|
+
readonly information: Information = {
|
|
28
|
+
descriptions: [
|
|
29
|
+
"PostgreSQL server driver.",
|
|
30
|
+
"Full connection, command, transaction, and repository support.",
|
|
31
|
+
],
|
|
32
|
+
overrides: [
|
|
33
|
+
"Overrides SelectInsertedIdStatement to SELECT lastval() AS {column}",
|
|
34
|
+
"Overrides CreateTableStatement identity to GENERATED BY DEFAULT AS IDENTITY",
|
|
35
|
+
"Overrides CreateTableStatement boolean defaults (0/1 → TRUE/FALSE)",
|
|
36
|
+
"Overrides CreateTableStatement type mapping (int→INTEGER, uuid→UUID, string→TEXT, bool→BOOLEAN, decimal→NUMERIC, float→DOUBLE PRECISION, datetime→TIMESTAMPTZ, binary→BYTEA)",
|
|
37
|
+
],
|
|
38
|
+
changeLogs: [],
|
|
39
|
+
};
|
|
40
|
+
rowTypeConverters: Record<string, TypeConverter<Cell, Cell>> = PostgresRowTypeConverters;
|
|
41
|
+
columnType = (name: string): string =>
|
|
42
|
+
columnTypes[name] ??
|
|
43
|
+
(() => {
|
|
44
|
+
throw new DatasourceError(`Unknown postgres type "${name}"`);
|
|
45
|
+
})();
|
|
46
|
+
connection = (options?: DatasourceOptions): IConnection =>
|
|
47
|
+
PostgresConnection.from(options);
|
|
48
|
+
command = (connection: IConnection): ICommand =>
|
|
49
|
+
new PostgresCommand(connection as PostgresConnection, statements);
|
|
50
|
+
transaction = (
|
|
51
|
+
connection: IConnection,
|
|
52
|
+
isolation: IsolationLevel = "readCommitted",
|
|
53
|
+
): ITransaction =>
|
|
54
|
+
new PostgresTransaction(connection as PostgresConnection, isolation);
|
|
55
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BulkUpdateStatement,
|
|
3
|
+
CreateTableStatement,
|
|
4
|
+
SelectInsertedIdStatement,
|
|
5
|
+
SelectInsertedIdStatementTextRenderer,
|
|
6
|
+
StatementRenderer,
|
|
7
|
+
type StatementTextRenderer,
|
|
8
|
+
} from "@deterministic-code/datasources";
|
|
9
|
+
import { PostgresBulkUpdateStatementTextRenderer } from "./PostgresBulkUpdateStatementTextRenderer.ts";
|
|
10
|
+
import { PostgresCreateTableStatementRenderer } from "./PostgresCreateTableStatementRenderer.ts";
|
|
11
|
+
|
|
12
|
+
class PostgresSelectInsertedIdStatementTextRenderer extends SelectInsertedIdStatementTextRenderer {
|
|
13
|
+
override render(statement: SelectInsertedIdStatement): string {
|
|
14
|
+
return `SELECT lastval() AS ${statement.column}`;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class PostgresStatementRenderer extends StatementRenderer {
|
|
19
|
+
constructor(text: StatementTextRenderer) {
|
|
20
|
+
super(text);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
protected override visitSelectInsertedId(inserted: SelectInsertedIdStatement): string {
|
|
24
|
+
return new PostgresSelectInsertedIdStatementTextRenderer().render(inserted);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
protected override visitCreateTable(create: CreateTableStatement): string {
|
|
28
|
+
return new PostgresCreateTableStatementRenderer().render(create);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
protected override visitBulkUpdate(update: BulkUpdateStatement): string {
|
|
32
|
+
return new PostgresBulkUpdateStatementTextRenderer().render(update);
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/bind.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { parametersToRecord, type Cell, type Parameter } from "@deterministic-code/datasources";
|
|
2
|
+
import { isBase64 } from "./converters/BinaryConverter.ts";
|
|
3
|
+
import { isIsoDate } from "./converters/DateTimeConverter.ts";
|
|
4
|
+
import { reverse } from "./converters/PostgresRowTypeConverters.ts";
|
|
5
|
+
import type { Scalar, Wire } from "./scalar.ts";
|
|
6
|
+
|
|
7
|
+
const NAME = /\$([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
8
|
+
|
|
9
|
+
const quoteMixedCase = (sql: string): string =>
|
|
10
|
+
sql.replace(/\$[A-Za-z_][A-Za-z0-9_]*|'[^']*'|"[^"]*"|`[^`]*`|\b[A-Za-z_][A-Za-z0-9_]*\b/g, (token) =>
|
|
11
|
+
token.startsWith("$") || token.startsWith("'") || token.startsWith("\"") || token.startsWith("`")
|
|
12
|
+
|| !/[A-Z]/.test(token) || !/[a-z]/.test(token)
|
|
13
|
+
? token
|
|
14
|
+
: `"${token}"`,
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const wire = (value: Scalar): Wire =>
|
|
18
|
+
typeof value !== "string"
|
|
19
|
+
? value
|
|
20
|
+
: isIsoDate(value)
|
|
21
|
+
? new Date(value)
|
|
22
|
+
: isBase64(value)
|
|
23
|
+
? Uint8Array.from(Buffer.from(value, "base64"))
|
|
24
|
+
: value;
|
|
25
|
+
|
|
26
|
+
export const toPostgres = (
|
|
27
|
+
sql: string,
|
|
28
|
+
params?: Record<string, Scalar>,
|
|
29
|
+
): { text: string; values: Wire[] } => {
|
|
30
|
+
const names: string[] = [];
|
|
31
|
+
const text = quoteMixedCase(sql).replace(NAME, (_match, name: string) => {
|
|
32
|
+
let index = names.indexOf(name);
|
|
33
|
+
if (index < 0) {
|
|
34
|
+
names.push(name);
|
|
35
|
+
index = names.length - 1;
|
|
36
|
+
}
|
|
37
|
+
return `$${index + 1}`;
|
|
38
|
+
});
|
|
39
|
+
return { text, values: names.map((name) => wire(params?.[name] ?? null)) };
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const bindParameters = (
|
|
43
|
+
parameters?: Parameter[] | null,
|
|
44
|
+
): Record<string, Scalar> => {
|
|
45
|
+
const record = parametersToRecord(parameters);
|
|
46
|
+
const bound: Record<string, Scalar> = {};
|
|
47
|
+
for (const [name, value] of Object.entries(record))
|
|
48
|
+
bound[name] = reverse(value as Cell) as Scalar;
|
|
49
|
+
return bound;
|
|
50
|
+
};
|
package/src/command.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArrayReader,
|
|
3
|
+
CommandMiddlewareHost,
|
|
4
|
+
FirstResultReader,
|
|
5
|
+
DatasourceError,
|
|
6
|
+
Native,
|
|
7
|
+
ParameterDirection,
|
|
8
|
+
ReturnStatement,
|
|
9
|
+
SelectStatement,
|
|
10
|
+
StatementTextRenderer,
|
|
11
|
+
Response,
|
|
12
|
+
splitStatements,
|
|
13
|
+
statementParameters,
|
|
14
|
+
statementReader,
|
|
15
|
+
type Cells,
|
|
16
|
+
type ICommand,
|
|
17
|
+
type IReader,
|
|
18
|
+
type IStatement,
|
|
19
|
+
type Parameter,
|
|
20
|
+
} from "@deterministic-code/datasources";
|
|
21
|
+
import { bindParameters, toPostgres } from "./bind.ts";
|
|
22
|
+
import type { PostgresConnection } from "./connection.ts";
|
|
23
|
+
import type { PostgresExecuteResult } from "./i-postgres-driver.ts";
|
|
24
|
+
import type { Scalar } from "./scalar.ts";
|
|
25
|
+
import { PostgresStatementRenderer } from "./PostgresStatementRenderer.ts";
|
|
26
|
+
|
|
27
|
+
export class PostgresCommand extends CommandMiddlewareHost implements ICommand {
|
|
28
|
+
private readonly statements: StatementTextRenderer;
|
|
29
|
+
|
|
30
|
+
constructor(readonly connection: PostgresConnection, statements?: StatementTextRenderer) {
|
|
31
|
+
super();
|
|
32
|
+
this.statements = statements ?? new StatementTextRenderer(t => new PostgresStatementRenderer(t));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async executeReaderNative(
|
|
36
|
+
text: Native,
|
|
37
|
+
parameters?: Parameter[],
|
|
38
|
+
): Promise<IReader<Cells>> {
|
|
39
|
+
return this.run(text.text, parameters, async (query, params) => {
|
|
40
|
+
if (!query.trim()) return { value: new ArrayReader([]), rows: [] };
|
|
41
|
+
this.connection.ensureOpen();
|
|
42
|
+
const statements = splitStatements(query);
|
|
43
|
+
if (statements.length === 0) return { value: new ArrayReader([]), rows: [] };
|
|
44
|
+
const bound = bindParameters(params);
|
|
45
|
+
return FirstResultReader.capture(statementReader(statements, (sql) => this.query(sql, bound)));
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async executeReaderStatement(
|
|
50
|
+
statement: IStatement,
|
|
51
|
+
parameters?: Parameter[],
|
|
52
|
+
): Promise<IReader<Cells>> {
|
|
53
|
+
return this.executeReaderNative(new Native(this.statements.render(statement)), parameters);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async executeStatement(statement: IStatement, parameters?: Parameter[]): Promise<Response> {
|
|
57
|
+
return statement instanceof SelectStatement || statement instanceof ReturnStatement
|
|
58
|
+
? this.firstCell(statement, parameters)
|
|
59
|
+
: this.executeNative(new Native(this.statements.render(statement)), parameters);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private async firstCell(statement: IStatement, parameters?: Parameter[]): Promise<Response> {
|
|
63
|
+
const reader = await this.executeReaderStatement(statement, parameters);
|
|
64
|
+
try {
|
|
65
|
+
if (!(await reader.next())) return new Response(null, 0, parameters);
|
|
66
|
+
const row = reader.current;
|
|
67
|
+
/* v8 ignore next */
|
|
68
|
+
if (!row) throw new DatasourceError("Reader current must be a row");
|
|
69
|
+
const values = Object.values(row);
|
|
70
|
+
return new Response(values.length === 0 ? null : values[0], 1, parameters);
|
|
71
|
+
} finally {
|
|
72
|
+
await reader.dispose();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async executeNative(text: Native, parameters?: Parameter[]): Promise<Response> {
|
|
77
|
+
return this.run(text.text, parameters, async (query, params) => {
|
|
78
|
+
if (!query.trim())
|
|
79
|
+
return { value: new Response(undefined, 0, params), rows: [] };
|
|
80
|
+
this.connection.ensureOpen();
|
|
81
|
+
const named = bindParameters(params);
|
|
82
|
+
let rowsAffected = 0;
|
|
83
|
+
let parameters = params;
|
|
84
|
+
for (const part of splitStatements(query)) {
|
|
85
|
+
const names = new Set(statementParameters(part));
|
|
86
|
+
const used = Object.fromEntries(
|
|
87
|
+
Object.entries(named).filter(([name]) => names.has(name.replace(/^[$:@]/, ""))),
|
|
88
|
+
);
|
|
89
|
+
const result = await this.runSql(part, used, params);
|
|
90
|
+
rowsAffected += result.rowsAffected;
|
|
91
|
+
parameters = result.parameters ?? params;
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
value: new Response(null, rowsAffected, parameters),
|
|
95
|
+
rows: [],
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private async runSql(
|
|
101
|
+
sql: string,
|
|
102
|
+
params?: Record<string, Scalar>,
|
|
103
|
+
parameters?: Parameter[],
|
|
104
|
+
): Promise<PostgresExecuteResult> {
|
|
105
|
+
const { text, values } = toPostgres(sql, params);
|
|
106
|
+
const queried = await this.connection.require().query(text, values);
|
|
107
|
+
const row = queried.rows[0];
|
|
108
|
+
return {
|
|
109
|
+
rowsAffected: queried.rowCount ?? queried.rows.length,
|
|
110
|
+
result: null,
|
|
111
|
+
parameters: parameters?.map((parameter) => {
|
|
112
|
+
if (!parameter.direction || parameter.direction === ParameterDirection.Input || !row)
|
|
113
|
+
return parameter;
|
|
114
|
+
const cells = row as Record<string, Scalar | object>;
|
|
115
|
+
const cell = cells[parameter.name] ?? cells[parameter.name.toLowerCase()];
|
|
116
|
+
if (cell === undefined) return parameter;
|
|
117
|
+
return { ...parameter, value: cellValue(cell) };
|
|
118
|
+
}),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private query(sql: string, params?: Record<string, Scalar>): AsyncIterable<Cells> {
|
|
123
|
+
const { text, values } = toPostgres(sql, params);
|
|
124
|
+
return this.connection.require().rows(text, values);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const cellValue = (cell: Scalar | object): Scalar =>
|
|
129
|
+
cell !== null && typeof cell === "object" ? JSON.stringify(cell) : cell;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DatasourceError,
|
|
3
|
+
DatasourceUrlParser,
|
|
4
|
+
IsolationLevel,
|
|
5
|
+
StatementTextRenderer,
|
|
6
|
+
type DatasourceOptions,
|
|
7
|
+
type ICommand,
|
|
8
|
+
type IConnection,
|
|
9
|
+
type IsolationLevelName,
|
|
10
|
+
type ITransaction,
|
|
11
|
+
} from "@deterministic-code/datasources";
|
|
12
|
+
import { createRequire } from "node:module";
|
|
13
|
+
import { PostgresCommand } from "./command.ts";
|
|
14
|
+
import type { PostgresConnectionOptions } from "./connection-options.ts";
|
|
15
|
+
import { pgClient, type IPgClient, type PgClientCtor, type PgModule } from "./i-postgres-driver.ts";
|
|
16
|
+
import { PostgresStatementRenderer } from "./PostgresStatementRenderer.ts";
|
|
17
|
+
import { PostgresTransaction } from "./transaction.ts";
|
|
18
|
+
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
|
|
21
|
+
const isoSql = (isolation: IsolationLevelName): string =>
|
|
22
|
+
isolation === IsolationLevel.ReadUncommitted
|
|
23
|
+
? "READ UNCOMMITTED"
|
|
24
|
+
: isolation === IsolationLevel.ReadCommitted
|
|
25
|
+
? "READ COMMITTED"
|
|
26
|
+
: isolation === IsolationLevel.RepeatableRead
|
|
27
|
+
? "REPEATABLE READ"
|
|
28
|
+
: "SERIALIZABLE";
|
|
29
|
+
|
|
30
|
+
export class PostgresConnection implements IConnection {
|
|
31
|
+
readonly provider = "postgres";
|
|
32
|
+
url: string;
|
|
33
|
+
schema?: string;
|
|
34
|
+
isOpen = false;
|
|
35
|
+
private client?: IPgClient;
|
|
36
|
+
private readonly Client?: PgClientCtor;
|
|
37
|
+
|
|
38
|
+
static from = (options?: DatasourceOptions): PostgresConnection => {
|
|
39
|
+
const url = options?.DATASOURCE_URL ?? "";
|
|
40
|
+
return new PostgresConnection({
|
|
41
|
+
url,
|
|
42
|
+
schema: DatasourceUrlParser.parse(url)?.schema,
|
|
43
|
+
Client: pgClient(require("pg") as PgModule),
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
constructor(options: PostgresConnectionOptions = {}) {
|
|
48
|
+
this.url = options.url ?? "";
|
|
49
|
+
this.schema = options.schema;
|
|
50
|
+
this.Client = options.Client;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
createCommand = (): ICommand =>
|
|
54
|
+
new PostgresCommand(this, new StatementTextRenderer(t => new PostgresStatementRenderer(t)));
|
|
55
|
+
|
|
56
|
+
async open(): Promise<void> {
|
|
57
|
+
if (this.isOpen) throw new DatasourceError("Connection is already open");
|
|
58
|
+
if (!this.client) {
|
|
59
|
+
if (!this.Client) throw new DatasourceError("Postgres driver is required");
|
|
60
|
+
this.client = new this.Client({ connectionString: this.url });
|
|
61
|
+
}
|
|
62
|
+
await this.client.connect();
|
|
63
|
+
if (this.schema) await this.client.query(`SET search_path TO ${this.schema}`);
|
|
64
|
+
this.isOpen = true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async close(): Promise<void> {
|
|
68
|
+
await this.client?.end();
|
|
69
|
+
this.client = undefined;
|
|
70
|
+
this.isOpen = false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
[Symbol.asyncDispose] = (): Promise<void> => this.close();
|
|
74
|
+
|
|
75
|
+
async startTransaction(
|
|
76
|
+
isolation: IsolationLevelName = IsolationLevel.ReadCommitted,
|
|
77
|
+
): Promise<ITransaction> {
|
|
78
|
+
this.ensureOpen();
|
|
79
|
+
await this.exec(`BEGIN ISOLATION LEVEL ${isoSql(isolation)}`);
|
|
80
|
+
return new PostgresTransaction(this, isolation);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
ensureOpen(): void {
|
|
84
|
+
if (!this.isOpen) throw new DatasourceError("Connection is not open");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
require(): IPgClient {
|
|
88
|
+
if (!this.client) throw new DatasourceError("Connection is not open");
|
|
89
|
+
return this.client;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async exec(sql: string): Promise<void> {
|
|
93
|
+
await this.require().query(sql);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { TypeConverter, type Cell } from "@deterministic-code/datasources";
|
|
2
|
+
|
|
3
|
+
export const isBase64 = (value: string): boolean => {
|
|
4
|
+
const text = value.trim();
|
|
5
|
+
return (
|
|
6
|
+
text.length >= 4 &&
|
|
7
|
+
text.length % 4 === 0 &&
|
|
8
|
+
text.includes("=") &&
|
|
9
|
+
/^[A-Za-z0-9+/]+=*$/.test(text) &&
|
|
10
|
+
Buffer.from(text, "base64").toString("base64") === text
|
|
11
|
+
);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const bytes = (value: Cell): Cell =>
|
|
15
|
+
typeof value === "string" ? Uint8Array.from(Buffer.from(value, "base64")) : value;
|
|
16
|
+
|
|
17
|
+
export class BinaryConverter extends TypeConverter<Cell, Cell> {
|
|
18
|
+
convert = (from: Cell): Cell => bytes(from);
|
|
19
|
+
reverseConvert = (to: Cell): Cell => bytes(to);
|
|
20
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { TypeConverter, type Cell } from "@deterministic-code/datasources";
|
|
2
|
+
|
|
3
|
+
export class BoolConverter extends TypeConverter<Cell, Cell> {
|
|
4
|
+
convert = (from: Cell): Cell => (typeof from === "number" ? from !== 0 : from);
|
|
5
|
+
reverseConvert = (to: Cell): Cell => to;
|
|
6
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { TypeConverter, type Cell } from "@deterministic-code/datasources";
|
|
2
|
+
|
|
3
|
+
export const isIsoDate = (value: string): boolean =>
|
|
4
|
+
/^\d{4}-\d{2}-\d{2}T/.test(value) && !Number.isNaN(Date.parse(value));
|
|
5
|
+
|
|
6
|
+
export class DateTimeConverter extends TypeConverter<Cell, Cell> {
|
|
7
|
+
convert = (from: Cell): Cell =>
|
|
8
|
+
typeof from === "string" ? new Date(from) : from;
|
|
9
|
+
reverseConvert = (to: Cell): Cell =>
|
|
10
|
+
to instanceof Date ? to.toISOString() : to;
|
|
11
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { TypeConverter, type Cell } from "@deterministic-code/datasources";
|
|
2
|
+
|
|
3
|
+
export class DecimalConverter extends TypeConverter<Cell, Cell> {
|
|
4
|
+
convert = (from: Cell): Cell => from;
|
|
5
|
+
reverseConvert = (to: Cell): Cell =>
|
|
6
|
+
typeof to === "number" ? String(to) : to;
|
|
7
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type Cell, type TypeConverter } from "@deterministic-code/datasources";
|
|
2
|
+
import { BinaryConverter } from "./BinaryConverter.ts";
|
|
3
|
+
import { BoolConverter } from "./BoolConverter.ts";
|
|
4
|
+
import { DateTimeConverter } from "./DateTimeConverter.ts";
|
|
5
|
+
import { DecimalConverter } from "./DecimalConverter.ts";
|
|
6
|
+
import { UuidConverter } from "./UuidConverter.ts";
|
|
7
|
+
|
|
8
|
+
export const PostgresRowTypeConverters: Record<string, TypeConverter<Cell, Cell>> = {
|
|
9
|
+
bool: new BoolConverter(),
|
|
10
|
+
datetime: new DateTimeConverter(),
|
|
11
|
+
uuid: new UuidConverter(),
|
|
12
|
+
decimal: new DecimalConverter(),
|
|
13
|
+
binary: new BinaryConverter(),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const reverse = (value: Cell): Cell =>
|
|
17
|
+
typeof value === "boolean"
|
|
18
|
+
? PostgresRowTypeConverters.bool.reverseConvert(value)
|
|
19
|
+
: value instanceof Date
|
|
20
|
+
? PostgresRowTypeConverters.datetime.reverseConvert(value)
|
|
21
|
+
: value instanceof Uint8Array
|
|
22
|
+
? PostgresRowTypeConverters.binary.reverseConvert(value)
|
|
23
|
+
: value;
|
|
24
|
+
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Parameter } from "@deterministic-code/datasources";
|
|
2
|
+
import QueryStream from "pg-query-stream";
|
|
3
|
+
import type { Cells, Scalar, Wire } from "./scalar.ts";
|
|
4
|
+
|
|
5
|
+
export interface PostgresExecuteResult {
|
|
6
|
+
rowsAffected: number;
|
|
7
|
+
result?: Scalar;
|
|
8
|
+
parameters?: Parameter[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface PgQueryResult {
|
|
12
|
+
rowCount: number | null;
|
|
13
|
+
rows: Cells[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface IPgClient {
|
|
17
|
+
connect(): Promise<void>;
|
|
18
|
+
query(text: string, values?: Wire[]): Promise<PgQueryResult>;
|
|
19
|
+
rows(text: string, values?: Wire[]): AsyncIterable<Cells>;
|
|
20
|
+
end(): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface PgNativeClient {
|
|
24
|
+
connect(): Promise<void>;
|
|
25
|
+
query(text: string, values?: Wire[]): Promise<PgQueryResult>;
|
|
26
|
+
query(query: QueryStream): AsyncIterable<Cells>;
|
|
27
|
+
end(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type PgClientCtor = new (options: {
|
|
31
|
+
connectionString: string;
|
|
32
|
+
}) => IPgClient;
|
|
33
|
+
|
|
34
|
+
type PgNativeCtor = new (options: { connectionString: string }) => PgNativeClient;
|
|
35
|
+
|
|
36
|
+
export type PgModule = {
|
|
37
|
+
Client: PgNativeCtor;
|
|
38
|
+
default?: { Client: PgNativeCtor };
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const pgClient = (imported: PgModule): PgClientCtor => {
|
|
42
|
+
const Client = imported.default?.Client ?? imported.Client;
|
|
43
|
+
return class implements IPgClient {
|
|
44
|
+
private readonly inner: PgNativeClient;
|
|
45
|
+
constructor(options: { connectionString: string }) {
|
|
46
|
+
this.inner = new Client(options);
|
|
47
|
+
}
|
|
48
|
+
connect(): Promise<void> {
|
|
49
|
+
return this.inner.connect();
|
|
50
|
+
}
|
|
51
|
+
query(text: string, values?: Wire[]): Promise<PgQueryResult> {
|
|
52
|
+
return this.inner.query(text, values);
|
|
53
|
+
}
|
|
54
|
+
rows(text: string, values?: Wire[]): AsyncIterable<Cells> {
|
|
55
|
+
return this.inner.query(new QueryStream(text, values ?? []));
|
|
56
|
+
}
|
|
57
|
+
end(): Promise<void> {
|
|
58
|
+
return this.inner.end();
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { PostgresDriver } from "./PostgresDriver.ts";
|
package/src/scalar.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IsolationLevel,
|
|
3
|
+
type ITransaction,
|
|
4
|
+
type IsolationLevelName,
|
|
5
|
+
} from "@deterministic-code/datasources";
|
|
6
|
+
import type { PostgresConnection } from "./connection.ts";
|
|
7
|
+
|
|
8
|
+
export class PostgresTransaction implements ITransaction {
|
|
9
|
+
constructor(
|
|
10
|
+
readonly connection: PostgresConnection,
|
|
11
|
+
readonly isolation: IsolationLevelName = IsolationLevel.ReadCommitted,
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
async commit(): Promise<void> {
|
|
15
|
+
await this.connection.exec("COMMIT");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async abort(): Promise<void> {
|
|
19
|
+
await this.connection.exec("ROLLBACK");
|
|
20
|
+
}
|
|
21
|
+
}
|
package/README.md
DELETED