The communications platform that puts data protection first.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
Rocket.Chat/app/models/server/raw/BaseRaw.ts

400 lines
12 KiB

import {
Collection,
CollectionInsertOneOptions,
CommonOptions,
Cursor,
DeleteWriteOpResultObject,
FilterQuery,
FindAndModifyWriteOpResultObject,
FindOneAndUpdateOption,
FindOneOptions,
IndexSpecification,
InsertOneWriteOpResult,
InsertWriteOpResult,
ObjectID,
ObjectId,
OptionalId,
UpdateManyOptions,
UpdateOneOptions,
UpdateQuery,
UpdateWriteOpResult,
WithId,
WithoutProjection,
WriteOpResult,
} from 'mongodb';
import { IRocketChatRecord, RocketChatRecordDeleted } from '../../../../definition/IRocketChatRecord';
import { setUpdatedAt } from '../lib/setUpdatedAt';
export { IndexSpecification } from 'mongodb';
// [extracted from @types/mongo] TypeScript Omit (Exclude to be specific) does not work for objects with an "any" indexed type, and breaks discriminated unions
type EnhancedOmit<T, K> = string | number extends keyof T
? T // T has indexed type e.g. { _id: string; [k: string]: any; } or it is "any"
: T extends any
? Pick<T, Exclude<keyof T, K>> // discriminated unions
: never;
// [extracted from @types/mongo]
type ExtractIdType<TSchema> = TSchema extends { _id: infer U } // user has defined a type for _id
? {} extends U
? Exclude<U, {}>
: unknown extends U
? ObjectId
: U
: ObjectId;
export type ModelOptionalId<T> = EnhancedOmit<T, '_id'> & { _id?: ExtractIdType<T> };
// InsertionModel forces both _id and _updatedAt to be optional, regardless of how they are declared in T
export type InsertionModel<T> = EnhancedOmit<ModelOptionalId<T>, '_updatedAt'> & {
_updatedAt?: Date;
};
export interface IBaseRaw<T> {
col: Collection<T>;
}
const baseName = 'rocketchat_';
type DefaultFields<Base> = Record<keyof Base, 1> | Record<keyof Base, 0> | void;
type ResultFields<Base, Defaults> = Defaults extends void
? Base
: Defaults[keyof Defaults] extends 1
? Pick<Defaults, keyof Defaults>
: Omit<Defaults, keyof Defaults>;
const warnFields =
process.env.NODE_ENV !== 'production'
? (...rest: any): void => {
console.warn(...rest, new Error().stack);
}
: new Function();
export class BaseRaw<T, C extends DefaultFields<T> = undefined> implements IBaseRaw<T> {
public readonly defaultFields: C;
protected indexes?: IndexSpecification[];
protected name: string;
private preventSetUpdatedAt: boolean;
public readonly trash?: Collection<RocketChatRecordDeleted<T>>;
constructor(public readonly col: Collection<T>, trash?: Collection<T>, options?: { preventSetUpdatedAt?: boolean }) {
this.name = this.col.collectionName.replace(baseName, '');
this.trash = trash as unknown as Collection<RocketChatRecordDeleted<T>>;
if (this.indexes?.length) {
this.col.createIndexes(this.indexes);
}
this.preventSetUpdatedAt = options?.preventSetUpdatedAt ?? false;
}
private doNotMixInclusionAndExclusionFields(options: FindOneOptions<T> = {}): FindOneOptions<T> {
const optionsDef = this.ensureDefaultFields(options);
if (optionsDef?.projection === undefined) {
return optionsDef;
}
const projection: Record<string, any> = optionsDef?.projection;
const keys = Object.keys(projection);
const removeKeys = keys.filter((key) => projection[key] === 0);
if (keys.length > removeKeys.length) {
removeKeys.forEach((key) => delete projection[key]);
}
return {
...optionsDef,
projection,
};
}
private ensureDefaultFields(options?: undefined): C extends void ? undefined : WithoutProjection<FindOneOptions<T>>;
private ensureDefaultFields(options: WithoutProjection<FindOneOptions<T>>): WithoutProjection<FindOneOptions<T>>;
private ensureDefaultFields<P>(options: FindOneOptions<P>): FindOneOptions<P>;
private ensureDefaultFields<P>(options?: any): FindOneOptions<P> | undefined | WithoutProjection<FindOneOptions<T>> {
if (this.defaultFields === undefined) {
return options;
}
const { fields: deprecatedFields, projection, ...rest } = options || {};
if (deprecatedFields) {
warnFields("Using 'fields' in models is deprecated.", options);
}
const fields = { ...deprecatedFields, ...projection };
return {
projection: this.defaultFields,
...(fields && Object.values(fields).length && { projection: fields }),
...rest,
};
}
public findOneAndUpdate(
query: FilterQuery<T>,
update: UpdateQuery<T> | T,
options?: FindOneAndUpdateOption<T>,
): Promise<FindAndModifyWriteOpResultObject<T>> {
return this.col.findOneAndUpdate(query, update, options);
}
async findOneById(_id: string, options?: WithoutProjection<FindOneOptions<T>> | undefined): Promise<T | null>;
async findOneById<P>(_id: string, options: FindOneOptions<P extends T ? T : P>): Promise<P | null>;
async findOneById<P>(_id: string, options?: any): Promise<T | P | null> {
const query = { _id } as FilterQuery<T>;
const optionsDef = this.doNotMixInclusionAndExclusionFields(options);
return this.col.findOne(query, optionsDef);
}
async findOne(query?: FilterQuery<T> | string, options?: undefined): Promise<T | null>;
async findOne(query: FilterQuery<T> | string, options: WithoutProjection<FindOneOptions<T>>): Promise<T | null>;
async findOne<P>(query: FilterQuery<T> | string, options: FindOneOptions<P extends T ? T : P>): Promise<P | null>;
async findOne<P>(query: FilterQuery<T> | string = {}, options?: any): Promise<T | P | null> {
const q = typeof query === 'string' ? ({ _id: query } as FilterQuery<T>) : query;
const optionsDef = this.doNotMixInclusionAndExclusionFields(options);
return this.col.findOne(q, optionsDef);
}
// findUsersInRoles(): void {
// throw new Error('[overwrite-function] You must overwrite this function in the extended classes');
// }
find(query?: FilterQuery<T>): Cursor<ResultFields<T, C>>;
find(query: FilterQuery<T>, options: WithoutProjection<FindOneOptions<T>>): Cursor<ResultFields<T, C>>;
find<P = T>(query: FilterQuery<T>, options: FindOneOptions<P extends T ? T : P>): Cursor<P>;
find<P>(query: FilterQuery<T> | undefined = {}, options?: any): Cursor<P> | Cursor<T> {
const optionsDef = this.doNotMixInclusionAndExclusionFields(options);
return this.col.find(query, optionsDef);
}
update(
filter: FilterQuery<T>,
update: UpdateQuery<T> | Partial<T>,
options?: UpdateOneOptions & { multi?: boolean },
): Promise<WriteOpResult> {
this.setUpdatedAt(update);
return this.col.update(filter, update, options);
}
updateOne(
filter: FilterQuery<T>,
update: UpdateQuery<T> | Partial<T>,
options?: UpdateOneOptions & { multi?: boolean },
): Promise<UpdateWriteOpResult> {
this.setUpdatedAt(update);
return this.col.updateOne(filter, update, options);
}
updateMany(filter: FilterQuery<T>, update: UpdateQuery<T> | Partial<T>, options?: UpdateManyOptions): Promise<UpdateWriteOpResult> {
this.setUpdatedAt(update);
return this.col.updateMany(filter, update, options);
}
insertMany(docs: Array<InsertionModel<T>>, options?: CollectionInsertOneOptions): Promise<InsertWriteOpResult<WithId<T>>> {
docs = docs.map((doc) => {
if (!doc._id || typeof doc._id !== 'string') {
const oid = new ObjectID();
return { _id: oid.toHexString(), ...doc };
}
this.setUpdatedAt(doc);
return doc;
});
// TODO reavaluate following type casting
return this.col.insertMany(docs as unknown as Array<OptionalId<T>>, options);
}
insertOne(doc: InsertionModel<T>, options?: CollectionInsertOneOptions): Promise<InsertOneWriteOpResult<WithId<T>>> {
if (!doc._id || typeof doc._id !== 'string') {
const oid = new ObjectID();
doc = { _id: oid.toHexString(), ...doc };
}
this.setUpdatedAt(doc);
// TODO reavaluate following type casting
return this.col.insertOne(doc as unknown as OptionalId<T>, options);
}
removeById(_id: string): Promise<DeleteWriteOpResultObject> {
return this.deleteOne({ _id } as FilterQuery<T>);
}
async deleteOne(
filter: FilterQuery<T>,
options?: CommonOptions & { bypassDocumentValidation?: boolean },
): Promise<DeleteWriteOpResultObject> {
if (!this.trash) {
return this.col.deleteOne(filter, options);
}
const doc = (await this.findOne(filter)) as unknown as (IRocketChatRecord & T) | undefined;
if (doc) {
const { _id, ...record } = doc;
const trash = {
...record,
_deletedAt: new Date(),
__collection__: this.name,
} as RocketChatRecordDeleted<T>;
// since the operation is not atomic, we need to make sure that the record is not already deleted/inserted
await this.trash?.updateOne(
{ _id } as FilterQuery<RocketChatRecordDeleted<T>>,
{ $set: trash },
{
upsert: true,
},
);
}
return this.col.deleteOne(filter, options);
}
async deleteMany(filter: FilterQuery<T>, options?: CommonOptions): Promise<DeleteWriteOpResultObject> {
if (!this.trash) {
return this.col.deleteMany(filter, options);
}
const cursor = this.find(filter);
const ids: string[] = [];
for await (const doc of cursor) {
const { _id, ...record } = doc as unknown as IRocketChatRecord & T;
const trash = {
...record,
_deletedAt: new Date(),
__collection__: this.name,
} as RocketChatRecordDeleted<T>;
ids.push(_id);
// since the operation is not atomic, we need to make sure that the record is not already deleted/inserted
await this.trash?.updateOne(
{ _id } as FilterQuery<RocketChatRecordDeleted<T>>,
{ $set: trash },
{
upsert: true,
},
);
}
return this.col.deleteMany({ _id: { $in: ids } } as unknown as FilterQuery<T>, options);
}
// Trash
trashFind<P extends RocketChatRecordDeleted<T>>(
query: FilterQuery<RocketChatRecordDeleted<T>>,
options: FindOneOptions<P extends RocketChatRecordDeleted<T> ? RocketChatRecordDeleted<T> : P>,
): Cursor<RocketChatRecordDeleted<P>> | undefined {
if (!this.trash) {
return undefined;
}
const { trash } = this;
return trash.find(
{
__collection__: this.name,
...query,
},
options,
);
}
trashFindOneById(_id: string): Promise<RocketChatRecordDeleted<T> | null>;
trashFindOneById(
_id: string,
options: WithoutProjection<RocketChatRecordDeleted<T>>,
): Promise<RocketChatRecordDeleted<RocketChatRecordDeleted<T>> | null>;
trashFindOneById<P>(
_id: string,
options: FindOneOptions<P extends RocketChatRecordDeleted<T> ? RocketChatRecordDeleted<T> : P>,
): Promise<P | null>;
async trashFindOneById<P extends RocketChatRecordDeleted<T>>(
_id: string,
options?:
| undefined
| WithoutProjection<RocketChatRecordDeleted<T>>
| FindOneOptions<P extends RocketChatRecordDeleted<T> ? RocketChatRecordDeleted<T> : P>,
): Promise<RocketChatRecordDeleted<P> | null> {
const query = {
_id,
__collection__: this.name,
} as FilterQuery<RocketChatRecordDeleted<T>>;
if (!this.trash) {
return null;
}
const { trash } = this;
return trash.findOne(query, options);
}
private setUpdatedAt(record: UpdateQuery<T> | InsertionModel<T>): void {
if (this.preventSetUpdatedAt) {
return;
}
setUpdatedAt(record);
}
trashFindDeletedAfter(deletedAt: Date): Cursor<RocketChatRecordDeleted<T>>;
trashFindDeletedAfter(
deletedAt: Date,
query: FilterQuery<RocketChatRecordDeleted<T>>,
options: WithoutProjection<RocketChatRecordDeleted<T>>,
): Cursor<RocketChatRecordDeleted<T>>;
trashFindDeletedAfter<P = RocketChatRecordDeleted<T>>(
deletedAt: Date,
query: FilterQuery<P>,
options: FindOneOptions<P extends RocketChatRecordDeleted<T> ? RocketChatRecordDeleted<T> : P>,
): Cursor<RocketChatRecordDeleted<P>>;
trashFindDeletedAfter<P = RocketChatRecordDeleted<T>>(
deletedAt: Date,
query?: FilterQuery<RocketChatRecordDeleted<T>>,
options?:
| WithoutProjection<RocketChatRecordDeleted<T>>
| FindOneOptions<P extends RocketChatRecordDeleted<T> ? RocketChatRecordDeleted<T> : P>,
): Cursor<RocketChatRecordDeleted<T>> {
const q = {
__collection__: this.name,
_deletedAt: {
$gt: deletedAt,
},
...query,
} as FilterQuery<RocketChatRecordDeleted<T>>;
const { trash } = this;
if (!trash) {
throw new Error('Trash is not enabled for this collection');
}
return trash.find(q, options as any);
}
}