Skip to content

Commit

Permalink
Add useTransactions option
Browse files Browse the repository at this point in the history
  • Loading branch information
plumdog committed Oct 19, 2021
1 parent 85b0f5e commit cf6d6a9
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 16 deletions.
78 changes: 63 additions & 15 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface QueryResult {

export interface Config {
query: (text: string, values?: Array<string>) => Promise<QueryResult>;
useTransactions?: boolean;
}

interface DatabaseOptions {
Expand Down Expand Up @@ -48,6 +49,33 @@ export interface Role extends RoleOptions {
name: string;
}

interface ExecuteQueriesProps {
config: Config;
queries: Array<string>;
}

const executeQueries = async (props: ExecuteQueriesProps): Promise<void> => {
const runAllQueries = async (): Promise<void> => {
for (const query of props.queries) {
await props.config.query(query);
}
};

if (!props.config.useTransactions) {
await runAllQueries();
return;
}

await props.config.query('BEGIN');
try {
await runAllQueries();
} catch (err) {
await props.config.query('ROLLBACK');
throw err;
}
await props.config.query('COMMIT');
};

export interface PgApplier {
database(database: Database): Promise<void>;
role(role: Role): Promise<void>;
Expand Down Expand Up @@ -103,19 +131,25 @@ export const pg_applier = (config: Config): PgApplier => ({
throw new Error('Cannot change the ctype of an existing database');
}

const queries: Array<string> = [];
// Apply changes
if (modifyChanges.owner) {
await config.query(`ALTER DATABASE ${database.name} OWNER TO ${database.owner}`);
queries.push(`ALTER DATABASE ${database.name} OWNER TO ${database.owner}`);
}

if (modifyChanges.tablespace) {
await config.query(`ALTER DATABASE ${database.name} TABLESPACE ${database.tablespace}`);
queries.push(`ALTER DATABASE ${database.name} TABLESPACE ${database.tablespace}`);
}

if (modifyChanges.connectionLimit) {
await config.query(`ALTER DATABASE ${database.name} CONNECTION LIMIT ${database.connectionLimit}`);
queries.push(`ALTER DATABASE ${database.name} CONNECTION LIMIT ${database.connectionLimit}`);
}

await executeQueries({
config,
queries,
});

return;
}

Expand Down Expand Up @@ -146,7 +180,14 @@ export const pg_applier = (config: Config): PgApplier => ({
const configQueryFragment = createConfig.join(' ');
const query = `CREATE DATABASE ${database.name} ${configQueryFragment}`.trim();

await config.query(query);
await executeQueries({
config: {
...config,
// Cannot use transaction for CREATE DATABASE. See https://www.postgresql.org/docs/14/sql-createdatabase.html
useTransactions: false,
},
queries: [query],
});
},

async role(role: Role): Promise<void> {
Expand Down Expand Up @@ -186,32 +227,37 @@ export const pg_applier = (config: Config): PgApplier => ({
}
// TODO: check pg_auth_members for inRoles, roles and adminRoles config

const queries: Array<string> = [];

if (typeof modifyChanges.isSuperuser !== 'undefined') {
await config.query(`ALTER ROLE ${role.name} ${modifyChanges.isSuperuser ? '' : 'NO'}SUPERUSER`);
queries.push(`ALTER ROLE ${role.name} ${modifyChanges.isSuperuser ? '' : 'NO'}SUPERUSER`);
}
if (typeof modifyChanges.canCreateDb !== 'undefined') {
await config.query(`ALTER ROLE ${role.name} ${modifyChanges.canCreateDb ? '' : 'NO'}CREATEDB`);
queries.push(`ALTER ROLE ${role.name} ${modifyChanges.canCreateDb ? '' : 'NO'}CREATEDB`);
}
if (typeof modifyChanges.canCreateRole !== 'undefined') {
await config.query(`ALTER ROLE ${role.name} ${modifyChanges.canCreateRole ? '' : 'NO'}CREATEROLE`);
queries.push(`ALTER ROLE ${role.name} ${modifyChanges.canCreateRole ? '' : 'NO'}CREATEROLE`);
}
if (typeof modifyChanges.inherit !== 'undefined') {
await config.query(`ALTER ROLE ${role.name} ${modifyChanges.inherit ? '' : 'NO'}INHERIT`);
queries.push(`ALTER ROLE ${role.name} ${modifyChanges.inherit ? '' : 'NO'}INHERIT`);
}
if (typeof modifyChanges.login !== 'undefined') {
await config.query(`ALTER ROLE ${role.name} ${modifyChanges.login ? '' : 'NO'}LOGIN`);
queries.push(`ALTER ROLE ${role.name} ${modifyChanges.login ? '' : 'NO'}LOGIN`);
}
if (typeof modifyChanges.connectionLimit !== 'undefined') {
await config.query(`ALTER ROLE ${role.name} CONNECTION LIMIT ${role.connectionLimit}`);
queries.push(`ALTER ROLE ${role.name} CONNECTION LIMIT ${role.connectionLimit}`);
}
if (typeof modifyChanges.password !== 'undefined') {
const encryptedQueryFragment = typeof modifyChanges.passwordEncrypted === 'undefined' ? '' : `${modifyChanges.passwordEncrypted ? '' : 'UN'}ENCRYPTED `;
await config.query(`ALTER ROLE ${role.name} ${encryptedQueryFragment}PASSWORD ${modifyChanges.password}`);
queries.push(`ALTER ROLE ${role.name} ${encryptedQueryFragment}PASSWORD ${modifyChanges.password}`);
}
if (typeof modifyChanges.passwordValidUntil !== 'undefined') {
await config.query(`ALTER ROLE ${role.name} VALID UNTIL '${modifyChanges.passwordValidUntil.toISOString()}'`);
queries.push(`ALTER ROLE ${role.name} VALID UNTIL '${modifyChanges.passwordValidUntil.toISOString()}'`);
}

await executeQueries({
config,
queries,
});
return;
}

Expand Down Expand Up @@ -255,7 +301,9 @@ export const pg_applier = (config: Config): PgApplier => ({
}

const query = `CREATE ROLE ${role.name} ${configFragments.join(' ')}`.trim();

await config.query(query);
await executeQueries({
config,
queries: [query],
});
},
});
2 changes: 1 addition & 1 deletion run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

docker-compose build
docker-compose down -v
docker-compose -f docker-compose.yaml run --rm pg-access-apply
docker-compose -f docker-compose.yaml run --rm pg-access-apply npm run -- test "$@"
121 changes: 121 additions & 0 deletions test/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,36 @@ describe('database', () => {
}
});

test('create database with transaction', async () => {
const client = getClient();
await client.connect();

try {
// This works, but only because the code knows to *not*
// use a transaction for database creation.
const applier = pg_applier({
query: client.query.bind(client),
useTransactions: true,
});

const databaseName = `testdb_${randString(12)}`;

await applier.database({
name: databaseName,
});

const result = await client.query('SELECT datname from pg_database');
const dbs = [];
for (const row of result.rows) {
dbs.push(row.datname);
}

expect(dbs).toEqual(expect.arrayContaining([databaseName]));
} finally {
await client.end();
}
});

test('handle existing database', async () => {
const client = getClient();
await client.connect();
Expand Down Expand Up @@ -105,6 +135,37 @@ describe('database', () => {
await client.end();
}
});

test('change connection limit with transaction', async () => {
const client = getClient();
await client.connect();

try {
const applier = pg_applier({
query: client.query.bind(client),
useTransactions: true,
});

const databaseName = `testdb_${randString(12)}`;

await applier.database({
name: databaseName,
connectionLimit: 10,
});

const db = (await client.query(`SELECT * FROM pg_database WHERE datname = '${databaseName}'`)).rows[0];
expect(db.datconnlimit).toEqual(10);

await applier.database({
name: databaseName,
connectionLimit: 20,
});
const db2 = (await client.query(`SELECT * FROM pg_database WHERE datname = '${databaseName}'`)).rows[0];
expect(db2.datconnlimit).toEqual(20);
} finally {
await client.end();
}
});
});

describe('role', () => {
Expand Down Expand Up @@ -135,6 +196,34 @@ describe('role', () => {
}
});

test('create role with transaction', async () => {
const client = getClient();
await client.connect();

try {
const applier = pg_applier({
query: client.query.bind(client),
useTransactions: true,
});

const roleName = `testrole_${randString(12)}`;

await applier.role({
name: roleName,
});

const result = await client.query('SELECT rolname from pg_roles');
const roles = [];
for (const row of result.rows) {
roles.push(row.rolname);
}

expect(roles).toEqual(expect.arrayContaining([roleName]));
} finally {
await client.end();
}
});

test('change connection limit', async () => {
const client = getClient();
await client.connect();
Expand Down Expand Up @@ -165,4 +254,36 @@ describe('role', () => {
await client.end();
}
});

test('change connection limit with transaction', async () => {
const client = getClient();
await client.connect();

try {
const applier = pg_applier({
query: client.query.bind(client),
useTransactions: true,
});

const roleName = `testrole_${randString(12)}`;

await applier.role({
name: roleName,
connectionLimit: 10,
});

const role = (await client.query(`SELECT * FROM pg_roles WHERE rolname = '${roleName}'`)).rows[0];
expect(role.rolconnlimit).toEqual(10);

await applier.role({
name: roleName,
connectionLimit: 20,
});

const role2 = (await client.query(`SELECT * FROM pg_roles WHERE rolname = '${roleName}'`)).rows[0];
expect(role2.rolconnlimit).toEqual(20);
} finally {
await client.end();
}
});
});

0 comments on commit cf6d6a9

Please sign in to comment.