drizzle-edge-pg-proxy-client
Version:
PostgreSQL HTTP client compatible with Neon's interface for edge environments
416 lines (359 loc) • 13.7 kB
text/typescript
// PostgreSQL Data Types IDs
// Based on https://github.com/brianc/node-pg-types/blob/master/lib/builtins.js
export enum PgTypeId {
BOOL = 16,
BYTEA = 17,
CHAR = 18,
INT8 = 20,
INT2 = 21,
INT4 = 23,
REGPROC = 24,
TEXT = 25,
OID = 26,
TID = 27,
XID = 28,
CID = 29,
JSON = 114,
XML = 142,
PG_NODE_TREE = 194,
JSONB = 3802,
FLOAT4 = 700,
FLOAT8 = 701,
ABSTIME = 702,
RELTIME = 703,
TINTERVAL = 704,
CIRCLE = 718,
MONEY = 790,
MACADDR = 829,
INET = 869,
CIDR = 650,
MACADDR8 = 774,
ACLITEM = 1033,
BPCHAR = 1042,
VARCHAR = 1043,
DATE = 1082,
TIME = 1083,
TIMESTAMP = 1114,
TIMESTAMPTZ = 1184,
INTERVAL = 1186,
TIMETZ = 1266,
BIT = 1560,
VARBIT = 1562,
NUMERIC = 1700,
REFCURSOR = 1790,
REGPROCEDURE = 2202,
REGOPER = 2203,
REGOPERATOR = 2204,
REGCLASS = 2205,
REGTYPE = 2206,
UUID = 2950,
TXID_SNAPSHOT = 2970,
PG_LSN = 3220,
PG_NDISTINCT = 3361,
PG_DEPENDENCIES = 3402,
TSVECTOR = 3614,
TSQUERY = 3615,
GTSVECTOR = 3642,
REGCONFIG = 3734,
REGDICTIONARY = 3769,
JSONPATH = 4072,
REGNAMESPACE = 4089,
REGROLE = 4096,
// Array types (OIDs are element type OID + 1 dimension)
// Defining common array types explicitly
BOOL_ARRAY = 1000, // Boolean Array -> 16 + 1000 - 16 = 1000
BYTEA_ARRAY = 1001, // Bytea Array -> 17 + 1000 - 16 = 1001
CHAR_ARRAY = 1002, // Char Array -> 18 + 1000 - 16 = 1002
INT8_ARRAY = 1016, // BigInt Array -> 20 + 1000 - 4 = 1016
INT2_ARRAY = 1005, // SmallInt Array -> 21 + 1000 - 16 = 1005
INT4_ARRAY = 1007, // Integer Array -> 23 + 1000 - 16 = 1007
TEXT_ARRAY = 1009, // Text Array -> 25 + 1000 - 16 = 1009
JSON_ARRAY = 199, // JSON Array -> 114 + 1000 - 915 = 199
JSONB_ARRAY = 3807, // JSONB Array -> 3802 + 5 = 3807
FLOAT4_ARRAY = 1021, // Float4 Array -> 700 + 1000 - 679 = 1021
FLOAT8_ARRAY = 1022, // Float8 Array -> 701 + 1000 - 679 = 1022
NUMERIC_ARRAY = 1231, // Numeric Array -> 1700 + 1000 - 1469 = 1231
DATE_ARRAY = 1182, // Date Array -> 1082 + 100 = 1182
TIMESTAMP_ARRAY = 1115, // Timestamp Array -> 1114 + 1 = 1115
TIMESTAMPTZ_ARRAY = 1185, // Timestamptz Array -> 1184 + 1 = 1185
UUID_ARRAY = 2951, // UUID Array -> 2950 + 1 = 2951
VARCHAR_ARRAY = 1015, // VarChar Array -> 1043 + 1000 - 1028 = 1015
}
// Helper function to check if a type is an array type
export function isArrayType(typeId: number): boolean {
// Array types in PostgreSQL are typically in ranges that follow patterns
return (
(typeId >= 1000 && typeId <= 1099) || // Common 1-dimensional array types
(typeId >= 1115 && typeId <= 1185) || // Date/time array types
typeId === 199 || // JSON array
typeId === 3807 || // JSONB array
typeId === 1231 || // Numeric array
typeId === 2951 // UUID array
// Add other ranges as needed
);
}
// Get the element type ID for an array type ID
export function getElementTypeId(arrayTypeId: number): number {
// Some common mappings for array types to their element types
const arrayToElementMap: Record<number, number> = {
1000: PgTypeId.BOOL, // Boolean array -> Boolean
1001: PgTypeId.BYTEA, // Bytea array -> Bytea
1002: PgTypeId.CHAR, // Char array -> Char
1005: PgTypeId.INT2, // SmallInt array -> SmallInt
1007: PgTypeId.INT4, // Integer array -> Integer
1009: PgTypeId.TEXT, // Text array -> Text
1016: PgTypeId.INT8, // BigInt array -> BigInt
1021: PgTypeId.FLOAT4, // Float4 array -> Float4
1022: PgTypeId.FLOAT8, // Float8 array -> Float8
1231: PgTypeId.NUMERIC, // Numeric array -> Numeric
1015: PgTypeId.VARCHAR, // VarChar array -> VarChar
1182: PgTypeId.DATE, // Date array -> Date
1115: PgTypeId.TIMESTAMP, // Timestamp array -> Timestamp
1185: PgTypeId.TIMESTAMPTZ, // TimestampTZ array -> TimestampTZ
199: PgTypeId.JSON, // JSON array -> JSON
3807: PgTypeId.JSONB, // JSONB array -> JSONB
2951: PgTypeId.UUID, // UUID array -> UUID
};
return arrayToElementMap[arrayTypeId] || 0;
}
// Type parser for PostgreSQL types
export class TypeParser {
private parsers: Record<number, (value: string) => any> = {};
constructor(customTypes?: Record<number, (value: string) => any>) {
// Initialize with default parsers
this.initializeDefaultParsers();
// Add custom type parsers if provided
if (customTypes) {
Object.keys(customTypes).forEach(key => {
const typeId = parseInt(key, 10);
if (!isNaN(typeId) && customTypes[typeId]) {
this.setTypeParser(typeId, customTypes[typeId] as (value: string) => any);
}
});
}
}
private initializeDefaultParsers() {
// Boolean type
this.setTypeParser(PgTypeId.BOOL, val => val === 't' || val === 'true');
// Integer types
this.setTypeParser(PgTypeId.INT2, val => parseInt(val, 10));
this.setTypeParser(PgTypeId.INT4, val => parseInt(val, 10));
this.setTypeParser(PgTypeId.INT8, val => BigInt(val));
this.setTypeParser(PgTypeId.OID, val => parseInt(val, 10));
// Floating point types
this.setTypeParser(PgTypeId.FLOAT4, val => parseFloat(val));
this.setTypeParser(PgTypeId.FLOAT8, val => parseFloat(val));
this.setTypeParser(PgTypeId.NUMERIC, val => parseFloat(val));
// JSON types
this.setTypeParser(PgTypeId.JSON, val => JSON.parse(val));
this.setTypeParser(PgTypeId.JSONB, val => JSON.parse(val));
// Date/Time types
this.setTypeParser(PgTypeId.DATE, val => new Date(val));
this.setTypeParser(PgTypeId.TIMESTAMP, val => new Date(val));
this.setTypeParser(PgTypeId.TIMESTAMPTZ, val => new Date(val));
// UUID
this.setTypeParser(PgTypeId.UUID, val => val);
// Set up array type parsers
this.setupArrayTypeParsers();
}
private setupArrayTypeParsers() {
// Define array type parsers for all base types
// Boolean array
this.setTypeParser(PgTypeId.BOOL_ARRAY, val =>
parsePostgresArray(val, this.getTypeParser(PgTypeId.BOOL)));
// Integer arrays
this.setTypeParser(PgTypeId.INT2_ARRAY, val =>
parsePostgresArray(val, this.getTypeParser(PgTypeId.INT2)));
this.setTypeParser(PgTypeId.INT4_ARRAY, val =>
parsePostgresArray(val, this.getTypeParser(PgTypeId.INT4)));
this.setTypeParser(PgTypeId.INT8_ARRAY, val =>
parsePostgresArray(val, this.getTypeParser(PgTypeId.INT8)));
// Floating point arrays
this.setTypeParser(PgTypeId.FLOAT4_ARRAY, val =>
parsePostgresArray(val, this.getTypeParser(PgTypeId.FLOAT4)));
this.setTypeParser(PgTypeId.FLOAT8_ARRAY, val =>
parsePostgresArray(val, this.getTypeParser(PgTypeId.FLOAT8)));
this.setTypeParser(PgTypeId.NUMERIC_ARRAY, val =>
parsePostgresArray(val, this.getTypeParser(PgTypeId.NUMERIC)));
// Text arrays
this.setTypeParser(PgTypeId.TEXT_ARRAY, val =>
parsePostgresArray(val, this.getTypeParser(PgTypeId.TEXT)));
this.setTypeParser(PgTypeId.VARCHAR_ARRAY, val =>
parsePostgresArray(val, this.getTypeParser(PgTypeId.VARCHAR)));
// JSON arrays
this.setTypeParser(PgTypeId.JSON_ARRAY, val =>
parsePostgresArray(val, this.getTypeParser(PgTypeId.JSON)));
this.setTypeParser(PgTypeId.JSONB_ARRAY, val =>
parsePostgresArray(val, this.getTypeParser(PgTypeId.JSONB)));
// Date/Time arrays
this.setTypeParser(PgTypeId.DATE_ARRAY, val =>
parsePostgresArray(val, this.getTypeParser(PgTypeId.DATE)));
this.setTypeParser(PgTypeId.TIMESTAMP_ARRAY, val =>
parsePostgresArray(val, this.getTypeParser(PgTypeId.TIMESTAMP)));
this.setTypeParser(PgTypeId.TIMESTAMPTZ_ARRAY, val =>
parsePostgresArray(val, this.getTypeParser(PgTypeId.TIMESTAMPTZ)));
// UUID array
this.setTypeParser(PgTypeId.UUID_ARRAY, val =>
parsePostgresArray(val, this.getTypeParser(PgTypeId.UUID)));
}
public setTypeParser(typeId: number, parseFn: (value: string) => any): void {
this.parsers[typeId] = parseFn;
}
public getTypeParser(typeId: number): (value: string) => any {
// If we're asked for a parser for an array type that doesn't have one explicitly defined,
// create an array parser dynamically using the element type parser
if (isArrayType(typeId) && !this.parsers[typeId]) {
const elementTypeId = getElementTypeId(typeId);
if (elementTypeId) {
const elementParser = this.getTypeParser(elementTypeId);
return (value: string) => parsePostgresArray(value, elementParser);
}
}
return this.parsers[typeId] || (value => value);
}
}
/**
* Parse a PostgreSQL array string into a JavaScript array
* This handles various array types and dimensions using the appropriate element parser
*/
export function parsePostgresArray(arrayString: string, elementParser: (value: string) => any): any[] {
if (!arrayString || arrayString === '{}') return [];
if (arrayString[0] !== '{' || arrayString[arrayString.length - 1] !== '}') {
throw new Error(`Invalid PostgreSQL array format: ${arrayString}`);
}
// Extract the content inside the outer braces
const content = arrayString.substring(1, arrayString.length - 1);
// Quick return for empty arrays
if (!content) return [];
const result: any[] = [];
let inQuotes = false;
let inEscape = false;
let currentItem = '';
let nestLevel = 0;
// Process each character to handle quotes, escapes, and nested arrays
for (let i = 0; i < content.length; i++) {
const char = content[i];
// Handle escape sequences
if (inEscape) {
currentItem += char;
inEscape = false;
continue;
}
// Start of escape sequence
if (char === '\\') {
inEscape = true;
continue;
}
// Toggle quote mode when we see a quote
if (char === '"') {
inQuotes = !inQuotes;
continue;
}
// Track nesting level for nested arrays
if (char === '{') {
nestLevel++;
currentItem += char;
continue;
}
if (char === '}') {
nestLevel--;
currentItem += char;
continue;
}
// If we're inside quotes or a nested array, add the character to current item
if (inQuotes || nestLevel > 0) {
currentItem += char;
continue;
}
// Handle item separator (comma)
if (char === ',') {
// Process the completed item
result.push(parseArrayItem(currentItem, elementParser));
currentItem = '';
continue;
}
// Normal character, add to current item
currentItem += char;
}
// Add the last item if there is one
if (currentItem) {
result.push(parseArrayItem(currentItem, elementParser));
}
return result;
}
/**
* Parse a single item from a PostgreSQL array
*/
export function parseArrayItem(item: string, elementParser: (value: string) => any): any {
// Trim whitespace
item = item.trim();
// Handle NULL values
if (item === 'NULL' || item === '') {
return null;
}
// Handle nested arrays recursively
if (item[0] === '{' && item[item.length - 1] === '}') {
return parsePostgresArray(item, elementParser);
}
// Parse normal values using the element parser
return elementParser(item);
}
// Process raw query results to apply type parsing
export function processQueryResult(
result: any,
typeParser: TypeParser,
arrayMode: boolean
): any {
if (!result) return null;
const fields = result.fields || [];
const rowsData = result.rows || [];
// Create parsers for each column based on its data type
const parsers = fields.map((field: { dataTypeID: number }) => {
const typeId = field.dataTypeID;
// If it's an array type, create a special parser that handles the array format
// and applies the element type parser to each item
if (isArrayType(typeId)) {
const elementTypeId = getElementTypeId(typeId);
const elementParser = typeParser.getTypeParser(elementTypeId);
// Return a function that parses the PostgreSQL array format
return (value: string) => parsePostgresArray(value, elementParser);
}
// For non-array types, use the normal type parser
return typeParser.getTypeParser(typeId);
});
// Extract column names
const colNames = fields.map((field: { name: string }) => field.name);
// Process rows with type parsers - with additional safety checks for Auth.js compatibility
let processedRows: any[] = [];
if (Array.isArray(rowsData)) {
processedRows = arrayMode
? rowsData.map((row: any) => {
// Ensure row is an array before trying to map over it
if (!Array.isArray(row)) {
return row; // Return as is if not an array
}
return row.map((val, i) => val === null ? null : parsers[i](val));
})
: rowsData.map((row: any) => {
// Handle cases where row might not be an array (Auth.js sometimes sends objects directly)
if (!Array.isArray(row)) {
return row; // Return as is if not an array
}
const obj: Record<string, any> = {};
row.forEach((val, i) => {
if (i < colNames.length) { // Ensure index is within bounds
obj[colNames[i]] = val === null ? null : parsers[i](val);
}
});
return obj;
});
}
// Return a complete result object
return {
...result,
rows: processedRows,
rowAsArray: arrayMode,
_parsers: parsers,
_types: typeParser
};
}