@smallprod/models
Version:
408 lines (390 loc) • 11.7 kB
text/typescript
import {
AttrAndAlias,
Attribute,
SortAttribute,
WhereAttribute,
WhereKeyWord,
} from '../../entities/querys/query';
import { Field, FieldType } from '../../migration/field';
import { Having, IJoin, Join } from '../../entities/querys/find.query';
import pg, { Pool, PoolClient, PoolConfig, QueryResult } from 'pg';
import GlobalSqlModel from './global.sql';
const getPool = async (
config: PoolConfig,
quitOnFail = false,
): Promise<Pool | null> => {
pg.types.setTypeParser(20, Number);
return new Promise(async (resolve, reject) => {
const pool = new Pool(config);
pool.on('error', (err) => {
if (GlobalPostgreModel.debug) {
console.error('Unexpected error on postgres database');
console.error(err);
}
if (quitOnFail) {
process.exit(-1);
} else {
reject(err);
}
});
resolve(pool);
});
};
export default class GlobalPostgreModel extends GlobalSqlModel {
protected pool: Pool | null = null;
protected transactionConnection: PoolClient | null = null;
protected transactionDone: (() => void) | null = null;
public disconnect = async () => {
await this.pool?.end();
};
/* Querying */
public query = async (
query: string,
params?: string[],
throwErrors = false,
): Promise<QueryResult | null> => {
if (!this.pool) {
if (throwErrors) {
throw Error('No pool');
}
return null;
}
try {
if (!this.transactionConnection) {
return await this.pool.query(query, params);
}
return await this.transactionConnection.query(query, params);
} catch (err) {
if (GlobalPostgreModel.debug) {
console.error(
`An error occured while executing ${query} with parameters:`,
params,
);
console.error(err);
}
if (throwErrors) {
throw err;
}
return null;
}
};
public insert = async (tableName: string, attributes: Attribute[]) => {
const columns = attributes.map((a) => `"${a.column}"`).join(', ');
const params = attributes.map((a, index) => `$${index + 1}`).join(', ');
const query = `INSERT INTO "${tableName}" (${columns}) VALUES (${params}) RETURNING *`;
const res = await this.query(
query,
attributes.map((a) => a.value),
);
return res?.rows[0].id;
};
public select = async (
tableName: string,
distinct: boolean = false,
attributes: AttrAndAlias[] = [],
wheres: (WhereAttribute | WhereKeyWord)[] = [],
sorts: SortAttribute[] = [],
tableAlias: string = '',
limit: number = -1,
offset = 0,
joins: IJoin[] = [],
groups: string[] = [],
havings: (WhereAttribute | WhereKeyWord)[] = [],
) => {
const query = `SELECT${distinct ? ' DISTINCT' : ''}${this.computeAttributes(
attributes,
'"',
)} FROM "${tableName}" ${
tableAlias ? `${tableAlias}` : ''
} ${this.computeJoins(joins, '"', '')}${this.computeWhere(
wheres,
'$',
true,
'"',
)}${this.computeGroupBy(groups)}${this.computeWhere(
havings,
'$',
true,
'"',
'HAVING',
wheres.filter((w: any) => !w.keyword).length,
)}${this.computeSort(sorts, '"')}${
limit !== -1 ? ` LIMIT ${limit} OFFSET ${offset}` : ''
}`;
const havingAttr = this.getWhereAttributes(havings);
return (
await this.query(
query,
havingAttr.concat(this.getWhereAttributes(wheres)),
)
)?.rows;
};
public update = async (
tableName: string,
attributes: Attribute[],
wheres: (WhereAttribute | WhereKeyWord)[],
) => {
const columns = attributes
.map((a, index) => `"${a.column}" = $${index + 1}`)
.join(', ');
const query = `UPDATE "${tableName}" SET ${columns} ${this.computeWhere(
wheres,
'$',
true,
'"',
'WHERE',
attributes.length,
)}`;
return (
await this.query(
query,
attributes.map((a) => a.value).concat(this.getWhereAttributes(wheres)),
)
)?.rowCount;
};
public delete = async (
tableName: string,
wheres: (WhereAttribute | WhereKeyWord)[],
) => {
const query = `DELETE FROM "${tableName}" ${this.computeWhere(
wheres,
'$',
true,
'"',
)}`;
return (await this.query(query, this.getWhereAttributes(wheres)))?.rowCount;
};
/* Table management */
public createTable = async (tableName: string, fields: Field[]) => {
// * Create the new table
const query = `CREATE TABLE ${tableName} (${fields
.map((f: Field) => this.formatFieldForTableManagement(f))
.join(', ')})`;
await this.query(query);
// * Add the constraints
await fields.reduce(async (prevField, curField) => {
await prevField;
const constraints = this.getFieldConstraintsForTableManagement(
curField,
tableName,
);
await constraints.reduce(async (prevConst, curConst) => {
await prevConst;
await this.query(curConst);
}, Promise.resolve());
}, Promise.resolve());
};
public removeTable = async (tableName: string) => {
const query = `DROP TABLE ${tableName}`;
return await this.query(query);
};
public alterTable = async (
tableName: string,
fieldsToAdd: Field[],
fieldsToRemove: string[],
) => {
await fieldsToRemove.reduce(async (prev, cur) => {
await prev;
const query = `ALTER TABLE ${tableName} DROP COLUMN ${cur}`;
await this.query(query);
}, Promise.resolve());
await fieldsToAdd.reduce(async (prev, cur) => {
await prev;
const query = `ALTER TABLE ${tableName} ADD ${this.formatFieldForTableManagement(
cur,
)}`;
await this.query(query);
const constraints = this.getFieldConstraintsForTableManagement(
cur,
tableName,
);
await constraints.reduce(async (prevConst, curConst) => {
await prevConst;
await this.query(curConst);
}, Promise.resolve());
}, Promise.resolve());
};
/* Transaction */
public startTransaction = async (): Promise<boolean> => {
return new Promise((resolve, reject) => {
if (this.transactionConnection) {
if (GlobalPostgreModel.debug) console.error('Already in a transaction');
return resolve(false);
}
if (!this.pool) {
if (GlobalPostgreModel.debug) console.error('No pool');
return resolve(false);
}
this.pool.connect((err, client, done) => {
if (err) {
if (GlobalPostgreModel.debug) console.error(err);
return resolve(false);
}
client.query('BEGIN', (error) => {
if (error) {
done();
return resolve(false);
}
this.transactionDone = done;
this.transactionConnection = client;
resolve(true);
});
});
});
};
public commit = async (): Promise<void> => {
return new Promise((resolve, reject) => {
if (!this.transactionConnection) {
if (GlobalPostgreModel.debug) console.error('Not in a transaction');
return resolve();
}
this.transactionConnection.query('COMMIT', (err) => {
if (err) {
if (GlobalPostgreModel.debug) console.error(err);
this.transactionConnection?.query('ROLLBACK', (e) => {
this.transactionConnection = null;
if (e) {
if (GlobalPostgreModel.debug) console.error(e);
}
resolve();
});
}
if (this.transactionDone) {
this.transactionDone();
this.transactionDone = null;
}
this.transactionConnection = null;
resolve();
});
});
};
public rollback = async (): Promise<void> => {
return new Promise((resolve) => {
if (!this.transactionConnection) {
if (GlobalPostgreModel.debug) console.error('Not in a transaction');
return resolve();
}
this.transactionConnection.query('ROLLBACK', (err) => {
this.transactionConnection = null;
if (err) {
if (GlobalPostgreModel.debug) console.error(err);
}
if (this.transactionDone) {
this.transactionDone();
this.transactionDone = null;
}
resolve();
});
});
};
/* Migration management */
public checkMigrationTable = async () => {
const res = await this.query(
'SELECT table_name FROM information_schema.tables WHERE table_name = $1',
['migrations'],
);
return res && res.rowCount ? true : false;
};
public setPool = async (config: PoolConfig) => {
const p = await getPool(config);
if (p) {
this.pool = p;
}
};
private formatFieldForTableManagement = (field: Field) => {
const fInfos = field.getAll();
if (fInfos.ai) {
return `${fInfos.name} ${
fInfos.type === 'bigint'
? 'BIGSERIAL'
: fInfos.type === 'smallint'
? 'SMALLSERIAL'
: 'SERIAL'
}${fInfos.len !== 0 ? `(${fInfos.len})` : ''}${
fInfos.null ? '' : ' NOT NULL'
}`;
}
return `${fInfos.name} ${this.getCorrespondingType(fInfos.type)}${
fInfos.len !== 0 ? `(${fInfos.len})` : ''
}${fInfos.null ? '' : ' NOT NULL'}`;
};
private getFieldConstraintsForTableManagement = (
field: Field,
tableName: string,
) => {
const fieldInfos = field.getAll();
const constraints = [];
if (fieldInfos.mustBeUnique || fieldInfos.primaryKey) {
constraints.push(
`ALTER TABLE ${tableName} ADD CONSTRAINT unique_${tableName.toLowerCase()}_${fieldInfos.name.toLowerCase()} UNIQUE (${
fieldInfos.name
})`,
);
}
if (fieldInfos.checkValue) {
constraints.push(
`ALTER TABLE ${tableName} ADD CONSTRAINT check_${tableName.toLowerCase()}_${fieldInfos.name.toLowerCase()} CHECK(${
fieldInfos.name
}${fieldInfos.checkValue})`,
);
}
if (fieldInfos.defaultValue) {
constraints.push(
`ALTER TABLE ${tableName} ALTER ${fieldInfos.name} SET DEFAULT ${
fieldInfos.defaultValue.isSystem
? `${fieldInfos.defaultValue.value}`
: `'${fieldInfos.defaultValue.value}'`
};`,
);
}
if (fieldInfos.primaryKey) {
constraints.push(
`ALTER TABLE ${tableName} ADD CONSTRAINT pk_${tableName.toLowerCase()}_${fieldInfos.name.toLowerCase()} PRIMARY KEY (${
fieldInfos.name
})`,
);
}
if (fieldInfos.foreignKey) {
constraints.push(
`ALTER TABLE ${tableName} ADD CONSTRAINT fk_${tableName.toLowerCase()}_${fieldInfos.name.toLowerCase()} FOREIGN KEY (${
fieldInfos.name
}) REFERENCES ${fieldInfos.foreignKey.table}(${
fieldInfos.foreignKey.column
})`,
);
}
return constraints;
};
private getCorrespondingType = (type: FieldType): string => {
switch (type) {
case 'binary':
return 'bytea';
case 'blob':
return 'bytea';
case 'datetime':
return 'timestamp';
case 'float':
return 'double';
case 'longblob':
return 'bytea';
case 'longtext':
return 'text';
case 'mediumblob':
return 'bytea';
case 'mediumint':
return 'int';
case 'tinyint':
return 'smallint';
case 'mediumtext':
return 'text';
case 'tinyblob':
return 'bytea';
case 'tinytext':
return 'text';
case 'varbinary':
return 'bytea';
default:
return type;
}
};
}