Skip to content

Commit 170903b

Browse files
committed
Ported pg-format directly into dumbo
It's not maintained and causing compatibility issues in Cloudflare Workers
1 parent b39aafc commit 170903b

File tree

10 files changed

+1269
-592
lines changed

10 files changed

+1269
-592
lines changed

src/package-lock.json

Lines changed: 483 additions & 560 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/package.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@
7070
"@types/node": "20.11.30",
7171
"@types/pg": "^8.11.6",
7272
"@types/pg-connection-string": "^2.0.0",
73-
"@types/pg-format": "^1.0.5",
7473
"@types/uuid": "9.0.8",
7574
"@typescript-eslint/eslint-plugin": "7.9.0",
7675
"@typescript-eslint/parser": "7.9.0",
@@ -91,13 +90,11 @@
9190
"peerDependencies": {
9291
"pg": "^8.12.0",
9392
"pg-connection-string": "^2.6.4",
94-
"pg-format": "^1.0.4",
9593
"testcontainers": "^10.10.1"
9694
},
9795
"dependencies": {
9896
"cpy-cli": "^5.0.0",
99-
"pg": "^8.12.0",
100-
"pg-format": "^1.0.4"
97+
"pg": "^8.12.0"
10198
},
10299
"workspaces": [
103100
"packages/dumbo",

src/packages/dumbo/package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,9 @@
4949
"peerDependencies": {
5050
"@types/pg": "^8.11.6",
5151
"@types/pg-connection-string": "^2.0.0",
52-
"@types/pg-format": "^1.0.5",
5352
"@types/uuid": "^9.0.8",
5453
"pg": "^8.12.0",
5554
"pg-connection-string": "^2.6.4",
56-
"pg-format": "^1.0.4",
5755
"uuid": "^9.0.1"
5856
},
5957
"devDependencies": {

src/packages/dumbo/src/core/sql/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import format from 'pg-format';
1+
import format from './pg-format';
22
// TODO: add core formatter, when adding other database type
33

44
export type SQL = string & { __brand: 'sql' };
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
// Ported from: https://github.com/datalanche/node-pg-format/blob/master/lib/index.js
2+
import reservedMap from './reserved.js';
3+
4+
type FormatterConfig = {
5+
pattern?: {
6+
ident?: string;
7+
literal?: string;
8+
string?: string;
9+
};
10+
};
11+
12+
type FormatterFunction = (value: unknown) => string;
13+
14+
const fmtPattern = {
15+
ident: 'I',
16+
literal: 'L',
17+
string: 's',
18+
};
19+
20+
// convert to Postgres default ISO 8601 format
21+
const formatDate = (date: string): string => {
22+
date = date.replace('T', ' ');
23+
date = date.replace('Z', '+00');
24+
return date;
25+
};
26+
27+
const isReserved = (value: string): boolean => {
28+
return !!reservedMap[value.toUpperCase()];
29+
};
30+
31+
const arrayToList = (
32+
useSpace: boolean,
33+
array: unknown[],
34+
formatter: FormatterFunction,
35+
): string => {
36+
let sql = '';
37+
sql += useSpace ? ' (' : '(';
38+
for (let i = 0; i < array.length; i++) {
39+
sql += (i === 0 ? '' : ', ') + formatter(array[i]);
40+
}
41+
sql += ')';
42+
return sql;
43+
};
44+
45+
// Ported from PostgreSQL 9.2.4 source code in src/interfaces/libpq/fe-exec.c
46+
const quoteIdent = (value: unknown): string => {
47+
if (value === undefined || value === null) {
48+
throw new Error('SQL identifier cannot be null or undefined');
49+
} else if (value === false) {
50+
return '"f"';
51+
} else if (value === true) {
52+
return '"t"';
53+
} else if (value instanceof Date) {
54+
return '"' + formatDate(value.toISOString()) + '"';
55+
} else if (value instanceof Buffer) {
56+
throw new Error('SQL identifier cannot be a buffer');
57+
} else if (Array.isArray(value)) {
58+
return value
59+
.map((v) => {
60+
if (Array.isArray(v)) {
61+
throw new Error(
62+
'Nested array to grouped list conversion is not supported for SQL identifier',
63+
);
64+
}
65+
return quoteIdent(v);
66+
})
67+
.toString();
68+
} else if (value === Object(value)) {
69+
throw new Error('SQL identifier cannot be an object');
70+
}
71+
72+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
73+
const ident = value.toString().slice(0); // create copy
74+
75+
// do not quote a valid, unquoted identifier
76+
if (/^[a-z_][a-z0-9_$]*$/.test(ident) && !isReserved(ident)) {
77+
return ident;
78+
}
79+
80+
let quoted = '"';
81+
for (let i = 0; i < ident.length; i++) {
82+
const c = ident[i];
83+
quoted += c === '"' ? c + c : c;
84+
}
85+
quoted += '"';
86+
return quoted;
87+
};
88+
89+
// Ported from PostgreSQL 9.2.4 source code in src/interfaces/libpq/fe-exec.c
90+
const quoteLiteral = (value: unknown): string => {
91+
let literal: string | null = null;
92+
let explicitCast: string | null = null;
93+
94+
if (value === undefined || value === null) {
95+
return 'NULL';
96+
} else if (value === false) {
97+
return "'f'";
98+
} else if (value === true) {
99+
return "'t'";
100+
} else if (value instanceof Date) {
101+
return "'" + formatDate(value.toISOString()) + "'";
102+
} else if (value instanceof Buffer) {
103+
return "E'\\\\x" + value.toString('hex') + "'";
104+
} else if (Array.isArray(value)) {
105+
return value
106+
.map((v, i) => {
107+
if (Array.isArray(v)) {
108+
return arrayToList(i !== 0, v, quoteLiteral);
109+
}
110+
return quoteLiteral(v);
111+
})
112+
.toString();
113+
} else if (value === Object(value)) {
114+
explicitCast = 'jsonb';
115+
literal = JSON.stringify(value);
116+
} else {
117+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
118+
literal = value.toString().slice(0); // create copy
119+
}
120+
121+
let hasBackslash = false;
122+
let quoted = "'";
123+
124+
for (let i = 0; i < literal.length; i++) {
125+
const c = literal[i];
126+
if (c === "'") {
127+
quoted += c + c;
128+
} else if (c === '\\') {
129+
quoted += c + c;
130+
hasBackslash = true;
131+
} else {
132+
quoted += c;
133+
}
134+
}
135+
136+
quoted += "'";
137+
138+
if (hasBackslash) {
139+
quoted = 'E' + quoted;
140+
}
141+
142+
if (explicitCast) {
143+
quoted += '::' + explicitCast;
144+
}
145+
146+
return quoted;
147+
};
148+
149+
const quoteString = (value: unknown): string => {
150+
if (value === undefined || value === null) {
151+
return '';
152+
} else if (value === false) {
153+
return 'f';
154+
} else if (value === true) {
155+
return 't';
156+
} else if (value instanceof Date) {
157+
return formatDate(value.toISOString());
158+
} else if (value instanceof Buffer) {
159+
return '\\x' + value.toString('hex');
160+
} else if (Array.isArray(value)) {
161+
return value
162+
.map((v, i) => {
163+
if (v !== null && v !== undefined) {
164+
if (Array.isArray(v)) {
165+
return arrayToList(i !== 0, v, quoteString);
166+
}
167+
return quoteString(v);
168+
}
169+
return ''; // Handle undefined or null values properly within arrays
170+
})
171+
.filter((v) => v !== '') // Filter out empty strings to avoid extra commas
172+
.toString();
173+
} else if (value === Object(value)) {
174+
return JSON.stringify(value);
175+
}
176+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
177+
return value.toString().slice(0); // return copy
178+
};
179+
180+
const config = (cfg: FormatterConfig): void => {
181+
// default
182+
fmtPattern.ident = 'I';
183+
fmtPattern.literal = 'L';
184+
fmtPattern.string = 's';
185+
186+
if (cfg && cfg.pattern) {
187+
if (cfg.pattern.ident) {
188+
fmtPattern.ident = cfg.pattern.ident;
189+
}
190+
if (cfg.pattern.literal) {
191+
fmtPattern.literal = cfg.pattern.literal;
192+
}
193+
if (cfg.pattern.string) {
194+
fmtPattern.string = cfg.pattern.string;
195+
}
196+
}
197+
};
198+
199+
const formatWithArray = (fmt: string, parameters: unknown[]): string => {
200+
let index = 0;
201+
const params = parameters;
202+
203+
let re: string | RegExp = '%(%|(\\d+\\$)?[';
204+
re += fmtPattern.ident;
205+
re += fmtPattern.literal;
206+
re += fmtPattern.string;
207+
re += '])';
208+
re = new RegExp(re, 'g');
209+
210+
return fmt.replace(re, (_, type) => {
211+
if (type === '%') {
212+
return '%';
213+
}
214+
215+
let position = index;
216+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
217+
const tokens = type.split('$');
218+
219+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
220+
if (tokens.length > 1) {
221+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
222+
position = parseInt(tokens[0], 10) - 1;
223+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
224+
type = tokens[1];
225+
}
226+
227+
if (position < 0) {
228+
throw new Error('specified argument 0 but arguments start at 1');
229+
} else if (position > params.length - 1) {
230+
throw new Error('too few arguments');
231+
}
232+
233+
index = position + 1;
234+
235+
if (type === fmtPattern.ident) {
236+
return quoteIdent(params[position]);
237+
} else if (type === fmtPattern.literal) {
238+
return quoteLiteral(params[position]);
239+
} else if (type === fmtPattern.string) {
240+
return quoteString(params[position]);
241+
}
242+
243+
return undefined!;
244+
});
245+
};
246+
247+
const format = (fmt: string, ...args: unknown[]): string => {
248+
return formatWithArray(fmt, args);
249+
};
250+
251+
format.config = config;
252+
format.format = format;
253+
format.ident = quoteIdent;
254+
format.literal = quoteLiteral;
255+
format.string = quoteString;
256+
format.withArray = formatWithArray;
257+
258+
export default format;

0 commit comments

Comments
 (0)