pg-tx
Version:
Transaction wrapper for node-postgres
147 lines • 7 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const pg_1 = require("pg");
const ts_mockito_1 = require("ts-mockito");
const _1 = __importDefault(require("."));
function testWithClient(makeClient, destroyClient) {
let pg;
beforeEach(async () => {
pg = await makeClient();
await pg.query(`CREATE TABLE things (
id SERIAL PRIMARY KEY,
thing TEXT NOT NULL
)`);
// https://dba.stackexchange.com/a/293929/200560
await pg.query(`
CREATE TABLE test_defer(
x int CONSTRAINT test_me UNIQUE DEFERRABLE INITIALLY DEFERRED
);
`);
});
afterEach(async () => {
await pg.query(`DROP TABLE things`);
await pg.query(`DROP TABLE test_defer`);
await destroyClient(pg);
});
it(`commits changes when there's no error`, async () => {
await _1.default(pg, async (db) => {
db.query(`INSERT INTO things (thing) VALUES ('committed')`);
});
const { rows: [committed] } = await pg.query(`SELECT id, thing FROM things WHERE thing = 'committed'`);
expect(committed).toBeDefined();
expect(committed.thing).toEqual('committed');
});
it(`doesn't commit changes with forcedRollback`, async () => {
await _1.default(pg, async (db) => {
await db.query(`INSERT INTO things (thing) VALUES ('committed')`);
}, true);
const { rowCount } = await pg.query(`SELECT id, thing FROM things WHERE thing = 'committed'`);
expect(rowCount).toBe(0);
});
it(`doesn't commit changes when there's a Node exception`, async () => {
await expect(_1.default(pg, async (db) => {
await db.query(`INSERT INTO things (thing) VALUES ('node_error')`);
throw new Error(`node error`);
})).rejects.toThrowError(`node error`);
const { rowCount } = await pg.query(`SELECT id, thing FROM things WHERE thing = 'node_error'`);
expect(rowCount).toBe(0);
});
it(`doesn't commit changes when there's a query error`, async () => {
await expect(_1.default(pg, async (db) => {
await db.query(`INSERT INTO things(thing) VALUES ('query_error')`);
await db.query(`this query has an error`);
})).rejects.toThrowErrorMatchingInlineSnapshot(`"syntax error at or near \\"this\\""`);
const { rowCount } = await pg.query(`SELECT id, thing FROM things WHERE thing = 'query_error'`);
expect(rowCount).toBe(0);
});
it(`doesn't commit changes on next tick after query error`, async () => {
let laterPromise;
const txPromise = _1.default(pg, async (db) => {
laterPromise = (async () => {
try {
await txPromise;
}
catch (e) {
// ignore error, we just needed to wait until it completed
}
await new Promise((resolve) => {
setImmediate(resolve);
});
await db.query(`INSERT INTO things(thing) VALUES ('query_error_tick')`);
})();
await Promise.all([db.query(`this query has an error`), laterPromise]);
});
await expect(txPromise).rejects.toThrowErrorMatchingInlineSnapshot(`"syntax error at or near \\"this\\""`);
await expect(laterPromise).rejects.toThrowErrorMatchingInlineSnapshot(`"Transaction client already released, see more: https://tinyurl.com/py2upfcw"`);
const { rowCount } = await pg.query(`SELECT id, thing FROM things WHERE thing = 'query_error_tick'`);
expect(rowCount).toBe(0);
});
it(`handles nested transactions`, async () => {
await _1.default(pg, async (rootDb) => {
await _1.default(rootDb, async (comittedDb) => {
await comittedDb.query(`INSERT INTO things (thing) VALUES ('committed')`);
});
await expect(_1.default(rootDb, async (rolledbackDb) => {
await rolledbackDb.query(`INSERT INTO things (thing) VALUES ('rolledback')`);
throw new Error(`rollback this block`);
})).rejects.toThrowError(`rollback this block`);
});
const { rowCount: comittedCount } = await pg.query(`SELECT id, thing FROM things WHERE thing = 'committed'`);
expect(comittedCount).toBe(1);
const { rowCount: rolledbackCount } = await pg.query(`SELECT id, thing FROM things WHERE thing = 'rolledback'`);
expect(rolledbackCount).toBe(0);
});
it(`handles errors thrown on commit gracefully`, async () => {
await expect(_1.default(pg, async (db) => {
await expect(db.query(`INSERT INTO test_defer VALUES(1)`)).resolves.toEqual(expect.anything());
await expect(db.query(`INSERT INTO test_defer VALUES(1)`)).resolves.toEqual(expect.anything());
})).rejects.toThrowError(`duplicate key value violates unique constraint \"test_me\"`);
});
}
describe(`tx`, () => {
describe('with Pool client', () => {
testWithClient(async () => {
const { POSTGRES_URL } = process.env;
if (!POSTGRES_URL) {
throw new Error('Must specify POSTGRES_URL');
}
return new pg_1.Pool({ connectionString: POSTGRES_URL });
}, async (pg) => {
await pg.end();
});
});
describe('with PoolClient client', () => {
let pool;
testWithClient(async () => {
const { POSTGRES_URL } = process.env;
if (!POSTGRES_URL) {
throw new Error('Must specify POSTGRES_URL');
}
pool = new pg_1.Pool({ connectionString: POSTGRES_URL });
return await pool.connect();
}, async (pg) => {
await pg.release();
await pool.end();
});
});
it(`handles error thrown on commit gracefully`, async () => {
const clientMock = ts_mockito_1.mock();
const exampleQuery = `SELECT 1`;
ts_mockito_1.when(clientMock.query(`SELECT txid_current() AS "txid"`))
.thenResolve({ rows: [{ txid: '1' }] })
.thenResolve({ rows: [{ txid: '2' }] });
ts_mockito_1.when(clientMock.query(`COMMIT`)).thenReject(new Error(`db error`));
ts_mockito_1.when(clientMock.query(`ROLLBACK`)).thenResolve();
const client = ts_mockito_1.instance(clientMock);
await expect(_1.default(client, async (db) => {
await db.query(exampleQuery);
})).rejects.toThrowError(`db error`);
ts_mockito_1.verify(clientMock.query(exampleQuery, ts_mockito_1.anything(), ts_mockito_1.anything())).called();
ts_mockito_1.verify(clientMock.query(`COMMIT`)).called();
ts_mockito_1.verify(clientMock.query(`ROLLBACK`)).never();
});
});
//# sourceMappingURL=index.test.js.map