Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tidier property passing #4

Merged
merged 1 commit into from
Oct 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 85 additions & 81 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
// Maybe split to have {Role,Database}Properties and
// {Role,Database}Options. The former for actual things that get set
// in PG, the latter for things about how this client handles changes.
// Maybe add {Role,Database}Options for things about how this client handles changes.

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type QueryResultRow = Record<string, any>;
Expand All @@ -16,7 +14,7 @@ export interface Config {
useTransactions?: boolean;
}

interface DatabaseOptions {
interface DatabaseProperties {
owner?: string;
template?: string;
encoding?: string;
Expand All @@ -26,11 +24,12 @@ interface DatabaseOptions {
connectionLimit?: number;
}

export interface Database extends DatabaseOptions {
export interface Database {
name: string;
properties?: DatabaseProperties;
}

interface RoleOptions {
interface RoleProperties {
isSuperuser?: boolean;
canCreateDb?: boolean;
canCreateRole?: boolean;
Expand All @@ -45,8 +44,9 @@ interface RoleOptions {
adminRoles?: Array<string>;
}

export interface Role extends RoleOptions {
export interface Role {
name: string;
properties?: RoleProperties;
}

interface ExecuteQueriesProps {
Expand Down Expand Up @@ -86,35 +86,38 @@ export const pg_applier = (config: Config): PgApplier => ({
const result = await config.query(
`SELECT *, pg_encoding_to_char(encoding) as encoding_name, rolname as owner_role_name, spcname as tablespace_name FROM pg_database JOIN pg_authid ON pg_database.datdba = pg_authid.oid JOIN pg_tablespace ON pg_database.dattablespace = pg_tablespace.oid WHERE datname = '${database.name}'`,
);

const { properties = {} } = database;

if (result.rows.length) {
// Database already exists

const currentDbConfig = result.rows[0];

const modifyChanges: DatabaseOptions = {};
const modifyChanges: DatabaseProperties = {};

if (database.owner && currentDbConfig.owner_role_name !== database.owner) {
modifyChanges.owner = database.owner;
if (properties.owner && currentDbConfig.owner_role_name !== properties.owner) {
modifyChanges.owner = properties.owner;
}

if (database.encoding && currentDbConfig.encoding_name !== database.encoding) {
modifyChanges.encoding = database.encoding;
if (properties.encoding && currentDbConfig.encoding_name !== properties.encoding) {
modifyChanges.encoding = properties.encoding;
}

if (database.lcCollate && currentDbConfig.datcollate !== database.lcCollate) {
modifyChanges.lcCollate = database.lcCollate;
if (properties.lcCollate && currentDbConfig.datcollate !== properties.lcCollate) {
modifyChanges.lcCollate = properties.lcCollate;
}

if (database.lcCtype && currentDbConfig.datctype !== database.lcCtype) {
modifyChanges.lcCtype = database.lcCtype;
if (properties.lcCtype && currentDbConfig.datctype !== properties.lcCtype) {
modifyChanges.lcCtype = properties.lcCtype;
}

if (database.tablespace && currentDbConfig.tablespace_name !== database.tablespace) {
modifyChanges.tablespace = database.tablespace;
if (properties.tablespace && currentDbConfig.tablespace_name !== properties.tablespace) {
modifyChanges.tablespace = properties.tablespace;
}

if (database.connectionLimit && currentDbConfig.datconnlimit !== database.connectionLimit) {
modifyChanges.connectionLimit = database.connectionLimit;
if (properties.connectionLimit && currentDbConfig.datconnlimit !== properties.connectionLimit) {
modifyChanges.connectionLimit = properties.connectionLimit;
}

// Check for invalid changes
Expand All @@ -134,15 +137,15 @@ export const pg_applier = (config: Config): PgApplier => ({
const queries: Array<string> = [];
// Apply changes
if (modifyChanges.owner) {
queries.push(`ALTER DATABASE ${database.name} OWNER TO ${database.owner}`);
queries.push(`ALTER DATABASE ${database.name} OWNER TO ${properties.owner}`);
}

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

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

await executeQueries({
Expand All @@ -155,26 +158,26 @@ export const pg_applier = (config: Config): PgApplier => ({

const createConfig: Array<string> = [];

if (database.owner) {
createConfig.push(`OWNER = ${database.owner}`);
if (properties.owner) {
createConfig.push(`OWNER = ${properties.owner}`);
}
if (database.template) {
createConfig.push(`TEMPLATE = ${database.template}`);
if (properties.template) {
createConfig.push(`TEMPLATE = ${properties.template}`);
}
if (database.encoding) {
createConfig.push(`ENCODING = ${database.encoding}`);
if (properties.encoding) {
createConfig.push(`ENCODING = ${properties.encoding}`);
}
if (database.lcCollate) {
createConfig.push(`LC_COLLATE = ${database.lcCollate}`);
if (properties.lcCollate) {
createConfig.push(`LC_COLLATE = ${properties.lcCollate}`);
}
if (database.lcCtype) {
createConfig.push(`LC_CTYPE = ${database.lcCtype}`);
if (properties.lcCtype) {
createConfig.push(`LC_CTYPE = ${properties.lcCtype}`);
}
if (database.tablespace) {
createConfig.push(`TABLESPACE = ${database.tablespace}`);
if (properties.tablespace) {
createConfig.push(`TABLESPACE = ${properties.tablespace}`);
}
if (database.connectionLimit) {
createConfig.push(`CONNECTION LIMIT = ${database.connectionLimit}`);
if (properties.connectionLimit) {
createConfig.push(`CONNECTION LIMIT = ${properties.connectionLimit}`);
}

const configQueryFragment = createConfig.join(' ');
Expand All @@ -192,38 +195,39 @@ export const pg_applier = (config: Config): PgApplier => ({

async role(role: Role): Promise<void> {
const result = await config.query(`SELECT * FROM pg_roles WHERE rolname = '${role.name}'`);
const { properties = {} } = role;
if (result.rows.length) {
// Already exists
const currentRoleConfig = result.rows[0];
const modifyChanges: RoleOptions = {};
const modifyChanges: RoleProperties = {};

if (typeof role.isSuperuser !== 'undefined' && currentRoleConfig.rolsuper !== role.isSuperuser) {
modifyChanges.isSuperuser = role.isSuperuser;
if (typeof properties.isSuperuser !== 'undefined' && currentRoleConfig.rolsuper !== properties.isSuperuser) {
modifyChanges.isSuperuser = properties.isSuperuser;
}
if (typeof role.canCreateDb !== 'undefined' && currentRoleConfig.rolcreatedb !== role.canCreateDb) {
modifyChanges.canCreateDb = role.canCreateDb;
if (typeof properties.canCreateDb !== 'undefined' && currentRoleConfig.rolcreatedb !== properties.canCreateDb) {
modifyChanges.canCreateDb = properties.canCreateDb;
}
if (typeof role.canCreateRole !== 'undefined' && currentRoleConfig.rolcreatedb !== role.canCreateRole) {
modifyChanges.canCreateRole = role.canCreateRole;
if (typeof properties.canCreateRole !== 'undefined' && currentRoleConfig.rolcreatedb !== properties.canCreateRole) {
modifyChanges.canCreateRole = properties.canCreateRole;
}
if (typeof role.inherit !== 'undefined' && currentRoleConfig.rolinherit !== role.inherit) {
modifyChanges.inherit = role.inherit;
if (typeof properties.inherit !== 'undefined' && currentRoleConfig.rolinherit !== properties.inherit) {
modifyChanges.inherit = properties.inherit;
}
if (typeof role.login !== 'undefined' && currentRoleConfig.rolcanlogin !== role.login) {
modifyChanges.login = role.login;
if (typeof properties.login !== 'undefined' && currentRoleConfig.rolcanlogin !== properties.login) {
modifyChanges.login = properties.login;
}
if (typeof role.connectionLimit !== 'undefined' && currentRoleConfig.rolconnlimit !== role.connectionLimit) {
modifyChanges.connectionLimit = role.connectionLimit;
if (typeof properties.connectionLimit !== 'undefined' && currentRoleConfig.rolconnlimit !== properties.connectionLimit) {
modifyChanges.connectionLimit = properties.connectionLimit;
}
// TODO join with pg_auth to check the password and whether it is encrypted
if (typeof role.password !== 'undefined') {
modifyChanges.password = role.password;
if (typeof role.passwordEncrypted !== 'undefined') {
modifyChanges.passwordEncrypted = role.passwordEncrypted;
if (typeof properties.password !== 'undefined') {
modifyChanges.password = properties.password;
if (typeof properties.passwordEncrypted !== 'undefined') {
modifyChanges.passwordEncrypted = properties.passwordEncrypted;
}
}
if (typeof role.passwordValidUntil !== 'undefined' && currentRoleConfig.rolvaliduntil !== role.passwordValidUntil) {
modifyChanges.passwordValidUntil = role.passwordValidUntil;
if (typeof properties.passwordValidUntil !== 'undefined' && currentRoleConfig.rolvaliduntil !== properties.passwordValidUntil) {
modifyChanges.passwordValidUntil = properties.passwordValidUntil;
}
// TODO: check pg_auth_members for inRoles, roles and adminRoles config

Expand All @@ -245,7 +249,7 @@ export const pg_applier = (config: Config): PgApplier => ({
queries.push(`ALTER ROLE ${role.name} ${modifyChanges.login ? '' : 'NO'}LOGIN`);
}
if (typeof modifyChanges.connectionLimit !== 'undefined') {
queries.push(`ALTER ROLE ${role.name} CONNECTION LIMIT ${role.connectionLimit}`);
queries.push(`ALTER ROLE ${role.name} CONNECTION LIMIT ${properties.connectionLimit}`);
}
if (typeof modifyChanges.password !== 'undefined') {
const encryptedQueryFragment = typeof modifyChanges.passwordEncrypted === 'undefined' ? '' : `${modifyChanges.passwordEncrypted ? '' : 'UN'}ENCRYPTED `;
Expand All @@ -263,41 +267,41 @@ export const pg_applier = (config: Config): PgApplier => ({

const configFragments: Array<string> = [];

if (typeof role.isSuperuser !== 'undefined') {
configFragments.push(`${role.isSuperuser ? '' : 'NO'}SUPERUSER`);
if (typeof properties.isSuperuser !== 'undefined') {
configFragments.push(`${properties.isSuperuser ? '' : 'NO'}SUPERUSER`);
}
if (typeof role.canCreateDb !== 'undefined') {
configFragments.push(`${role.canCreateDb ? '' : 'NO'}CREATEDB`);
if (typeof properties.canCreateDb !== 'undefined') {
configFragments.push(`${properties.canCreateDb ? '' : 'NO'}CREATEDB`);
}
if (typeof role.canCreateRole !== 'undefined') {
configFragments.push(`${role.canCreateRole ? '' : 'NO'}CREATEROLE`);
if (typeof properties.canCreateRole !== 'undefined') {
configFragments.push(`${properties.canCreateRole ? '' : 'NO'}CREATEROLE`);
}
if (typeof role.inherit !== 'undefined') {
configFragments.push(`${role.inherit ? '' : 'NO'}INHERIT`);
if (typeof properties.inherit !== 'undefined') {
configFragments.push(`${properties.inherit ? '' : 'NO'}INHERIT`);
}
if (typeof role.login !== 'undefined') {
configFragments.push(`${role.login ? '' : 'NO'}LOGIN`);
if (typeof properties.login !== 'undefined') {
configFragments.push(`${properties.login ? '' : 'NO'}LOGIN`);
}
if (typeof role.connectionLimit !== 'undefined') {
configFragments.push(`CONNECTION LIMIT ${role.connectionLimit}`);
if (typeof properties.connectionLimit !== 'undefined') {
configFragments.push(`CONNECTION LIMIT ${properties.connectionLimit}`);
}
if (typeof role.password !== 'undefined') {
if (typeof role.passwordEncrypted !== 'undefined') {
configFragments.push(`${role.passwordEncrypted ? '' : 'UN'}ENCRYPTED`);
if (typeof properties.password !== 'undefined') {
if (typeof properties.passwordEncrypted !== 'undefined') {
configFragments.push(`${properties.passwordEncrypted ? '' : 'UN'}ENCRYPTED`);
}
configFragments.push(`PASSWORD '${role.password}'`);
configFragments.push(`PASSWORD '${properties.password}'`);
}
if (typeof role.passwordValidUntil !== 'undefined') {
configFragments.push(`VALID UNTIL '${role.passwordValidUntil.toISOString()}'`);
if (typeof properties.passwordValidUntil !== 'undefined') {
configFragments.push(`VALID UNTIL '${properties.passwordValidUntil.toISOString()}'`);
}
if (typeof role.inRoles !== 'undefined' && role.inRoles.length > 0) {
configFragments.push(`IN ROLE ${role.inRoles.join(', ')}`);
if (typeof properties.inRoles !== 'undefined' && properties.inRoles.length > 0) {
configFragments.push(`IN ROLE ${properties.inRoles.join(', ')}`);
}
if (typeof role.roles !== 'undefined' && role.roles.length > 0) {
configFragments.push(`ROLE ${role.roles.join(', ')}`);
if (typeof properties.roles !== 'undefined' && properties.roles.length > 0) {
configFragments.push(`ROLE ${properties.roles.join(', ')}`);
}
if (typeof role.adminRoles !== 'undefined' && role.adminRoles.length > 0) {
configFragments.push(`ADMIN ${role.adminRoles.join(', ')}`);
if (typeof properties.adminRoles !== 'undefined' && properties.adminRoles.length > 0) {
configFragments.push(`ADMIN ${properties.adminRoles.join(', ')}`);
}

const query = `CREATE ROLE ${role.name} ${configFragments.join(' ')}`.trim();
Expand Down
32 changes: 24 additions & 8 deletions test/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,15 +119,19 @@ describe('database', () => {

await applier.database({
name: databaseName,
connectionLimit: 10,
properties: {
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,
properties: {
connectionLimit: 20,
},
});
const db2 = (await client.query(`SELECT * FROM pg_database WHERE datname = '${databaseName}'`)).rows[0];
expect(db2.datconnlimit).toEqual(20);
Expand All @@ -150,15 +154,19 @@ describe('database', () => {

await applier.database({
name: databaseName,
connectionLimit: 10,
properties: {
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,
properties: {
connectionLimit: 20,
},
});
const db2 = (await client.query(`SELECT * FROM pg_database WHERE datname = '${databaseName}'`)).rows[0];
expect(db2.datconnlimit).toEqual(20);
Expand Down Expand Up @@ -237,15 +245,19 @@ describe('role', () => {

await applier.role({
name: roleName,
connectionLimit: 10,
properties: {
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,
properties: {
connectionLimit: 20,
},
});

const role2 = (await client.query(`SELECT * FROM pg_roles WHERE rolname = '${roleName}'`)).rows[0];
Expand All @@ -269,15 +281,19 @@ describe('role', () => {

await applier.role({
name: roleName,
connectionLimit: 10,
properties: {
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,
properties: {
connectionLimit: 20,
},
});

const role2 = (await client.query(`SELECT * FROM pg_roles WHERE rolname = '${roleName}'`)).rows[0];
Expand Down