Created
July 11, 2022 14:28
-
-
Save offlinehacker/f4b506d28f3a2147e2a93b6978f5e91c to your computer and use it in GitHub Desktop.
RxDB plugin to allow storage of attachments in external storage
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
BulkWriteRow, | |
EventBulk, | |
RxAttachmentData, | |
RxAttachmentWriteData, | |
RxDocumentData, | |
RxDocumentDataById, | |
RxJsonSchema, | |
RxStorage, | |
RxStorageBulkWriteResponse, | |
RxStorageChangeEvent, | |
RxStorageInstance, | |
RxStorageInstanceCreationParams, | |
RxStorageQueryResult, | |
RxStorageStatics, | |
} from "rxdb"; | |
import { | |
CompositePrimaryKey, | |
PrimaryKey, | |
StringKeys, | |
} from "rxdb/dist/types/types"; | |
import { Observable } from "rxjs"; | |
export interface RxAttachmentProvider { | |
getDocumentAttachments: ( | |
collectionName: string, | |
documentId: string | |
) => Promise<{ [id: string]: RxAttachmentData }>; | |
getAttachment: ( | |
collectionName: string, | |
documentId: string, | |
attachmentId: string | |
) => Promise<string>; | |
putAttachment: ( | |
collectionName: string, | |
documentId: string, | |
attachmentId: string, | |
attachment: RxAttachmentWriteData | |
) => Promise<void>; | |
} | |
type ExternalAttachmentsInternals<RxDocType> = { | |
storageInstance: RxStorageInstance<RxDocType, any, any>; | |
}; | |
export type ExternalAttachmentsSettings = { | |
storage: RxStorage<any, any>; | |
attachments: RxAttachmentProvider; | |
}; | |
export class RxExternalAttachments | |
implements | |
RxStorage<ExternalAttachmentsInternals<any>, ExternalAttachmentsSettings> | |
{ | |
public name = "external-attachments"; | |
public statics: RxStorageStatics; | |
private storage: RxStorage<any, any>; | |
constructor(public readonly settings: ExternalAttachmentsSettings) { | |
this.statics = settings.storage.statics; | |
this.storage = settings.storage; | |
} | |
async createStorageInstance<RxDocType>( | |
params: RxStorageInstanceCreationParams<RxDocType, any> | |
): Promise<RxStorageInstance<RxDocType, any, any>> { | |
const storageInstance = await this.storage.createStorageInstance(params); | |
const internals: ExternalAttachmentsInternals<any> = { | |
storageInstance, | |
}; | |
return new RxExternalAttachmentsStorageInstance( | |
this, | |
params.databaseName, | |
params.collectionName, | |
params.schema, | |
internals, | |
this.settings | |
); | |
} | |
} | |
export class RxExternalAttachmentsStorageInstance<RxDocType> | |
implements | |
RxStorageInstance<RxDocType, ExternalAttachmentsInternals<RxDocType>, any> | |
{ | |
private primaryKey: Extract<keyof RxDocType, string>; | |
private provider: RxAttachmentProvider; | |
constructor( | |
public readonly storage: RxExternalAttachments, | |
public readonly databaseName: string, | |
public readonly collectionName: string, | |
public readonly schema: Readonly<RxJsonSchema<RxDocumentData<RxDocType>>>, | |
public readonly internals: ExternalAttachmentsInternals<RxDocType>, | |
public readonly options: Readonly<ExternalAttachmentsSettings> | |
) { | |
this.primaryKey = getPrimaryFieldOfPrimaryKey(schema.primaryKey) as any; | |
this.provider = options.attachments; | |
} | |
async _resolveDocumentAttachments( | |
documentId: string, | |
doc: RxDocumentData<RxDocType> | |
): Promise<RxDocumentData<RxDocType>> { | |
const attachments = await this.provider.getDocumentAttachments( | |
this.collectionName, | |
documentId | |
); | |
return { ...doc, _attachments: attachments }; | |
} | |
async _resolveAttachments( | |
docs: RxDocumentDataById<RxDocType> | |
): Promise<RxDocumentDataById<RxDocType>> { | |
if (!this.schema.attachments) return docs; | |
const promises = Object.entries(docs).map(async ([documentId, doc]) => [ | |
documentId, | |
await this._resolveDocumentAttachments(documentId, doc), | |
]); | |
const results = await Promise.all(promises); | |
return Object.fromEntries(results); | |
} | |
async bulkWrite( | |
documentWrites: BulkWriteRow<RxDocType>[] | |
): Promise<RxStorageBulkWriteResponse<RxDocType>> { | |
for (const row of documentWrites) { | |
for (const [attachmentId, attachment] of Object.entries( | |
row.document._attachments | |
)) { | |
if (!("data" in attachment)) continue; | |
await this.provider.putAttachment( | |
this.collectionName, | |
row.document[this.primaryKey] as any, | |
attachmentId, | |
attachment | |
); | |
} | |
} | |
const result = await this.internals.storageInstance.bulkWrite( | |
documentWrites | |
); | |
if (result.success) { | |
const docsWithAttachments = await this._resolveAttachments( | |
result.success | |
); | |
return { ...result, success: docsWithAttachments }; | |
} | |
return result; | |
} | |
async findDocumentsById( | |
ids: string[], | |
withDeleted: boolean | |
): Promise<RxDocumentDataById<RxDocType>> { | |
const docs = await this.internals.storageInstance.findDocumentsById( | |
ids, | |
withDeleted | |
); | |
return await this._resolveAttachments(docs); | |
} | |
async query(preparedQuery: any): Promise<RxStorageQueryResult<RxDocType>> { | |
const results = await this.internals.storageInstance.query(preparedQuery); | |
if (!this.schema.attachments) return results; | |
const documents = await Promise.all( | |
results.documents.map(async (doc) => ({ | |
...doc, | |
_attachments: await this._resolveDocumentAttachments( | |
doc[this.primaryKey] as any, | |
doc | |
), | |
})) | |
); | |
return { documents: documents as any }; | |
} | |
getAttachmentData(documentId: string, attachmentId: string): Promise<string> { | |
return this.provider.getAttachment( | |
this.collectionName, | |
documentId, | |
attachmentId | |
); | |
} | |
async getChangedDocumentsSince( | |
limit: number, | |
checkpoint?: any | |
): Promise<{ document: RxDocumentData<RxDocType>; checkpoint: any }[]> { | |
const results = | |
await this.internals.storageInstance.getChangedDocumentsSince( | |
limit, | |
checkpoint | |
); | |
// await Promise.all( | |
// results.map(async ({ document: doc }) => { | |
// doc._attachments = await this.options.getAttachmentsData(doc); | |
// }) | |
// ); | |
return results; | |
} | |
changeStream(): Observable< | |
EventBulk<RxStorageChangeEvent<RxDocumentData<RxDocType>>> | |
> { | |
return this.internals.storageInstance.changeStream(); | |
} | |
cleanup(minimumDeletedTime: number): Promise<boolean> { | |
return this.internals.storageInstance.cleanup(minimumDeletedTime); | |
} | |
close(): Promise<void> { | |
return this.internals.storageInstance.close(); | |
} | |
remove(): Promise<void> { | |
return this.internals.storageInstance.remove(); | |
} | |
} | |
export function getRxExternalAttachments( | |
settings: ExternalAttachmentsSettings | |
): RxExternalAttachments { | |
return new RxExternalAttachments(settings); | |
} | |
function getPrimaryFieldOfPrimaryKey<RxDocType>( | |
primaryKey: PrimaryKey<RxDocType> | |
): StringKeys<RxDocType> { | |
if (typeof primaryKey === "string") { | |
return primaryKey as any; | |
} else { | |
return (primaryKey as CompositePrimaryKey<RxDocType>).key; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment