diff --git a/dexie/dexie-tests.ts b/dexie/dexie-tests.ts new file mode 100644 index 000000000..89318f070 --- /dev/null +++ b/dexie/dexie-tests.ts @@ -0,0 +1,256 @@ +/// + +import Dexie = require("Dexie"); + +module Utils { + + export class Console { + textarea: HTMLTextAreaElement; + + constructor() { + this.textarea = document.createElement('textarea'); + } + + log(txt: string, type?: string) { + if (type) this.textarea.value += type + " "; + this.textarea.value += txt + "\n"; + } + error = function (txt: string) { + this.log(txt, "ERROR!"); + } + } + +} + +module AppDb { + + export class AppDatabase extends Dexie { + + contacts: Dexie.Table; + emails: Dexie.Table; + phones: Dexie.Table; + + constructor() { + + super("MyTypeScriptAppDb"); + + var db = this; + + // + // Define tables and indexes + // + db.version(1).stores({ + contacts: '++id, first, last', + emails: '++id, contactId, type, email', + phones: '++id, contactId, type, phone', + }); + + // Let's physically map Contact class to contacts table. + // This will make it possible to call loadEmailsAndPhones() + // directly on retrieved database objects. + db.contacts.mapToClass(Contact); + } + } + + /* Just for code completion and compilation - defines + * the interface of objects stored in the emails table. + */ + export interface IEmailAddress { + id?: number; + contactId: number; + type: string; + email: string; + } + + /* Just for code completion and compilation - defines + * the interface of objects stored in the phones table. + */ + export interface IPhoneNumber { + id?: number; + contactId: number; + type: string; + phone: string; + } + + /* This is a 'physical' class that is mapped to + * the contacts table. We can have methods on it that + * we could call on retrieved database objects. + */ + export class Contact { + id: number; + first: string; + last: string; + emails: IEmailAddress[]; + phones: IPhoneNumber[]; + + constructor(first: string, last: string, id?: number) { + this.first = first; + this.last = last; + if (id) this.id = id; + } + + loadEmailsAndPhones() { + return Dexie.Promise.all( + db.emails + .where('contactId').equals(this.id) + .toArray(emails => this.emails = emails) + , + db.phones + .where('contactId').equals(this.id) + .toArray(phones => this.phones = phones) + + ).then(x => this); + } + + save() { + return db.transaction('rw', db.contacts, db.emails, db.phones, () => { + Dexie.Promise.all( + // Save existing arrays + Dexie.Promise.all(this.emails.map(email => db.emails.put(email))), + Dexie.Promise.all(this.phones.map(phone => db.phones.put(phone)))) + .then(results => { + // Remove items from DB that is was not saved here: + var emailIds = results[0], // array of resulting primary keys + phoneIds = results[1]; // array of resulting primary keys + + db.emails.where('contactId').equals(this.id) + .and(email => emailIds.indexOf(email.id) === -1) + .delete(); + + db.phones.where('contactId').equals(this.id) + .and(phone => phoneIds.indexOf(phone.id) === -1) + .delete(); + + // At last, save our own properties. + // (Must not do put(this) because we would get + // reduntant emails/phones arrays saved into db) + db.contacts.put( + new Contact(this.first, this.last, this.id)) + .then(id => this.id = id); + }); + }); + } + } + + export var db = new AppDatabase(); + db.open(); +} + + +import Console = Utils.Console; +import db = AppDb.db; + +document.addEventListener('DOMContentLoaded', () => { + + // Initialize our Console widget - it will log browser window. + var console = new Console(); + document.getElementById('consoleArea').appendChild(console.textarea); + + // Test it: + console.log("Hello world!"); + + // Make sure to never miss any unexpected error: + Dexie.Promise.on.error.subscribe(e => { + // Log any uncatched error: + console.error(e); + }); + + // + // Let's clear and re-seed the database: + // + clearDatabase() + .then(seedDatabase) + .then(playALittle_add_phone_to_adam) + .then(printContacts); + + function clearDatabase() { + console.log("Clearing database..."); + return Dexie.Promise.all( + db.contacts.clear(), + db.emails.clear(), + db.phones.clear()); + } + + function seedDatabase() { + console.log("Seeding database with some contacts..."); + return db.transaction('rw', db.contacts, db.emails, db.phones, () => { + // Populate a contact + db.contacts.add(new AppDb.Contact('Arnold', 'Fitzgerald')).then(id => { + // Populate some emails and phone numbers for the contact + db.emails.add({ contactId: id, type: 'home', email: 'arnold@email.com' }); + db.emails.add({ contactId: id, type: 'work', email: 'arnold@abc.com' }); + db.phones.add({ contactId: id, type: 'home', phone: '12345678' }); + db.phones.add({ contactId: id, type: 'work', phone: '987654321' }); + }); + + // ... and another one... + db.contacts.add(new AppDb.Contact('Adam', 'Tensta')).then(id => { + // Populate some emails and phone numbers for the contact + db.emails.add({ contactId: id, type: 'home', email: 'adam@tensta.se' }); + db.phones.add({ contactId: id, type: 'work', phone: '88888888' }); + }); + }); + } + + function playALittle_add_phone_to_adam() { + // Now, just to examplify how to use the save() method as an alternative + // to db.phones.add(), we will add yet another phone number + // to an existing contact and then re-save it: + console.log("Playing a little: adding another phone entry for Adam Tensta..."); + return db.contacts + .where('last').equals('Tensta').first(c => c.loadEmailsAndPhones()) + .then(contact => { + // Also add another phone number to Adam Tensta: + contact.phones.push({ + contactId: contact.id, + type: 'custom', + phone: '112' + }); + contact.save(); + }); + } + + function printContacts() { + + // Now we're gonna list all contacts starting with letter 'A' + // and print them out. + // For each contact, also resolve its collection of + // phone number entries and email addresses by reverse-quering + // the foreign tables. + + // For atomicity and speed, use a single transaction for the + // queries to make: + db.transaction('r', [db.contacts, db.phones, db.emails], () => { + + // Query some contacts + return db.contacts + .where('first').startsWithIgnoreCase('a') + .sortBy('id') + .then(contacts => + + // Resolve array properties 'emails' and 'phones' + // on each and every contact: + Dexie.Promise.all( + contacts.map(contact => + contact.loadEmailsAndPhones())) + ); + + }).then(contacts => { + + // Print result + console.log("Database contains the following contacts:"); + contacts.forEach(contact => { + console.log(contact.id + ". " + contact.first + " " + contact.last); + console.log(" Phone numbers: "); + contact.phones.forEach(phone => { + console.log(" " + phone.phone + "(" + phone.type + ")"); + }); + console.log(" Emails: "); + contact.emails.forEach(email => { + console.log(" " + email.email + "(" + email.type + ")"); + }); + }); + }); + } +}); + diff --git a/dexie/dexie.d.ts b/dexie/dexie.d.ts new file mode 100644 index 000000000..7d498f4d3 --- /dev/null +++ b/dexie/dexie.d.ts @@ -0,0 +1,316 @@ + +interface Thenable { + then(onFulfilled: (value: R) => Thenable, onRejected: (error: any) => Thenable): Thenable; + then(onFulfilled: (value: R) => Thenable, onRejected?: (error: any) => U): Thenable; + then(onFulfilled: (value: R) => U, onRejected: (error: any) => Thenable): Thenable; + then(onFulfilled?: (value: R) => U, onRejected?: (error: any) => U): Thenable; +} + +declare class Dexie { + constructor(databaseName: string); + + constructor(databaseName: string, options: { addons: Array<(db: Dexie) => void> }); + + name: string; + tables: Dexie.Table[]; + verno: number; + + static addons: Array<(db: Dexie) => void>; + static version: number; + + static getDatabaseNames(): Dexie.Promise>; + + static getDatabaseNames(onFulfilled: (value: Array) => Thenable): Dexie.Promise; + + static getDatabaseNames(onFulfilled: (value: Array) => U): Dexie.Promise; + + static getByKeyPath(obj: Object, keyPath: string): any; + + static setByKeyPath(obj: Object, keyPath: string, value: any): void; + + static delByKeyPath(obj: Object, keyPath: string): void; + + static shallowClone(obj: Object): Object; + + static deepClone(obj: Object): Object; + + version(versionNumber: Number): Dexie.Version + + on: { + (eventName: string, subscriber: () => any): void; + (eventName: string, subscriber: () => any, bSticky: boolean): void; + ready: Dexie.DexieOnReadyEvent; + error: Dexie.DexieErrorEvent; + populate: Dexie.DexieEvent; + blocked: Dexie.DexieEvent; + versionchange: Dexie.DexieVersionChangeEvent; + } + + open(): Dexie.Promise; + + table(tableName: string): Dexie.Table; + + table(tableName: string): Dexie.Table; + + table(tableName: string): Dexie.Table; + + transaction(mode: string, table: Dexie.Table, scope: () => Thenable): Dexie.Promise; + + transaction(mode: string, table: Dexie.Table, table2: Dexie.Table, scope: () => Thenable): Dexie.Promise; + + transaction(mode: string, table: Dexie.Table, table2: Dexie.Table, table3: Dexie.Table, scope: () => Thenable): Dexie.Promise; + + transaction(mode: string, tables: Dexie.Table[], scope: () => Thenable): Dexie.Promise; + + transaction(mode: string, table: Dexie.Table, scope: () => U): Dexie.Promise; + + transaction(mode: string, table: Dexie.Table, table2: Dexie.Table, scope: () => U): Dexie.Promise; + + transaction(mode: string, table: Dexie.Table, table2: Dexie.Table, table3: Dexie.Table, scope: () => U): Dexie.Promise; + + transaction(mode: string, tables: Dexie.Table[], scope: () => U): Dexie.Promise; + + close(): void; + + delete(): Dexie.Promise; + + isOpen(): boolean; + + hasFailed(): boolean; + + backendDB(): IDBDatabase; + + vip(scopeFunction: () => U): U; +} + +declare module Dexie { + + class Promise implements Thenable { + constructor(callback: (resolve: (value?: Thenable) => void, reject: (error?: any) => void) => void); + + constructor(callback: (resolve: (value?: R) => void, reject: (error?: any) => void) => void); + + then(onFulfilled: (value: R) => Thenable, onRejected: (error: any) => Thenable): Promise; + + then(onFulfilled: (value: R) => Thenable, onRejected?: (error: any) => U): Promise; + + then(onFulfilled: (value: R) => U, onRejected: (error: any) => Thenable): Promise; + + then(onFulfilled?: (value: R) => U, onRejected?: (error: any) => U): Promise; + + catch(onRejected: (error: any) => Promise): Promise; + + catch(ExceptionType: Function, onRejected: (error : any) => Promise): Promise; + + catch(errorName: string, onRejected: (error : any) => Promise): Promise; + + finally(onFinally: () => any): Promise; + + onuncatched: () => any; + } + + module Promise { + function resolve(value?: Thenable): Promise; + + function resolve(value?: R): Promise; + + function reject(error: any): Promise; + + function all(promises: Thenable[]): Promise; + + function all(...promises: Thenable[]): Promise; + + function race(promises: Thenable[]): Promise; + + function newPSD(scope: () => R): R; + + var PSD: any; + + var on: { + (eventName: string, subscriber: (...args : any[]) => any): void; + error: DexieErrorEvent; + } + } + + + interface Version { + stores(schema: { [key: string]: string }): Version; + upgrade(fn: (trans: Transaction) => void): Version; + } + + interface Transaction { + active: boolean; + db: Dexie; + mode: string; + idbtrans: IDBTransaction; + tables: { [type: string]: Table }; + storeNames: Array; + on: { + (eventName: string, subscriber: () => any): void; + complete: DexieEvent; + abort: DexieEvent; + error: DexieEvent; + } + abort(): void; + table(tableName: string): Table; + table(tableName: string): Table; + table(tableName: string): Table; + } + + interface DexieEvent { + subscribe(fn: () => any) : void; + unsubscribe(fn: () => any) : void; + fire() : any; + } + + interface DexieErrorEvent { + subscribe(fn: (error: any) => any) : void; + unsubscribe(fn: (error: any) => any) : void; + fire(error: any) : any; + } + + interface DexieVersionChangeEvent { + subscribe(fn: (event: IDBVersionChangeEvent) => any) : void; + unsubscribe(fn: (event: IDBVersionChangeEvent) => any) : void; + fire(event: IDBVersionChangeEvent) : any; + } + + interface DexieOnReadyEvent + { + subscribe(fn: () => any, bSticky: boolean) : void; + unsubscribe(fn: () => any) : void; + fire() : any; + } + + interface Table { + name: string; + schema: TableSchema; + hook: { + (eventName: string, subscriber: () => any): void; + creating: DexieEvent; + reading: DexieEvent; + updating: DexieEvent; + deleting: DexieEvent; + } + + get(key: Key): Promise; + where(index: string): WhereClause; + + filter(fn: (obj: T) => boolean): Collection; + + count(): Promise; + count(onFulfilled: (value: number) => Thenable): Promise; + count(onFulfilled: (value: number) => U): Promise; + + offset(n: number): Collection; + + limit(n: number): Collection; + + each(callback: (obj: T, cursor: IDBCursor) => any): Promise; + + toArray(): Promise>; + toArray(onFulfilled: (value: Array) => Thenable): Promise; + toArray(onFulfilled: (value: Array) => U): Promise; + + toCollection(): Collection; + orderBy(index: string): Collection; + reverse(): Collection; + mapToClass(constructor: Function): Function; + add(item: T, key?: Key): Promise; + update(key: Key, changes: { [keyPath: string]: any }) : Promise; + put(item: T, key?: Key): Promise; + delete(key: Key): Promise; + clear(): Promise; + } + + interface WhereClause { + above(key: number): Collection; + above(key: string): Collection; + above(key: Date): Collection; + above(key: Array): Collection; + aboveOrEqual(key: number): Collection; + aboveOrEqual(key: string): Collection; + aboveOrEqual(key: Date): Collection; + aboveOrEqual(key: Array): Collection; + anyOf(keys: Array): Collection; + anyOf(...keys: any[]): Collection; + below(key: number): Collection; + below(key: string): Collection; + below(key: Date): Collection; + below(key: Array): Collection; + belowOrEqual(key: number): Collection; + belowOrEqual(key: string): Collection; + belowOrEqual(key: Date): Collection; + belowOrEqual(key: Array): Collection; + between(lower: number, upper: number, includeLower?: boolean, includeUpper?: boolean): Collection; + between(lower: string, upper: string, includeLower?: boolean, includeUpper?: boolean): Collection; + between(lower: Date, upper: Date, includeLower?: boolean, includeUpper?: boolean): Collection; + between(lower: Array, upper: Array, includeLower?: boolean, includeUpper?: boolean): Collection; + equals(key: number): Collection; + equals(key: string): Collection; + equals(key: Date): Collection; + equals(key: Array): Collection; + equalsIgnoreCase(key: string): Collection; + startsWith(key: string): Collection; + startsWithIgnoreCase(key: string): Collection; + } + + interface Collection { + and(filter: (x: T) => boolean): Collection; + count(): Promise; + count(onFulfilled: (value: number) => Thenable): Promise; + count(onFulfilled: (value: number) => U): Promise; + distinct(): Collection; + each(callback: (obj: T, cursor: IDBCursor) => any): Promise; + eachKey(callback: (key: Key, cursor: IDBCursor) => any): Promise; + eachUniqueKey(callback: (key: Key, cursor: IDBCursor) => any): Promise; + first(): Promise; + first(onFulfilled: (value: T) => Thenable): Promise; + first(onFulfilled: (value: T) => U): Promise; + keys(): Promise; + keys(onFulfilled: (value: Key[]) => Thenable): Promise; + keys(onFulfilled: (value: Key[]) => U): Promise; + last(): Promise; + last(onFulfilled: (value: T) => Thenable): Promise; + last(onFulfilled: (value: T) => U): Promise; + limit(n: number): Collection; + offset(n: number): Collection; + or(indexOrPrimayKey: string): WhereClause; + reverse(): Collection; + sortBy(keyPath: string): Promise; + sortBy(keyPath: string, onFulfilled: (value: T[]) => Thenable): Promise; + sortBy(keyPath: string, onFulfilled: (value: T[]) => U): Promise; + toArray(): Promise>; + toArray(onFulfilled: (value: Array) => Thenable): Promise; + toArray(onFulfilled: (value: Array) => U): Promise; + uniqueKeys(): Promise; + uniqueKeys(onFulfilled: (value: Key[]) => Thenable): Promise; + uniqueKeys(onFulfilled: (value: Key[]) => U): Promise; + until(filter: (value: T) => boolean, includeStopEntry?: boolean): Collection; + // WriteableCollection: + delete(): Promise; + modify(changes: { [keyPath: string]: any }): Promise; + modify(changeCallback: (obj: T) => void): Promise; + } + + interface TableSchema { + name: string; + primKey: IndexSpec; + indexes: IndexSpec[]; + mappedClass: Function; + } + + interface IndexSpec { + name: string; + keyPath: any; + unique: boolean; + multi: boolean; + auto: boolean; + compound: boolean; + src: string; + } +} + +declare module 'Dexie' { + export = Dexie; +}