UNPKG

@itrocks/mysql

Version:

Transforms model objects to and from MySQL database records

400 lines 18.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Mysql = exports.DEBUG = void 0; exports.joinTableName = joinTableName; exports.mysqlDependsOn = mysqlDependsOn; const class_type_1 = require("@itrocks/class-type"); const class_type_2 = require("@itrocks/class-type"); const class_type_3 = require("@itrocks/class-type"); const class_type_4 = require("@itrocks/class-type"); const composition_1 = require("@itrocks/composition"); const property_type_1 = require("@itrocks/property-type"); const reflect_1 = require("@itrocks/reflect"); const reflect_2 = require("@itrocks/reflect"); const sort_1 = require("@itrocks/sort"); const sort_2 = require("@itrocks/sort"); const storage_1 = require("@itrocks/storage"); const storage_2 = require("@itrocks/storage"); const storage_3 = require("@itrocks/storage"); const mariadb_1 = require("mariadb"); exports.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 => (0, class_type_4.typeOf)(target).name.toLowerCase() }; 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('_'); } function mysqlDependsOn(dependencies) { Object.assign(depends, dependencies); } class Mysql extends storage_1.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 (0, mariadb_1.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 (exports.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 (exports.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 reflect_2.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 (exports.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 (exports.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 reflect_2.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 (exports.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 reflect_1.ReflectClass(type).properties) { const propertyType = property.type; if (propertyType instanceof property_type_1.CollectionType) continue; const propertyName = ((0, class_type_3.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 (exports.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 (exports.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 reflect_2.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 reflect_2.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 (exports.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 reflect_2.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 = (0, class_type_4.typeOf)(object); for (const candidate of new reflect_1.ReflectClass(component).properties) { if (!(0, composition_1.compositeOf)(component, candidate.name)) continue; const candidateType = candidate.type.type; if (!(0, class_type_3.isAnyType)(candidateType)) continue; if (!(0, class_type_1.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 reflect_1.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 reflect_2.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 === storage_3.Sort) { sortOption = new storage_3.Sort((0, sort_2.sortOf)(type)); } if (option instanceof storage_2.Limit) { limitOption = option; } if (option instanceof storage_3.Sort) { sortOption = option.properties.length ? option : new storage_3.Sort((0, sort_2.sortOf)(type)); } } Object.setPrototypeOf(search, type.prototype); const sql = this.propertiesToSearchSql(search); const [values] = await this.valuesToDb(search); if (exports.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 sort_1.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 (exports.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 ((0, class_type_2.isAnyFunction)(value)) { deferred.push(value); continue; } record[depends.columnOf(property)] = value; } return [record, deferred]; } } exports.Mysql = Mysql; //# sourceMappingURL=mysql.js.map