ionic-orm-2
Version:
Data-mapper ORM for Ionic WebSQL and SQLite
566 lines (552 loc) • 27.8 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { TransactionAlreadyStartedError } from "../error/TransactionAlreadyStartedError";
import { TransactionNotStartedError } from "../error/TransactionNotStartedError";
import { DataTypeNotSupportedByDriverError } from "../error/DataTypeNotSupportedByDriverError";
import { ColumnMetadata } from "../../metadata/ColumnMetadata";
import { QueryRunnerAlreadyReleasedError } from "../../query-runner/error/QueryRunnerAlreadyReleasedError";
/**
* Runs queries on a single websql database connection.
*
* Does not support compose primary keys with autoincrement field.
* todo: need to throw exception for this case.
*/
export class WebSqlQueryRunner {
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
constructor(databaseConnection, driver, logger) {
this.databaseConnection = databaseConnection;
this.driver = driver;
this.logger = logger;
// -------------------------------------------------------------------------
// Protected Properties
// -------------------------------------------------------------------------
/**
* Indicates if connection for this query runner is released.
* Once its released, query runner cannot run queries anymore.
*/
this.isReleased = false;
}
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
/**
* Releases database connection. This is needed when using connection pooling.
* If connection is not from a pool, it should not be released.
*/
release() {
if (this.databaseConnection.releaseCallback) {
this.isReleased = true;
return this.databaseConnection.releaseCallback();
}
return Promise.resolve();
}
/**
* Removes all tables from the currently connected database.
*/
clearDatabase() {
return __awaiter(this, void 0, void 0, function* () {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const selectDropsQuery = `select 'drop table ' || name || ';' as query from sqlite_master where type = 'table' and name != 'sqlite_sequence'`;
const dropQueries = yield this.query(selectDropsQuery);
yield Promise.all(dropQueries.map(q => this.query(q["query"])));
});
}
/**
* Starts transaction.
*/
beginTransaction() {
return __awaiter(this, void 0, void 0, function* () {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
if (this.databaseConnection.isTransactionActive)
throw new TransactionAlreadyStartedError();
this.databaseConnection.isTransactionActive = true;
// await this.query("BEGIN TRANSACTION");
});
}
/**
* Commits transaction.
*/
commitTransaction() {
return __awaiter(this, void 0, void 0, function* () {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
if (!this.databaseConnection.isTransactionActive)
throw new TransactionNotStartedError();
// await this.query("COMMIT");
this.databaseConnection.isTransactionActive = false;
});
}
/**
* Rollbacks transaction.
*/
rollbackTransaction() {
return __awaiter(this, void 0, void 0, function* () {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
if (!this.databaseConnection.isTransactionActive)
throw new TransactionNotStartedError();
yield this.query("ROLLBACK");
this.databaseConnection.isTransactionActive = false;
});
}
/**
* Checks if transaction is in progress.
*/
isTransactionActive() {
return this.databaseConnection.isTransactionActive;
}
/**
* Executes a given SQL query.
*/
query(query, parameters = []) {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
this.logger.logQuery(query, parameters);
return new Promise((ok, fail) => {
const _this = this;
this.databaseConnection.connection.transaction(function (transaction) {
transaction.executeSql(query, parameters, function (transaction, result) {
if (result.rows) {
let sqlResultSetRowListArray = Object.keys(result.rows).map(key => result.rows[key]);
ok(sqlResultSetRowListArray);
}
else {
ok(result);
}
}, function (transaction, error) {
_this.logger.logFailedQuery(query, parameters);
_this.logger.logQueryError(error);
fail(error);
});
});
});
}
/**
* Insert a new row into given table.
*/
insert(tableName, keyValues, generatedColumn) {
return __awaiter(this, void 0, void 0, function* () {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const keys = Object.keys(keyValues);
const columns = keys.map(key => this.driver.escapeColumnName(key)).join(", ");
const values = keys.map((key, index) => "$" + (index + 1)).join(",");
const sql = `INSERT INTO ${this.driver.escapeTableName(tableName)}(${columns}) VALUES (${values})`;
const parameters = keys.map(key => keyValues[key]);
this.logger.logQuery(sql, parameters);
return new Promise((ok, fail) => {
const _this = this;
this.databaseConnection.connection.transaction(function (transaction) {
transaction.executeSql(sql, parameters, function (transaction, result) {
if (generatedColumn)
return ok(result["insertId"]);
//return ok(this["lastID"]);
ok();
}, function (transaction, error) {
_this.logger.logFailedQuery(sql, parameters);
_this.logger.logQueryError(error);
fail(error);
});
});
});
});
}
/**
* Updates rows that match given conditions in the given table.
*/
update(tableName, valuesMap, conditions) {
return __awaiter(this, void 0, void 0, function* () {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const updateValues = this.parametrize(valuesMap).join(", ");
const conditionString = this.parametrize(conditions, Object.keys(valuesMap).length).join(" AND ");
const query = `UPDATE ${this.driver.escapeTableName(tableName)} SET ${updateValues} ${conditionString ? (" WHERE " + conditionString) : ""}`;
const updateParams = Object.keys(valuesMap).map(key => valuesMap[key]);
const conditionParams = Object.keys(conditions).map(key => conditions[key]);
const allParameters = updateParams.concat(conditionParams);
yield this.query(query, allParameters);
});
}
/**
* Deletes from the given table by a given conditions.
*/
delete(tableName, conditions) {
return __awaiter(this, void 0, void 0, function* () {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const conditionString = this.parametrize(conditions).join(" AND ");
const parameters = Object.keys(conditions).map(key => conditions[key]);
const query = `DELETE FROM "${tableName}" WHERE ${conditionString}`;
yield this.query(query, parameters);
});
}
/**
* Inserts rows into closure table.
*/
insertIntoClosureTable(tableName, newEntityId, parentId, hasLevel) {
return __awaiter(this, void 0, void 0, function* () {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
let sql = "";
if (hasLevel) {
sql = `INSERT INTO ${this.driver.escapeTableName(tableName)}(ancestor, descendant, level) ` +
`SELECT ancestor, ${newEntityId}, level + 1 FROM ${this.driver.escapeTableName(tableName)} WHERE descendant = ${parentId} ` +
`UNION ALL SELECT ${newEntityId}, ${newEntityId}, 1`;
}
else {
sql = `INSERT INTO ${this.driver.escapeTableName(tableName)}(ancestor, descendant) ` +
`SELECT ancestor, ${newEntityId} FROM ${this.driver.escapeTableName(tableName)} WHERE descendant = ${parentId} ` +
`UNION ALL SELECT ${newEntityId}, ${newEntityId}`;
}
yield this.query(sql);
const results = yield this.query(`SELECT MAX(level) as level FROM ${tableName} WHERE descendant = ${parentId}`);
return results && results[0] && results[0]["level"] ? parseInt(results[0]["level"]) + 1 : 1;
});
}
/**
* Loads all tables (with given names) from the database and creates a TableSchema from them.
*/
loadSchemaTables(tableNames, namingStrategy) {
return __awaiter(this, void 0, void 0, function* () {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
// if no tables given then no need to proceed
return [];
/*
if (!tableNames || !tableNames.length)
return [];
// load tables, columns, indices and foreign keys
const dbTables: ObjectLiteral[] = await this.query(`SELECT * FROM sqlite_master WHERE type = 'table' AND name != 'sqlite_sequence'`);
// if tables were not found in the db, no need to proceed
if (!dbTables || !dbTables.length)
return [];
// create table schemas for loaded tables
return Promise.all(dbTables.map(async dbTable => {
const tableSchema = new TableSchema(dbTable["name"]);
// load columns and indices
const [dbColumns, dbIndices, dbForeignKeys]: ObjectLiteral[][] = await Promise.all([
this.query(`PRAGMA table_info("${dbTable["name"]}")`),
this.query(`PRAGMA index_list("${dbTable["name"]}")`),
this.query(`PRAGMA foreign_key_list("${dbTable["name"]}")`),
]);
// find column name with auto increment
let autoIncrementColumnName: string|undefined = undefined;
const tableSql: string = dbTable["sql"];
if (tableSql.indexOf("AUTOINCREMENT") !== -1) {
autoIncrementColumnName = tableSql.substr(0, tableSql.indexOf("AUTOINCREMENT"));
const comma = autoIncrementColumnName.lastIndexOf(",");
const bracket = autoIncrementColumnName.lastIndexOf("(");
if (comma !== -1) {
autoIncrementColumnName = autoIncrementColumnName.substr(comma);
autoIncrementColumnName = autoIncrementColumnName.substr(0, autoIncrementColumnName.lastIndexOf("\""));
autoIncrementColumnName = autoIncrementColumnName.substr(autoIncrementColumnName.indexOf("\"") + 1);
} else if (bracket !== -1) {
autoIncrementColumnName = autoIncrementColumnName.substr(bracket);
autoIncrementColumnName = autoIncrementColumnName.substr(0, autoIncrementColumnName.lastIndexOf("\""));
autoIncrementColumnName = autoIncrementColumnName.substr(autoIncrementColumnName.indexOf("\"") + 1);
}
}
// create column schemas from the loaded columns
tableSchema.columns = dbColumns.map(dbColumn => {
const columnSchema = new ColumnSchema();
columnSchema.name = dbColumn["name"];
columnSchema.type = dbColumn["type"].toLowerCase();
columnSchema.default = dbColumn["dflt_value"] !== null && dbColumn["dflt_value"] !== undefined ? dbColumn["dflt_value"] : undefined;
columnSchema.isNullable = dbColumn["notnull"] === 0;
columnSchema.isPrimary = dbColumn["pk"] === 1;
columnSchema.comment = ""; // todo later
columnSchema.isGenerated = autoIncrementColumnName === dbColumn["name"];
const columnForeignKeys = dbForeignKeys
.filter(foreignKey => foreignKey["from"] === dbColumn["name"])
.map(foreignKey => {
const keyName = namingStrategy.foreignKeyName(dbTable["name"], [foreignKey["from"]], foreignKey["table"], [foreignKey["to"]]);
return new ForeignKeySchema(keyName, [foreignKey["from"]], [foreignKey["to"]], foreignKey["table"], foreignKey["on_delete"]); // todo: how websql return from and to when they are arrays? (multiple column foreign keys)
});
tableSchema.addForeignKeys(columnForeignKeys);
return columnSchema;
});
// create primary key schema
await Promise.all(dbIndices
.filter(index => index["origin"] === "pk")
.map(async index => {
const indexInfos: ObjectLiteral[] = await this.query(`PRAGMA index_info("${index["name"]}")`);
const indexColumns = indexInfos.map(indexInfo => indexInfo["name"]);
indexColumns.forEach(indexColumn => {
tableSchema.primaryKeys.push(new PrimaryKeySchema(index["name"], indexColumn));
});
}));
// create index schemas from the loaded indices
const indicesPromises = dbIndices
.filter(dbIndex => {
return dbIndex["origin"] !== "pk" &&
(!tableSchema.foreignKeys.find(foreignKey => foreignKey.name === dbIndex["name"])) &&
(!tableSchema.primaryKeys.find(primaryKey => primaryKey.name === dbIndex["name"]));
})
.map(dbIndex => dbIndex["name"])
.filter((value, index, self) => self.indexOf(value) === index) // unqiue
.map(async dbIndexName => {
const dbIndex = dbIndices.find(dbIndex => dbIndex["name"] === dbIndexName);
const indexInfos: ObjectLiteral[] = await this.query(`PRAGMA index_info("${dbIndex!["name"]}")`);
const indexColumns = indexInfos.map(indexInfo => indexInfo["name"]);
// check if db index is generated by websql itself and has special use case
if (dbIndex!["name"].substr(0, "sqlite_autoindex".length) === "sqlite_autoindex") {
if (dbIndex!["unique"] === 1) { // this means we have a special index generated for a column
// so we find and update the column
indexColumns.forEach(columnName => {
const column = tableSchema.columns.find(column => column.name === columnName);
if (column)
column.isUnique = true;
});
}
return Promise.resolve(undefined);
} else {
return new IndexSchema(dbTable["name"], dbIndex!["name"], indexColumns, dbIndex!["unique"] === "1");
}
});
const indices = await Promise.all(indicesPromises);
tableSchema.indices = indices.filter(index => !!index) as IndexSchema[];
return tableSchema;
}));*/
});
}
/**
* Creates a new table from the given table metadata and column metadatas.
*/
createTable(table) {
return __awaiter(this, void 0, void 0, function* () {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
// skip columns with foreign keys, we will add them later
const columnDefinitions = table.columns.map(column => this.buildCreateColumnSql(column)).join(", ");
let sql = `CREATE TABLE IF NOT EXISTS "${table.name}" (${columnDefinitions}`;
const primaryKeyColumns = table.columns.filter(column => column.isPrimary && !column.isGenerated);
if (primaryKeyColumns.length > 0)
sql += `, PRIMARY KEY(${primaryKeyColumns.map(column => `${column.name}`).join(", ")})`; // for some reason column escaping here generates a wrong schema
sql += `)`;
yield this.query(sql);
});
}
/**
* Creates a new column from the column metadata in the table.
*/
createColumns(tableSchema, columns) {
return __awaiter(this, void 0, void 0, function* () {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
yield this.recreateTable(tableSchema);
});
}
/**
* Changes a column in the table.
* Changed column looses all its keys in the db.
*/
changeColumns(tableSchema, changedColumns) {
return __awaiter(this, void 0, void 0, function* () {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
return this.recreateTable(tableSchema);
});
}
/**
* Drops the columns in the table.
*/
dropColumns(tableSchema, columns) {
return __awaiter(this, void 0, void 0, function* () {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const newTable = tableSchema.clone();
newTable.removeColumns(columns);
return this.recreateTable(newTable);
});
}
/**
* Updates table's primary keys.
*/
updatePrimaryKeys(dbTable) {
return __awaiter(this, void 0, void 0, function* () {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
return this.recreateTable(dbTable);
});
}
/**
* Creates a new foreign keys.
*/
createForeignKeys(tableSchema, foreignKeys) {
return __awaiter(this, void 0, void 0, function* () {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const newTable = tableSchema.clone();
newTable.addForeignKeys(foreignKeys);
return this.recreateTable(newTable);
});
}
/**
* Drops a foreign keys from the table.
*/
dropForeignKeys(tableSchema, foreignKeys) {
return __awaiter(this, void 0, void 0, function* () {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const newTable = tableSchema.clone();
newTable.removeForeignKeys(foreignKeys);
return this.recreateTable(newTable);
});
}
/**
* Creates a new index.
*/
createIndex(index) {
return __awaiter(this, void 0, void 0, function* () {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const columnNames = index.columnNames.map(columnName => `"${columnName}"`).join(",");
const sql = `CREATE ${index.isUnique ? "UNIQUE " : ""}INDEX "${index.name}" ON "${index.tableName}"(${columnNames})`;
yield this.query(sql);
});
}
/**
* Drops an index from the table.
*/
dropIndex(tableName, indexName, isGenerated = false) {
return __awaiter(this, void 0, void 0, function* () {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const sql = `DROP INDEX "${indexName}"`;
yield this.query(sql);
});
}
/**
* Creates a database type from a given column metadata.
*/
normalizeType(column) {
switch (column.normalizedDataType) {
case "string":
return "character varying(" + (column.length ? column.length : 255) + ")";
case "text":
return "text";
case "boolean":
return "boolean";
case "integer":
case "int":
return "integer";
case "smallint":
return "smallint";
case "bigint":
return "bigint";
case "float":
return "real";
case "double":
case "number":
return "double precision";
case "decimal":
if (column.precision && column.scale) {
return `decimal(${column.precision},${column.scale})`;
}
else if (column.scale) {
return `decimal(${column.scale})`;
}
else if (column.precision) {
return `decimal(${column.precision})`;
}
else {
return "decimal";
}
case "date":
return "date";
case "time":
if (column.timezone) {
return "time with time zone";
}
else {
return "time without time zone";
}
case "datetime":
if (column.timezone) {
return "timestamp with time zone";
}
else {
return "timestamp without time zone";
}
case "json":
return "json";
case "simple_array":
return column.length ? "character varying(" + column.length + ")" : "text";
}
throw new DataTypeNotSupportedByDriverError(column.type, "SQLite");
}
// -------------------------------------------------------------------------
// Protected Methods
// -------------------------------------------------------------------------
/**
* Parametrizes given object of values. Used to create column=value queries.
*/
parametrize(objectLiteral, startIndex = 0) {
return Object.keys(objectLiteral).map((key, index) => this.driver.escapeColumnName(key) + "=$" + (startIndex + index + 1));
}
/**
* Builds a query for create column.
*/
buildCreateColumnSql(column) {
let c = "\"" + column.name + "\"";
if (column instanceof ColumnMetadata) {
c += " " + this.normalizeType(column);
}
else {
c += " " + column.type;
}
if (column.isNullable !== true)
c += " NOT NULL";
if (column.isUnique === true)
c += " UNIQUE";
if (column.isGenerated === true)
c += " PRIMARY KEY AUTOINCREMENT";
return c;
}
recreateTable(tableSchema) {
return __awaiter(this, void 0, void 0, function* () {
// const withoutForeignKeyColumns = columns.filter(column => column.foreignKeys.length === 0);
// const createForeignKeys = options && options.createForeignKeys;
const columnDefinitions = tableSchema.columns.map(dbColumn => this.buildCreateColumnSql(dbColumn)).join(", ");
const columnNames = tableSchema.columns.map(column => `"${column.name}"`).join(", ");
let sql1 = `CREATE TABLE "temporary_${tableSchema.name}" (${columnDefinitions}`;
// if (options && options.createForeignKeys) {
tableSchema.foreignKeys.forEach(foreignKey => {
const columnNames = foreignKey.columnNames.map(name => `"${name}"`).join(", ");
const referencedColumnNames = foreignKey.referencedColumnNames.map(name => `"${name}"`).join(", ");
sql1 += `, FOREIGN KEY(${columnNames}) REFERENCES "${foreignKey.referencedTableName}"(${referencedColumnNames})`;
});
const primaryKeyColumns = tableSchema.columns.filter(column => column.isPrimary && !column.isGenerated);
if (primaryKeyColumns.length > 0)
sql1 += `, PRIMARY KEY(${primaryKeyColumns.map(column => `${column.name}`).join(", ")})`; // for some reason column escaping here generate a wrong schema
sql1 += ")";
// todo: need also create uniques and indices?
// recreate a table with a temporary name
yield this.query(sql1);
// migrate all data from the table into temporary table
const sql2 = `INSERT INTO "temporary_${tableSchema.name}" SELECT ${columnNames} FROM "${tableSchema.name}"`;
yield this.query(sql2);
// drop old table
const sql3 = `DROP TABLE "${tableSchema.name}"`;
yield this.query(sql3);
// rename temporary table
const sql4 = `ALTER TABLE "temporary_${tableSchema.name}" RENAME TO "${tableSchema.name}"`;
yield this.query(sql4);
// also re-create indices
const indexPromises = tableSchema.indices.map(index => this.createIndex(index));
// const uniquePromises = tableSchema.uniqueKeys.map(key => this.createIndex(key));
yield Promise.all(indexPromises /*.concat(uniquePromises)*/);
});
}
}
//# sourceMappingURL=WebSqlQueryRunner.js.map