rxdb
Version:
A local-first realtime NoSQL Database for JavaScript applications - https://rxdb.info/
319 lines (298 loc) • 12.6 kB
text/typescript
import { filter, mergeMap, tap } from 'rxjs/operators';
import { getPrimaryFieldOfPrimaryKey } from './rx-schema-helper.ts';
import { WrappedRxStorageInstance } from './rx-storage-helper.ts';
import type {
BulkWriteRow,
EventBulk,
RxChangeEvent,
RxDocumentData,
RxDocumentWriteData,
RxJsonSchema,
RxStorage,
RxStorageWriteError,
RxStorageBulkWriteResponse,
RxStorageChangeEvent,
RxStorageInstance,
RxStorageInstanceCreationParams,
RxValidationError,
RxStorageWriteErrorConflict,
MaybePromise
} from './types/index.d.ts';
import {
flatClone,
getFromMapOrCreate,
requestIdleCallbackIfAvailable
} from './plugins/utils/index.ts';
import { BehaviorSubject, firstValueFrom } from 'rxjs';
type WrappedStorageFunction = <Internals, InstanceCreationOptions>(
args: {
storage: RxStorage<Internals, InstanceCreationOptions>;
}
) => RxStorage<Internals, InstanceCreationOptions>;
/**
* Returns the validation errors.
* If document is fully valid, returns an empty array.
*/
type ValidatorFunction = (docData: RxDocumentData<any>) => RxValidationError[];
/**
* cache the validators by the schema string
* so we can reuse them when multiple collections have the same schema
*
* Notice: to make it easier and not dependent on a hash function,
* we use the plain json string.
*/
const VALIDATOR_CACHE_BY_VALIDATOR_KEY: Map<string, Map<string, ValidatorFunction>> = new Map();
/**
* This factory is used in the validation plugins
* so that we can reuse the basic storage wrapping code.
*/
export function wrappedValidateStorageFactory(
/**
* Returns a method that can be used to validate
* documents and throws when the document is not valid.
*/
getValidator: (schema: RxJsonSchema<any>) => ValidatorFunction,
/**
* A string to identify the validation library.
*/
validatorKey: string
): WrappedStorageFunction {
const VALIDATOR_CACHE = getFromMapOrCreate(
VALIDATOR_CACHE_BY_VALIDATOR_KEY,
validatorKey,
() => new Map()
);
function initValidator(
schema: RxJsonSchema<any>
): ValidatorFunction {
return getFromMapOrCreate(
VALIDATOR_CACHE,
JSON.stringify(schema),
() => getValidator(schema)
);
}
return (args) => {
return Object.assign(
{},
args.storage,
{
name: 'validate-' + validatorKey + '-' + args.storage.name,
async createStorageInstance<RxDocType>(
params: RxStorageInstanceCreationParams<RxDocType, any>
) {
const instance = await args.storage.createStorageInstance(params);
const primaryPath = getPrimaryFieldOfPrimaryKey(params.schema.primaryKey);
/**
* Lazy initialize the validator
* to save initial page load performance.
* Some libraries take really long to initialize the validator
* from the schema.
*/
let validatorCached: ValidatorFunction;
requestIdleCallbackIfAvailable(() => validatorCached = initValidator(params.schema));
const oldBulkWrite = instance.bulkWrite.bind(instance);
instance.bulkWrite = (
documentWrites: BulkWriteRow<RxDocType>[],
context: string
) => {
if (!validatorCached) {
validatorCached = initValidator(params.schema);
}
const errors: RxStorageWriteError<RxDocType>[] = [];
const continueWrites: typeof documentWrites = [];
documentWrites.forEach(row => {
const documentId: string = row.document[primaryPath] as any;
const validationErrors = validatorCached(row.document);
if (validationErrors.length > 0) {
errors.push({
status: 422,
isError: true,
documentId,
writeRow: row,
validationErrors,
schema: instance.schema
});
} else {
continueWrites.push(row);
}
});
const writePromise: Promise<RxStorageBulkWriteResponse<RxDocType>> = continueWrites.length > 0 ?
oldBulkWrite(continueWrites, context) :
Promise.resolve({ error: [], success: [] });
return writePromise.then(writeResult => {
errors.forEach(validationError => {
writeResult.error.push(validationError);
});
return writeResult;
});
};
return instance;
}
}
);
};
}
/**
* Used in plugins to easily modify all in- and outgoing
* data of that storage instance.
*/
export function wrapRxStorageInstance<RxDocType>(
originalSchema: RxJsonSchema<RxDocumentData<RxDocType>>,
instance: RxStorageInstance<RxDocType, any, any>,
modifyToStorage: (docData: RxDocumentWriteData<RxDocType>) => MaybePromise<RxDocumentData<any>>,
modifyFromStorage: (docData: RxDocumentData<any>) => MaybePromise<RxDocumentData<RxDocType>>,
modifyAttachmentFromStorage: (attachmentData: string) => MaybePromise<string> = (v) => v
): WrappedRxStorageInstance<RxDocType, any, any> {
async function toStorage(docData: RxDocumentWriteData<RxDocType>): Promise<RxDocumentData<any>> {
if (!docData) {
return docData;
}
return await modifyToStorage(docData);
}
async function fromStorage(docData: RxDocumentData<any> | null): Promise<RxDocumentData<RxDocType>> {
if (!docData) {
return docData;
}
return await modifyFromStorage(docData);
}
async function errorFromStorage(
error: RxStorageWriteError<any>
): Promise<RxStorageWriteError<RxDocType>> {
const ret = flatClone(error);
ret.writeRow = flatClone(ret.writeRow);
if ((ret as RxStorageWriteErrorConflict<any>).documentInDb) {
(ret as RxStorageWriteErrorConflict<any>).documentInDb = await fromStorage((ret as RxStorageWriteErrorConflict<any>).documentInDb);
}
if (ret.writeRow.previous) {
ret.writeRow.previous = await fromStorage(ret.writeRow.previous);
}
ret.writeRow.document = await fromStorage(ret.writeRow.document);
return ret;
}
const processingChangesCount$ = new BehaviorSubject(0);
const wrappedInstance: WrappedRxStorageInstance<RxDocType, any, any> = {
databaseName: instance.databaseName,
internals: instance.internals,
cleanup: instance.cleanup.bind(instance),
options: instance.options,
close: instance.close.bind(instance),
schema: originalSchema,
collectionName: instance.collectionName,
count: instance.count.bind(instance),
remove: instance.remove.bind(instance),
originalStorageInstance: instance,
bulkWrite: async (
documentWrites: BulkWriteRow<RxDocType>[],
context: string
) => {
const useRows: BulkWriteRow<any>[] = [];
await Promise.all(
documentWrites.map(async (row) => {
const [previous, document] = await Promise.all([
row.previous ? toStorage(row.previous) : undefined,
toStorage(row.document)
]);
useRows.push({ previous, document });
})
);
const writeResult = await instance.bulkWrite(useRows, context);
const ret: RxStorageBulkWriteResponse<RxDocType> = {
error: []
};
const promises: Promise<any>[] = [];
writeResult.error.forEach(error => {
promises.push(
errorFromStorage(error).then(err => ret.error.push(err))
);
});
await Promise.all(promises);
/**
* By definition, all change events must be emitted
* BEFORE the write call resolves.
* To ensure that even when the modifiers are async,
* we wait here until the processing queue is empty.
*/
await firstValueFrom(
processingChangesCount$.pipe(
filter(v => v === 0)
)
);
return ret;
},
query: (preparedQuery) => {
return instance.query(preparedQuery)
.then(queryResult => {
return Promise.all(queryResult.documents.map(doc => fromStorage(doc)));
})
.then(documents => ({ documents: documents as any }));
},
getAttachmentData: async (
documentId: string,
attachmentId: string,
digest: string
) => {
let data = await instance.getAttachmentData(documentId, attachmentId, digest);
data = await modifyAttachmentFromStorage(data);
return data;
},
findDocumentsById: (ids, deleted) => {
return instance.findDocumentsById(ids, deleted)
.then(async (findResult) => {
const ret: RxDocumentData<RxDocType>[] = [];
await Promise.all(
findResult
.map(async (doc) => {
ret.push(await fromStorage(doc));
})
);
return ret;
});
},
getChangedDocumentsSince: !instance.getChangedDocumentsSince ? undefined : (limit, checkpoint) => {
return ((instance as any).getChangedDocumentsSince)(limit, checkpoint)
.then(async (result: any) => {
return {
checkpoint: result.checkpoint,
documents: await Promise.all(
result.documents.map((d: any) => fromStorage(d))
)
};
});
},
changeStream: () => {
return instance.changeStream().pipe(
tap(() => processingChangesCount$.next(processingChangesCount$.getValue() + 1)),
mergeMap(async (eventBulk) => {
const useEvents = await Promise.all(
eventBulk.events.map(async (event) => {
const [
documentData,
previousDocumentData
] = await Promise.all([
fromStorage(event.documentData),
fromStorage(event.previousDocumentData)
]);
const ev: RxChangeEvent<RxDocType> = {
operation: event.operation,
documentId: event.documentId,
documentData: documentData as any,
previousDocumentData: previousDocumentData as any,
isLocal: false
};
return ev;
})
);
const ret: EventBulk<RxStorageChangeEvent<RxDocumentData<RxDocType>>, any> = {
id: eventBulk.id,
events: useEvents,
checkpoint: eventBulk.checkpoint,
context: eventBulk.context
};
return ret;
}),
tap(() => processingChangesCount$.next(processingChangesCount$.getValue() - 1))
);
},
};
return wrappedInstance;
}