@itrocks/mysql
Version:
Transforms model objects to and from MySQL database records
394 lines • 18.3 kB
JavaScript
import { inherits } from '@itrocks/class-type';
import { isAnyFunction } from '@itrocks/class-type';
import { isAnyType } from '@itrocks/class-type';
import { typeOf } from '@itrocks/class-type';
import { compositeOf } from '@itrocks/composition';
import { CollectionType } from '@itrocks/property-type';
import { ReflectClass } from '@itrocks/reflect';
import { ReflectProperty } from '@itrocks/reflect';
import { Reverse } from '@itrocks/sort';
import { sortOf } from '@itrocks/sort';
import { DataSource } from '@itrocks/storage';
import { Limit } from '@itrocks/storage';
import { Sort } from '@itrocks/storage';
import { createConnection } from 'mariadb';
export const DEBUG = false;
const depends = {
applyReadTransformer: (record, property) => record[property],
applySaveTransformer: (object, property) => object[property],
columnOf: name => name.toLowerCase(),
componentOf: () => false,
ignoreTransformedValue: Symbol('ignoreTransformedValue'),
QueryFunction: class {
},
queryFunctionCall: () => [undefined, ' = ?'],
storeOf: target => typeOf(target).name.toLowerCase()
};
export function joinTableName(object1, object2) {
if (typeof object1 !== 'string')
object1 = depends.storeOf(object1);
if (typeof object2 !== 'string')
object2 = depends.storeOf(object2);
return [object1, object2].sort().join('_');
}
export function mysqlDependsOn(dependencies) {
Object.assign(depends, dependencies);
}
export class Mysql extends DataSource {
config;
connection;
saveQueue = new WeakMap();
constructor(config) {
super();
this.config = config;
}
async connect() {
const mariaDbConfig = Object.assign(this.config, {
allowPublicKeyRetrieval: true,
dateStrings: false
});
return this.connection = await createConnection(mariaDbConfig);
}
async count(type, search = {}) {
const connection = this.connection ?? await this.connect();
Object.setPrototypeOf(search, type.prototype);
const sql = this.propertiesToSearchSql(search);
const [values] = await this.valuesToDb(search);
if (DEBUG)
console.log('SELECT COUNT(*) FROM `' + depends.storeOf(type) + '`' + sql, JSON.stringify(values));
const row = (await connection.query('SELECT COUNT(*) `count` FROM `' + depends.storeOf(type) + '`' + sql, Object.values(values)))[0];
return row?.count;
}
async delete(object, property = 'id') {
await this.deleteId(object, object[property], property);
return this.disconnectObject(object);
}
async deleteId(type, id, property = 'id') {
const connection = this.connection ?? await this.connect();
if (DEBUG)
console.log('DELETE FROM `' + depends.storeOf(type) + '` WHERE `' + depends.columnOf(property) + '` = ?', [id]);
await connection.query('DELETE FROM `' + depends.storeOf(type) + '` WHERE `' + depends.columnOf(property) + '` = ?', [id]);
}
async deleteRelatedId(object, property, id) {
const connection = this.connection ?? await this.connect();
const objectTable = depends.storeOf(object);
const propertyTable = depends.storeOf(new ReflectProperty(object, property).collectionType.elementType.type);
if (!objectTable || !propertyTable) {
throw 'Collection objects are not stored';
}
const joinTable = joinTableName(objectTable, propertyTable);
const query = 'DELETE FROM `' + joinTable + '` WHERE ' + objectTable + '_id = ? AND ' + propertyTable + '_id = ?';
const values = [object.id, id];
if (DEBUG)
console.log(query, JSON.stringify(values));
connection.query(query, values);
}
async insert(object) {
const connection = this.connection ?? await this.connect();
const [values, deferred] = await this.valuesToDb(object);
const sql = this.propertiesToSql(values);
const query = 'INSERT INTO `' + depends.storeOf(object) + '` SET ' + sql;
if (DEBUG)
console.log(query, JSON.stringify(Object.values(values)));
const result = await connection.query(query, Object.values(values));
const id = result.insertId;
const entity = this.connectObject(object, ((id >= Number.MIN_SAFE_INTEGER) && (id <= Number.MAX_SAFE_INTEGER)) ? Number(id) : id);
for (const callback of deferred) {
callback(object);
}
return entity;
}
async insertRelatedId(object, property, id) {
const connection = this.connection ?? await this.connect();
const objectTable = depends.storeOf(object);
const propertyTable = depends.storeOf(new ReflectProperty(object, property).collectionType.elementType.type);
if (!objectTable || !propertyTable) {
throw 'Collection objects are not stored';
}
const joinTable = joinTableName(objectTable, propertyTable);
const query = 'INSERT INTO `' + joinTable + '` SET ' + objectTable + '_id = ?, ' + propertyTable + '_id = ?';
const values = [object.id, id];
if (DEBUG)
console.log(query, JSON.stringify(values));
connection.query(query, values);
}
propertiesToSearchSql(search) {
const sql = Object.entries(search)
.map(([name, value]) => {
let sql;
if (value instanceof depends.QueryFunction) {
[value, sql] = depends.queryFunctionCall(value);
search[name] = value;
}
else {
sql = ' = ?';
}
if ((typeof value)[0] === 'o') {
name = Array.isArray(value) ? 'id' : (name + '_id');
}
return '`' + depends.columnOf(name) + '`' + sql;
})
.join(' AND ');
return sql.length
? ' WHERE ' + sql
: '';
}
propertiesToSql(object) {
return Object.keys(object).map(name => '`' + depends.columnOf(name) + '` = ?').join(', ');
}
propertiesToSqlSelect(type) {
const sql = ['id'];
for (const property of new ReflectClass(type).properties) {
const propertyType = property.type;
if (propertyType instanceof CollectionType)
continue;
const propertyName = (isAnyType(propertyType.type) && depends.storeOf(propertyType.type))
? property.name + 'Id'
: property.name;
const columnName = depends.columnOf(propertyName);
sql.push((columnName.length !== propertyName.length)
? ('`' + columnName + '` `' + propertyName + '`')
: ('`' + propertyName + '`'));
}
return sql.join(', ');
}
async query(type, query, values) {
const connection = this.connection ?? await this.connect();
if (DEBUG)
console.log(query, values);
const rows = await connection.query(query, values);
return Promise.all(rows.map(row => this.valuesFromDb(row, type)));
}
async read(type, id) {
const connection = this.connection ?? await this.connect();
const propertiesSql = this.propertiesToSqlSelect(type);
if (DEBUG)
console.log('SELECT ' + propertiesSql + ' FROM `' + depends.storeOf(type) + '` WHERE id = ?', [id]);
const rows = await connection.query('SELECT ' + propertiesSql + ' FROM `' + depends.storeOf(type) + '` WHERE id = ?', [id]);
return this.valuesFromDb(rows[0], type);
}
async readCollection(object, property, type = new ReflectProperty(object, property).collectionType.elementType.type) {
const connection = this.connection ?? await this.connect();
const propertiesSql = this.propertiesToSqlSelect(type);
const objectTable = depends.storeOf(object);
const propertyTable = depends.storeOf(type);
if (!objectTable || !propertyTable) {
throw 'Collection objects are not stored';
}
let query;
if (depends.componentOf(object, property)) {
query = 'SELECT ' + propertiesSql + ' FROM `' + propertyTable + '` WHERE ' + objectTable + '_id = ?';
}
else {
const joinTable = joinTableName(objectTable, propertyTable);
query = 'SELECT `' + propertyTable + '`.' + propertiesSql + ' FROM `' + propertyTable + '`'
+ ' INNER JOIN `' + joinTable + '` ON `' + joinTable + '`.' + propertyTable + '_id = `' + propertyTable + '`.id'
+ ' WHERE `' + joinTable + '`.' + objectTable + '_id = ?';
}
const rows = await connection.query(query, [object.id]);
return Promise.all(rows.map(row => this.valuesFromDb(row, type)));
}
async readCollectionIds(object, property, type = new ReflectProperty(object, property).collectionType.elementType.type) {
const connection = this.connection ?? await this.connect();
const objectTable = depends.storeOf(object);
const propertyTable = depends.storeOf(type);
if (!objectTable || !propertyTable) {
throw 'Collection objects are not stored';
}
let query;
if (depends.componentOf(object, property)) {
query = 'SELECT id FROM `' + propertyTable + '` WHERE ' + objectTable + '_id = ?';
}
else {
const joinTable = joinTableName(objectTable, propertyTable);
query = 'SELECT ' + propertyTable + '_id id FROM `' + joinTable + '`'
+ ' WHERE `' + joinTable + '`.' + objectTable + '_id = ?';
}
const rows = await connection.query(query, [object.id]);
return Promise.all(rows.map(row => row.id));
}
async readMultiple(type, ids) {
if (!ids.length)
return [];
const connection = this.connection ?? await this.connect();
const propertiesSql = this.propertiesToSqlSelect(type);
const questionMarks = Array(ids.length).fill('?').join(', ');
if (DEBUG)
console.log('SELECT ' + propertiesSql + ' FROM `' + depends.storeOf(type) + '` WHERE id IN (' + questionMarks + ')', ids);
const rows = await connection.query('SELECT ' + propertiesSql + ' FROM `' + depends.storeOf(type) + '` WHERE id IN (' + questionMarks + ')', ids);
return Promise.all(rows.map(row => this.valuesFromDb(row, type)));
}
async runSerialized(object, task) {
const prev = this.saveQueue.get(object) || Promise.resolve();
const next = prev.then(task, task);
this.saveQueue.set(object, next.then(() => { this.saveQueue.delete(object); }, () => { this.saveQueue.delete(object); }));
return next;
}
async save(object) {
return this.runSerialized(object, async () => {
return this.isObjectConnected(object)
? this.update(object)
: this.insert(object);
});
}
async saveCollection(object, property, value) {
if (property.endsWith('Ids')) {
property = property.slice(0, -3);
}
return depends.componentOf(object, property)
? this.saveComponents(object, property, value)
: this.saveLinks(object, property, value);
}
async saveComponents(object, property, components) {
const connection = this.connection ?? await this.connect();
const propertyType = new ReflectProperty(object, property).collectionType.elementType.type;
const stored = await this.readCollectionIds(object, property, propertyType);
const saved = new Array;
let compositeProperty;
for (const component of components) {
if (typeof component !== 'object') {
saved.push(component);
continue;
}
if (compositeProperty === undefined) {
const objectType = typeOf(object);
for (const candidate of new ReflectClass(component).properties) {
if (!compositeOf(component, candidate.name))
continue;
const candidateType = candidate.type.type;
if (!isAnyType(candidateType))
continue;
if (!inherits(objectType, candidateType))
continue;
compositeProperty = candidate;
break;
}
}
if (compositeProperty) {
// @ts-ignore TS2322 Don't understand this error
component[compositeProperty.name] = object;
}
saved.push((await this.save(component)).id);
}
let componentTable;
for (const storedId of stored) {
if (saved.includes(storedId))
continue;
if (!componentTable) {
componentTable = depends.storeOf(propertyType);
if (!componentTable) {
throw 'Missing @Store on type ' + propertyType.name
+ ' used by @Component ' + new ReflectClass(object).name + '.' + property;
}
}
await connection.query('DELETE FROM `' + componentTable + '` WHERE id = ?', [storedId]);
}
}
async saveLinks(object, property, links) {
const connection = this.connection ?? await this.connect();
const objectTable = depends.storeOf(object);
const propertyType = new ReflectProperty(object, property).collectionType.elementType.type;
const propertyTable = depends.storeOf(propertyType);
const linkColumn = depends.columnOf(propertyTable) + '_id';
const linkTable = joinTableName(objectTable, propertyTable);
const objectColumn = depends.columnOf(objectTable) + '_id';
const objectId = object.id;
const stored = await this.readCollectionIds(object, property, propertyType);
const saved = new Array;
for (const link of links) {
const linkId = (typeof link === 'object')
? (this.isObjectConnected(link) ? link.id : (await this.save(link)).id)
: link;
saved.push(linkId);
if (stored.includes(linkId))
continue;
await connection.query('INSERT INTO `' + linkTable + '` SET ' + objectColumn + ' = ?, ' + linkColumn + ' = ?', [objectId, linkId]);
stored.push(linkId);
}
for (const storedId of stored) {
if (saved.includes(storedId))
continue;
await connection.query('DELETE FROM `' + linkTable + '` WHERE ' + objectColumn + ' = ? AND ' + linkColumn + ' = ?', [objectId, storedId]);
}
}
async search(type, search = {}, options) {
const connection = this.connection ?? await this.connect();
const propertiesSql = this.propertiesToSqlSelect(type);
let limitOption = undefined;
let sortOption = undefined;
for (const option of this.options(options)) {
if (option === Sort) {
sortOption = new Sort(sortOf(type));
}
if (option instanceof Limit) {
limitOption = option;
}
if (option instanceof Sort) {
sortOption = option.properties.length ? option : new Sort(sortOf(type));
}
}
Object.setPrototypeOf(search, type.prototype);
const sql = this.propertiesToSearchSql(search);
const [values] = await this.valuesToDb(search);
if (DEBUG)
console.log('SELECT ' + propertiesSql + ' FROM `' + depends.storeOf(type) + '`' + sql, JSON.stringify(values));
const limit = limitOption?.limit ? ' LIMIT ' + limitOption.limit : '';
const offset = limitOption?.offset ? ' OFFSET ' + limitOption.offset : '';
const sort = sortOption?.properties.length
? ' ORDER BY '
+ sortOption.properties
.map(property => '`' + property + '`' + (property instanceof Reverse ? ' DESC' : ''))
.join(', ')
: '';
const rows = await connection.query('SELECT ' + propertiesSql + ' FROM `' + depends.storeOf(type) + '`' + sql + sort + limit + offset, Object.values(values));
return Promise.all(rows.map(row => this.valuesFromDb(row, type)));
}
async update(object) {
const connection = this.connection ?? await this.connect();
const [values, deferred] = await this.valuesToDb(object);
const sql = this.propertiesToSql(values);
const query = 'UPDATE `' + depends.storeOf(object) + '` SET ' + sql + ' WHERE id = ?';
if (DEBUG)
console.log(query, JSON.stringify(Object.values(values).concat([object.id])));
await connection.query(query, Object.values(values).concat([object.id]));
for (const callback of deferred) {
callback(object);
}
return object;
}
async valuesFromDb(record, type) {
const object = (new type);
let property;
for (property in record) {
const value = await depends.applyReadTransformer(record, property, object);
if (value === depends.ignoreTransformedValue)
continue;
object[property] = value;
if (property.endsWith('Id')) {
delete object[property.slice(0, -2)];
}
}
return object;
}
async valuesToDb(object) {
const deferred = [];
const record = {};
for (const property of Object.keys(object)) {
const value = await depends.applySaveTransformer(object, property, record);
if (value === depends.ignoreTransformedValue) {
continue;
}
if (Array.isArray(value)) {
deferred.push((object) => this.saveCollection(object, property, value));
continue;
}
if (isAnyFunction(value)) {
deferred.push(value);
continue;
}
record[depends.columnOf(property)] = value;
}
return [record, deferred];
}
}
//# sourceMappingURL=mysql.js.map