forked from boos/BoosAPI
[codex] fix cross-database JSON boundary handling (#283)
* fix: normalize cross-database json boundaries * fix: address json boundary review comments --------- Co-authored-by: apple <apple@appledeMacBook-Pro.local>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { mkdtempSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
type DbModule = typeof import('./index.js');
|
||||
|
||||
@@ -14,6 +14,10 @@ describe('db proxy query wrapper', () => {
|
||||
testUtils = dbModule.__dbProxyTestUtils;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
testUtils.resetPostgresJsonTextParsersInstallStateForTests();
|
||||
});
|
||||
|
||||
it('wraps thenable query builders and provides all/get shims', async () => {
|
||||
const execute = vi.fn(async () => [{ id: 1, name: 'demo' }]);
|
||||
const queryLike = {
|
||||
@@ -53,5 +57,46 @@ describe('db proxy query wrapper', () => {
|
||||
expect(runResult).toEqual({ changes: 3, lastInsertRowid: 9 });
|
||||
expect(testUtils.shouldWrapObject(Promise.resolve())).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('builds mysql pool options with jsonStrings enabled', () => {
|
||||
expect(testUtils.buildMysqlPoolOptions('mysql://root:pass@db.example.com:3306/metapi', false)).toMatchObject({
|
||||
uri: 'mysql://root:pass@db.example.com:3306/metapi',
|
||||
jsonStrings: true,
|
||||
});
|
||||
expect(testUtils.buildMysqlPoolOptions('mysql://root:pass@db.example.com:3306/metapi', true)).toMatchObject({
|
||||
uri: 'mysql://root:pass@db.example.com:3306/metapi',
|
||||
jsonStrings: true,
|
||||
ssl: { rejectUnauthorized: false },
|
||||
});
|
||||
});
|
||||
|
||||
it('builds postgres pool options with ssl when requested', () => {
|
||||
expect(testUtils.buildPostgresPoolOptions('postgres://user:pass@db.example.com:5432/metapi', false)).toEqual({
|
||||
connectionString: 'postgres://user:pass@db.example.com:5432/metapi',
|
||||
});
|
||||
expect(testUtils.buildPostgresPoolOptions('postgres://user:pass@db.example.com:5432/metapi', true)).toMatchObject({
|
||||
connectionString: 'postgres://user:pass@db.example.com:5432/metapi',
|
||||
ssl: { rejectUnauthorized: false },
|
||||
});
|
||||
});
|
||||
|
||||
it('installs postgres JSON text parsers idempotently', () => {
|
||||
const setTypeParser = vi.fn();
|
||||
const fakeTypes = {
|
||||
builtins: {
|
||||
JSON: 114,
|
||||
JSONB: 3802,
|
||||
},
|
||||
setTypeParser,
|
||||
};
|
||||
|
||||
testUtils.installPostgresJsonTextParsers(fakeTypes as any);
|
||||
testUtils.installPostgresJsonTextParsers(fakeTypes as any);
|
||||
|
||||
expect(setTypeParser).toHaveBeenCalledTimes(2);
|
||||
expect(setTypeParser).toHaveBeenNthCalledWith(1, 114, 'text', expect.any(Function));
|
||||
expect(setTypeParser).toHaveBeenNthCalledWith(2, 3802, 'text', expect.any(Function));
|
||||
const parser = setTypeParser.mock.calls[0][2] as (value: string) => string;
|
||||
expect(parser('{"ok":true}')).toBe('{"ok":true}');
|
||||
});
|
||||
});
|
||||
|
||||
+38
-9
@@ -5,6 +5,10 @@ import { drizzle as drizzleSqliteProxy } from 'drizzle-orm/sqlite-proxy';
|
||||
import { drizzle as drizzleMysqlProxy } from 'drizzle-orm/mysql-proxy';
|
||||
import { drizzle as drizzlePgProxy } from 'drizzle-orm/pg-proxy';
|
||||
import * as schema from './schema.js';
|
||||
import {
|
||||
installPostgresJsonTextParsers,
|
||||
resetPostgresJsonTextParsersInstallStateForTests,
|
||||
} from './postgresJsonTextParsers.js';
|
||||
import { ensureSiteSchemaCompatibility, type SiteSchemaInspector } from './siteSchemaCompatibility.js';
|
||||
import { ensureRouteGroupingSchemaCompatibility } from './routeGroupingSchemaCompatibility.js';
|
||||
import { ensureProxyFileSchemaCompatibility } from './proxyFileSchemaCompatibility.js';
|
||||
@@ -46,6 +50,31 @@ let proxyLogBillingDetailsColumnAvailable: boolean | null = null;
|
||||
let proxyLogDownstreamApiKeyIdColumnAvailable: boolean | null = null;
|
||||
let proxyLogClientColumnsAvailable: boolean | null = null;
|
||||
|
||||
function buildMysqlPoolOptions(
|
||||
connectionString = config.dbUrl,
|
||||
sslEnabled = config.dbSsl,
|
||||
): mysql.PoolOptions {
|
||||
const poolOptions: mysql.PoolOptions = {
|
||||
uri: connectionString,
|
||||
jsonStrings: true,
|
||||
};
|
||||
if (sslEnabled) {
|
||||
poolOptions.ssl = { rejectUnauthorized: false };
|
||||
}
|
||||
return poolOptions;
|
||||
}
|
||||
|
||||
function buildPostgresPoolOptions(
|
||||
connectionString = config.dbUrl,
|
||||
sslEnabled = config.dbSsl,
|
||||
): pg.PoolConfig {
|
||||
const poolOptions: pg.PoolConfig = { connectionString };
|
||||
if (sslEnabled) {
|
||||
poolOptions.ssl = { rejectUnauthorized: false };
|
||||
}
|
||||
return poolOptions;
|
||||
}
|
||||
|
||||
function resolveSqlitePath(): string {
|
||||
const raw = (config.dbUrl || '').trim();
|
||||
if (!raw) {
|
||||
@@ -1238,11 +1267,7 @@ function initMysqlDb(): AppDb {
|
||||
if (!config.dbUrl) {
|
||||
throw new Error('DB_URL is required when DB_TYPE=mysql');
|
||||
}
|
||||
const poolOptions: mysql.PoolOptions = { uri: config.dbUrl };
|
||||
if (config.dbSsl) {
|
||||
poolOptions.ssl = { rejectUnauthorized: false };
|
||||
}
|
||||
mysqlPool = mysql.createPool(poolOptions);
|
||||
mysqlPool = mysql.createPool(buildMysqlPoolOptions());
|
||||
|
||||
const rawDb = drizzleMysqlProxy(
|
||||
(sqlText, params, method) => mysqlProxyQuery(mysqlPool!, sqlText, params, method as SqlMethod),
|
||||
@@ -1274,10 +1299,8 @@ function initPostgresDb(): AppDb {
|
||||
if (!config.dbUrl) {
|
||||
throw new Error('DB_URL is required when DB_TYPE=postgres');
|
||||
}
|
||||
const poolOptions: pg.PoolConfig = { connectionString: config.dbUrl };
|
||||
if (config.dbSsl) {
|
||||
poolOptions.ssl = { rejectUnauthorized: false };
|
||||
}
|
||||
installPostgresJsonTextParsers();
|
||||
const poolOptions = buildPostgresPoolOptions();
|
||||
pgPool = new pg.Pool(poolOptions);
|
||||
|
||||
const rawDb = drizzlePgProxy(
|
||||
@@ -1375,4 +1398,10 @@ export const __dbProxyTestUtils = {
|
||||
shouldWrapObject,
|
||||
resolveSqlitePath,
|
||||
resolveVitestSqlitePath,
|
||||
buildMysqlPoolOptions,
|
||||
buildPostgresPoolOptions,
|
||||
installPostgresJsonTextParsers,
|
||||
ensurePostgresJsonTextParsers: installPostgresJsonTextParsers,
|
||||
resetPostgresJsonTextParsersInstallStateForTests,
|
||||
pg,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import pg from 'pg';
|
||||
|
||||
export type PgTypesLike = {
|
||||
builtins: {
|
||||
JSON: number;
|
||||
JSONB: number;
|
||||
};
|
||||
setTypeParser(oid: number, format: 'text', parser: (value: string) => string): void;
|
||||
};
|
||||
|
||||
let postgresJsonTextParsersInstalled = false;
|
||||
|
||||
export function installPostgresJsonTextParsers(typesRegistry: PgTypesLike = pg.types as PgTypesLike): void {
|
||||
if (postgresJsonTextParsersInstalled) return;
|
||||
const identity = (value: string) => value;
|
||||
typesRegistry.setTypeParser(typesRegistry.builtins.JSON, 'text', identity);
|
||||
typesRegistry.setTypeParser(typesRegistry.builtins.JSONB, 'text', identity);
|
||||
postgresJsonTextParsersInstalled = true;
|
||||
}
|
||||
|
||||
export function resetPostgresJsonTextParsersInstallStateForTests(): void {
|
||||
postgresJsonTextParsersInstalled = false;
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
generateUpgradeSql,
|
||||
type MysqlIndexPrefixRequirementMap,
|
||||
} from './schemaArtifactGenerator.js';
|
||||
import { installPostgresJsonTextParsers } from './postgresJsonTextParsers.js';
|
||||
import { introspectLiveSchema } from './schemaIntrospection.js';
|
||||
import { resolveGeneratedSchemaContractPath, type SchemaContract } from './schemaContract.js';
|
||||
|
||||
@@ -324,6 +325,7 @@ async function createPostgresClient(connectionString: string, ssl: boolean): Pro
|
||||
if (ssl) {
|
||||
clientOptions.ssl = { rejectUnauthorized: false };
|
||||
}
|
||||
installPostgresJsonTextParsers();
|
||||
const client = new pg.Client(clientOptions);
|
||||
await client.connect();
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
SchemaContractUnique,
|
||||
} from './schemaContract.js';
|
||||
import { resolveMigrationsFolder } from './schemaContract.js';
|
||||
import { installPostgresJsonTextParsers } from './postgresJsonTextParsers.js';
|
||||
import {
|
||||
normalizeLogicalColumnType,
|
||||
type LogicalColumnType,
|
||||
@@ -535,6 +536,7 @@ async function introspectPostgresSchema(input: SchemaIntrospectionInput): Promis
|
||||
if (input.ssl) {
|
||||
clientOptions.ssl = { rejectUnauthorized: false };
|
||||
}
|
||||
installPostgresJsonTextParsers();
|
||||
const client = new pg.Client(clientOptions);
|
||||
await client.connect();
|
||||
|
||||
@@ -785,6 +787,7 @@ async function applyPostgresStatements(
|
||||
if (ssl) {
|
||||
clientOptions.ssl = { rejectUnauthorized: false };
|
||||
}
|
||||
installPostgresJsonTextParsers();
|
||||
const client = new pg.Client(clientOptions);
|
||||
await client.connect();
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user