Skip to content

Commit

Permalink
fix: modify the SqliteDriver API to support autosave (#41)
Browse files Browse the repository at this point in the history
* Modified the SqliteDriver API to behave more like the original TypeORM's SqljsDriver to support the browser in the future.
  • Loading branch information
uki00a authored Apr 5, 2020
1 parent c2b615a commit 48f0e2b
Show file tree
Hide file tree
Showing 20 changed files with 536 additions and 51 deletions.
7 changes: 6 additions & 1 deletion src/driver/DriverUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Driver } from "./Driver.ts";
import { hash } from "../util/StringUtils.ts";
import type { AutoSavable, AutoSavableDriver } from "./types/AutoSavable.ts";

/**
* Common driver utility functions.
Expand Down Expand Up @@ -35,7 +36,7 @@ export class DriverUtils {

/**
* Builds column alias from given alias name and column name.
*
*
* If alias length is greater than the limit (if any) allowed by the current
* driver, replaces it with a hashed string.
*
Expand All @@ -55,6 +56,10 @@ export class DriverUtils {
return columnAliasName;
}

static isAutoSavable(driver: Driver): driver is AutoSavableDriver {
return typeof (driver as AutoSavableDriver)["autoSave"] === "function";
}

// -------------------------------------------------------------------------
// Private Static Methods
// -------------------------------------------------------------------------
Expand Down
3 changes: 2 additions & 1 deletion src/driver/sqlite/SqliteConnectionOptions.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {BaseConnectionOptions} from "../../connection/BaseConnectionOptions.ts";
import {AutoSavableOptions} from "../types/AutoSavable.ts";

/**
* Sqlite-specific connection options.
*/
export interface SqliteConnectionOptions extends BaseConnectionOptions {
export interface SqliteConnectionOptions extends BaseConnectionOptions, AutoSavableOptions {

/**
* Database type.
Expand Down
141 changes: 136 additions & 5 deletions src/driver/sqlite/SqliteDriver.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {open, DB} from "../../../vendor/https/deno.land/x/sqlite/mod.ts";
import {open, save, DB} from "../../../vendor/https/deno.land/x/sqlite/mod.ts";
import {ensureDir} from "../../../vendor/https/deno.land/std/fs/mod.ts";
import {dirname} from "../../../vendor/https/deno.land/std/path/mod.ts";
import {SqliteQueryRunner} from "./SqliteQueryRunner.ts";
Expand All @@ -7,12 +7,17 @@ import {Connection} from "../../connection/Connection.ts";
import {SqliteConnectionOptions} from "./SqliteConnectionOptions.ts";
import {ColumnType} from "../types/ColumnTypes.ts";
import {QueryRunner} from "../../query-runner/QueryRunner.ts";
import {PlatformTools} from "../../platform/PlatformTools.ts";
import {AbstractSqliteDriver} from "../sqlite-abstract/AbstractSqliteDriver.ts";
import type {AutoSavableDriver} from "../types/AutoSavable.ts";
import {NotImplementedError} from "../../error/NotImplementedError.ts";

/**
* Organizes communication with sqlite DBMS.
*
* This driver provides the same behavior as the original TypeORM's SqljsDriver.
*/
export class SqliteDriver extends AbstractSqliteDriver {
export class SqliteDriver extends AbstractSqliteDriver implements AutoSavableDriver {

// -------------------------------------------------------------------------
// Public Properties
Expand All @@ -23,7 +28,97 @@ export class SqliteDriver extends AbstractSqliteDriver {
*/
options: SqliteConnectionOptions;

databaseConnection: DB;
databaseConnection!: DB;

/**
* This method is simply copied from `SqljsDriver#load`
*
* Loads a database from a given file (Deno), local storage key (browser) or array.
* This will delete the current database!
*/
async load(fileNameOrLocalStorageOrData: string | Uint8Array, checkIfFileOrLocalStorageExists: boolean = true): Promise<any> {
if (typeof fileNameOrLocalStorageOrData === "string") {
// content has to be loaded
if (PlatformTools.type === "deno") {
// Node.js
// fileNameOrLocalStorageOrData should be a path to the file
if (checkIfFileOrLocalStorageExists && !PlatformTools.fileExist(fileNameOrLocalStorageOrData)) {
throw new Error(`File ${fileNameOrLocalStorageOrData} does not exist`);
}

// TODO(uki00a) Should we disconnect from current database if exists?
this.databaseConnection = await this.createDatabaseConnectionWithImport(fileNameOrLocalStorageOrData);
}
else {
// browser
// fileNameOrLocalStorageOrData should be a local storage / indexedDB key
throw new NotImplementedError("SqliteDriver#load");
/*
let localStorageContent = null;
if (this.options.useLocalForage) {
if (window.localforage) {
localStorageContent = await window.localforage.getItem(fileNameOrLocalStorageOrData);
} else {
throw new Error(`localforage is not defined - please import localforage.js into your site`);
}
} else {
localStorageContent = PlatformTools.getGlobalVariable().localStorage.getItem(fileNameOrLocalStorageOrData);
}
if (localStorageContent != null) {
// localStorage value exists.
return this.createDatabaseConnectionWithImport(JSON.parse(localStorageContent));
}
else if (checkIfFileOrLocalStorageExists) {
throw new Error(`File ${fileNameOrLocalStorageOrData} does not exist`);
}
else {
// localStorage value doesn't exist and checkIfFileOrLocalStorageExists is set to false.
// Therefore open a database without importing anything.
// localStorage value will be written on first write operation.
return this.createDatabaseConnectionWithImport();
}
*/
}
}
else {
throw new NotImplementedError("SqliteDriver#load does not currently support Uint8Array");
// return this.createDatabaseConnectionWithImport(fileNameOrLocalStorageOrData);
}
}

/**
* This method is simply copied from SqljsDriver#autoSave.
*/
async autoSave(): Promise<void> {
if (this.options.autoSave) {
if (this.options.autoSaveCallback) {
await this.options.autoSaveCallback(this.databaseConnection.data());
}
else {
await this.save();
}
}
}

async save(location?: string): Promise<void> {
if (location) {
return this.saveToLocation(location);
}

if (this.isInMemory()) {
return;
}

save(this.databaseConnection);
}

/**
* Returns the current database as Uint8Array.
*/
export(): Uint8Array {
return this.databaseConnection.data();
}

// -------------------------------------------------------------------------
// Constructor
Expand Down Expand Up @@ -95,11 +190,19 @@ export class SqliteDriver extends AbstractSqliteDriver {
* Creates connection with the database.
*/
protected async createDatabaseConnection() {
return this.createDatabaseConnectionWithImport(this.options.database);
}

/**
* Creates connection with a database.
* If database is specified it is loaded, otherwise a new empty database is created.
*/
protected async createDatabaseConnectionWithImport(database: string) {
if (!this.isInMemory()) {
await this.createDatabaseDirectory(this.options.database);
await this.createDatabaseDirectory(database);
}

const databaseConnection = await open(this.options.database);
const databaseConnection = await open(database, true);

// we need to enable foreign keys in sqlite to make sure all foreign key related features
// working properly. this also makes onDelete to work with sqlite.
Expand All @@ -126,4 +229,32 @@ export class SqliteDriver extends AbstractSqliteDriver {
protected createDatabaseDirectory(fullPath: string): Promise<void> {
return ensureDir(dirname(fullPath));
}

private async saveToLocation(location: string) {
if (PlatformTools.type === "deno") {
try {
const content = this.export();
await PlatformTools.writeFile(location, content);
}
catch (e) {
throw new Error(`Could not save database, error: ${e}`);
}
} else {
throw new NotImplementedError("SqliteDriver does not currently support browser");
/*
const database: Uint8Array = this.databaseConnection.export();
// convert Uint8Array to number array to improve local-storage storage
const databaseArray = [].slice.call(database);
if (this.options.useLocalForage) {
if (window.localforage) {
await window.localforage.setItem(path, JSON.stringify(databaseArray));
} else {
throw new Error(`localforage is not defined - please import localforage.js into your site`);
}
} else {
PlatformTools.getGlobalVariable().localStorage.setItem(path, JSON.stringify(databaseArray));
}
*/
}
}
}
34 changes: 12 additions & 22 deletions src/driver/sqlite/SqliteQueryRunner.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {DB, save} from "../../../vendor/https/deno.land/x/sqlite/mod.ts";
import {DB} from "../../../vendor/https/deno.land/x/sqlite/mod.ts";
import {QueryRunnerAlreadyReleasedError} from "../../error/QueryRunnerAlreadyReleasedError.ts";
import {QueryFailedError} from "../../error/QueryFailedError.ts";
import {AbstractSqliteQueryRunner} from "../sqlite-abstract/AbstractSqliteQueryRunner.ts";
Expand Down Expand Up @@ -30,6 +30,17 @@ export class SqliteQueryRunner extends AbstractSqliteQueryRunner {
this.broadcaster = new Broadcaster(this);
}

/**
* Commits transaction.
* Error will be thrown if transaction was not started.
*/
async commitTransaction(): Promise<void> {
await super.commitTransaction();
await this.driver.autoSave();
}



/**
* Executes a given SQL query.
*/
Expand Down Expand Up @@ -67,34 +78,13 @@ export class SqliteQueryRunner extends AbstractSqliteQueryRunner {
const isInsertQuery = SqlUtils.isInsertQuery(query);
try {
const result = run();
await this.saveDatabaseToFileIfNeeded(databaseConnection, query);
return result;
} catch (err) {
connection.logger.logQueryError(err, query, parameters, this);
throw new QueryFailedError(query, parameters, err);
}
}

// TODO(uki00a) Optimize this method.
private async saveDatabaseToFileIfNeeded(databaseConnection: DB, executedQuery: string): Promise<void> {
if (this.driver.isInMemory()) {
return;
}

// FIXME(uki00a) I'm not sure if this is correct or not.
if (SqlUtils.isCommitQuery(executedQuery)) {
this.connection.logger.log("info", "Saving database to file...", this);
await save(databaseConnection);
return;
}

const hasSideEffects = !SqlUtils.isSelectQuery(executedQuery);
if (hasSideEffects && !this.isTransactionActive) {
this.connection.logger.log("info", "Saving database to file...", this);
await save(databaseConnection);
}
}

private getLastInsertRowID(databaseConnection: DB): unknown {
const query = "SELECT last_insert_rowid()";
this.connection.logger.logQuery(query, [], this);
Expand Down
3 changes: 2 additions & 1 deletion src/driver/sqljs/SqljsDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {PlatformTools} from "../../platform/PlatformTools.ts";
import {EntityMetadata} from "../../metadata/EntityMetadata.ts";
import {OrmUtils} from "../../util/OrmUtils.ts";
import {ObjectLiteral} from "../../common/ObjectLiteral.ts";
import {AutoSavableDriver} from "../types/AutoSavable.ts";

// This is needed to satisfy the typescript compiler.
interface Window {
Expand All @@ -17,7 +18,7 @@ interface Window {
}
declare var window: Window;

export class SqljsDriver extends AbstractSqliteDriver {
export class SqljsDriver extends AbstractSqliteDriver implements AutoSavableDriver {
// The driver specific options.
options!: SqljsConnectionOptions;

Expand Down
21 changes: 21 additions & 0 deletions src/driver/types/AutoSavable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {Driver} from "../Driver.ts";

export interface AutoSavableOptions {
/**
* Enables the autoSave mechanism which either saves to location
* or calls autoSaveCallback every time a change to the database is made.
*/
readonly autoSave?: boolean;

/**
* A function that gets called on every change instead of the internal autoSave function.
* autoSave has to be enabled for this to work.
*/
readonly autoSaveCallback?: Function;
}

export interface AutoSavableDriver extends Driver {
options: Driver["options"] & AutoSavableOptions;
autoSave(): Promise<void>;
save(): Promise<void>;
}
5 changes: 5 additions & 0 deletions src/entity-manager/EntityManagerFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {MongoEntityManager} from "./MongoEntityManager.ts";
import {MongoDriver} from "../driver/mongodb/MongoDriver.ts";
import {SqljsEntityManager} from "./SqljsEntityManager.ts";
import {SqljsDriver} from "../driver/sqljs/SqljsDriver.ts";
import {SqliteEntityManager} from "./SqliteEntityManager.ts";
import {SqliteDriver} from "../driver/sqlite/SqliteDriver.ts";
import {QueryRunner} from "../query-runner/QueryRunner.ts";

/**
Expand All @@ -21,6 +23,9 @@ export class EntityManagerFactory {
if (connection.driver instanceof SqljsDriver)
return new SqljsEntityManager(connection, queryRunner);

if (connection.driver instanceof SqliteDriver)
return new SqliteEntityManager(connection, queryRunner);

return new EntityManager(connection, queryRunner);
}

Expand Down
53 changes: 53 additions & 0 deletions src/entity-manager/SqliteEntityManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// This code is simply copied from src/entity-manager/SqljsEntityManager.ts
import {Connection} from "../connection/Connection.ts";
import {QueryRunner} from "../query-runner/QueryRunner.ts";
import {EntityManager} from "./EntityManager.ts";
import {SqliteDriver} from "../driver/sqlite/SqliteDriver.ts";

/**
* A special EntityManager that includes import/export and load/save function
* that are unique to deno-sqlite.
*
* This class provides the same behavior as the original TypeORM's SqljsEntityManager.
*/
export class SqliteEntityManager extends EntityManager {
private driver: SqliteDriver;

// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------

constructor(connection: Connection, queryRunner?: QueryRunner) {
super(connection, queryRunner);
this.driver = connection.driver as SqliteDriver;
}

// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------

/**
* Loads either the definition from a file (Deno) or localstorage (browser)
* or uses the given definition to open a new database.
*/
async loadDatabase(fileNameOrLocalStorageOrData: string | Uint8Array): Promise<void> {
await this.driver.load(fileNameOrLocalStorageOrData);
}

/**
* Saves the current database to a file (Deno) or localstorage (browser)
* if fileNameOrLocalStorage is not set options.location is used.
*/
async saveDatabase(fileNameOrLocalStorage: string): Promise<void> {
await this.driver.save(fileNameOrLocalStorage);
}

/**
* Returns the current database definition.
*/
exportDatabase(): Uint8Array {
return this.driver.export();
}

}

Loading

0 comments on commit 48f0e2b

Please sign in to comment.