rxdb
Version:
A local-first realtime NoSQL Database for JavaScript applications - https://rxdb.info/
163 lines (146 loc) • 5.03 kB
text/typescript
import { BehaviorSubject } from 'rxjs';
import {
INTERNAL_CONTEXT_COLLECTION,
getPrimaryKeyOfInternalDocument
} from '../../rx-database-internal-store.ts';
import { getPreviousVersions } from '../../rx-schema.ts';
import type {
ById,
InternalStoreCollectionDocType,
InternalStoreDocType,
RxCollection,
RxDatabase,
RxDocumentData
} from '../../types/index.d.ts';
import {
PROMISE_RESOLVE_FALSE,
PROMISE_RESOLVE_NULL,
clone,
flatClone,
getFromMapOrCreate,
toPromise
} from '../utils/index.ts';
import { RxMigrationState } from './rx-migration-state.ts';
export async function getOldCollectionMeta(
migrationState: RxMigrationState
): Promise<RxDocumentData<InternalStoreCollectionDocType> | undefined> {
const collectionDocKeys = getPreviousVersions(migrationState.collection.schema.jsonSchema)
.map(version => migrationState.collection.name + '-' + version);
const found = await migrationState.database.internalStore.findDocumentsById(
collectionDocKeys.map(key => getPrimaryKeyOfInternalDocument(
key,
INTERNAL_CONTEXT_COLLECTION
)),
false
);
/**
* It can happen that a previous migration was canceled or the browser was reloaded
* and on the next startup a new migration was added.
* So we can have multiple collection states with different versions.
* In this case, use the one with the lowest version number and start
* migrating from this one upwards.
*/
const foundById: ById<RxDocumentData<InternalStoreDocType>> = {};
found.forEach(f => foundById[f.key] = f);
const oldest = collectionDocKeys.find(key => foundById[key]);
return oldest ? foundById[oldest] : undefined;
}
/**
* runs the doc-data through all following migrationStrategies
* so it will match the newest schema.
* @throws Error if final doc does not match final schema or migrationStrategy crashes
* @return final object or null if migrationStrategy deleted it
*/
export function migrateDocumentData(
collection: RxCollection,
docSchemaVersion: number,
docData: any
): Promise<any | null> {
/**
* We cannot deep-clone Blob or Buffer
* so we just flat clone it here
* and attach it to the deep cloned document data.
*/
const attachmentsBefore = flatClone(docData._attachments);
const mutateableDocData = clone(docData);
const meta = mutateableDocData._meta;
delete mutateableDocData._meta;
mutateableDocData._attachments = attachmentsBefore;
let nextVersion = docSchemaVersion + 1;
// run the document through migrationStrategies
let currentPromise = Promise.resolve(mutateableDocData);
while (nextVersion <= collection.schema.version) {
const version = nextVersion;
currentPromise = currentPromise.then(docOrNull => runStrategyIfNotNull(
collection,
version,
docOrNull
));
nextVersion++;
}
return currentPromise.then(doc => {
if (doc === null) {
return PROMISE_RESOLVE_NULL;
}
if (meta) {
doc._meta = meta;
}
return doc;
});
}
export function runStrategyIfNotNull(
collection: RxCollection,
version: number,
docOrNull: any | null
): Promise<any | null> {
if (docOrNull === null) {
return PROMISE_RESOLVE_NULL;
} else {
const ret = collection.migrationStrategies[version](docOrNull, collection);
const retPromise = toPromise(ret);
return retPromise;
}
}
/**
* returns true if a migration is needed
*/
export async function mustMigrate(
migrationState: RxMigrationState
): Promise<boolean> {
if (migrationState.collection.schema.version === 0) {
return PROMISE_RESOLVE_FALSE;
}
const oldColDoc = await getOldCollectionMeta(migrationState);
return !!oldColDoc;
}
export const MIGRATION_DEFAULT_BATCH_SIZE = 200;
export type MigrationStateWithCollection = {
collection: RxCollection;
migrationState: RxMigrationState;
};
export const DATA_MIGRATION_STATE_SUBJECT_BY_DATABASE = new WeakMap<RxDatabase, BehaviorSubject<RxMigrationState[]>>();
export function addMigrationStateToDatabase(
migrationState: RxMigrationState
) {
const allSubject = getMigrationStateByDatabase(migrationState.database);
const allList = allSubject.getValue().slice(0);
allList.push(migrationState);
allSubject.next(allList);
}
export function getMigrationStateByDatabase(database: RxDatabase): BehaviorSubject<RxMigrationState[]> {
return getFromMapOrCreate(
DATA_MIGRATION_STATE_SUBJECT_BY_DATABASE,
database,
() => new BehaviorSubject<RxMigrationState[]>([])
);
}
/**
* Complete on database close
* so people do not have to unsubscribe
*/
export function onDatabaseClose(database: RxDatabase) {
const subject = DATA_MIGRATION_STATE_SUBJECT_BY_DATABASE.get(database);
if (subject) {
subject.complete();
}
}