const isFunction = require('lodash/isFunction'); const isObject = require('lodash/isObject'); const isString = require('lodash/isString'); const { runMigrations } = require('./run_migrations'); // IMPORTANT: The migrations below are run on a database that may be very large // due to attachments being directly stored inside the database. Please avoid // any expensive operations, e.g. modifying all messages / attachments, etc., as // it may cause out-of-memory errors for users with long histories: // https://github.com/signalapp/Signal-Desktop/issues/2163 const migrations = [ { version: '12.0', migrate(transaction, next) { console.log('Migration 12'); console.log('creating object stores'); const messages = transaction.db.createObjectStore('messages'); messages.createIndex('conversation', ['conversationId', 'received_at'], { unique: false, }); messages.createIndex('receipt', 'sent_at', { unique: false }); messages.createIndex('unread', ['conversationId', 'unread'], { unique: false }); messages.createIndex('expires_at', 'expires_at', { unique: false }); const conversations = transaction.db.createObjectStore('conversations'); conversations.createIndex('inbox', 'active_at', { unique: false }); conversations.createIndex('group', 'members', { unique: false, multiEntry: true, }); conversations.createIndex('type', 'type', { unique: false, }); conversations.createIndex('search', 'tokens', { unique: false, multiEntry: true, }); transaction.db.createObjectStore('groups'); transaction.db.createObjectStore('sessions'); transaction.db.createObjectStore('identityKeys'); transaction.db.createObjectStore('preKeys'); transaction.db.createObjectStore('signedPreKeys'); transaction.db.createObjectStore('items'); console.log('creating debug log'); transaction.db.createObjectStore('debug'); next(); }, }, { version: '13.0', migrate(transaction, next) { console.log('Migration 13'); console.log('Adding fields to identity keys'); const identityKeys = transaction.objectStore('identityKeys'); const request = identityKeys.openCursor(); const promises = []; request.onsuccess = (event) => { const cursor = event.target.result; if (cursor) { const attributes = cursor.value; attributes.timestamp = 0; attributes.firstUse = false; attributes.nonblockingApproval = false; attributes.verified = 0; promises.push(new Promise(((resolve, reject) => { const putRequest = identityKeys.put(attributes, attributes.id); putRequest.onsuccess = resolve; putRequest.onerror = (e) => { console.log(e); reject(e); }; }))); cursor.continue(); } else { // no more results // eslint-disable-next-line more/no-then Promise.all(promises).then(() => { next(); }); } }; request.onerror = (event) => { console.log(event); }; }, }, { version: '14.0', migrate(transaction, next) { console.log('Migration 14'); console.log('Adding unprocessed message store'); const unprocessed = transaction.db.createObjectStore('unprocessed'); unprocessed.createIndex('received', 'timestamp', { unique: false }); next(); }, }, { version: '15.0', migrate(transaction, next) { console.log('Migration 15'); console.log('Adding messages index for de-duplication'); const messages = transaction.objectStore('messages'); messages.createIndex('unique', ['source', 'sourceDevice', 'sent_at'], { unique: true, }); next(); }, }, { version: '16.0', migrate(transaction, next) { console.log('Migration 16'); console.log('Dropping log table, since we now log to disk'); transaction.db.deleteObjectStore('debug'); next(); }, }, { version: 17, async migrate(transaction, next) { console.log('Migration 17'); const start = Date.now(); const messagesStore = transaction.objectStore('messages'); console.log('Create index from attachment schema version to attachment'); messagesStore.createIndex('schemaVersion', 'schemaVersion', { unique: false }); const duration = Date.now() - start; console.log( 'Complete migration to database version 17.', `Duration: ${duration}ms` ); next(); }, }, ]; const database = { id: 'signal', nolog: true, migrations, }; exports.run = ({ Backbone } = {}) => runMigrations({ Backbone, database }); exports.createCollection = ({ Backbone, storeName }) => { if (!isObject(Backbone) || !isObject(Backbone.Collection) || !isFunction(Backbone.Collection.extend)) { throw new TypeError('"Backbone" is required'); } if (!isString(storeName)) { throw new TypeError('"database" is required'); } const collection = new (Backbone.Collection.extend({ database, storeName, }))(); return collection; };