Skip to content

Commit

Permalink
hasChanges and throw error if no changes
Browse files Browse the repository at this point in the history
  • Loading branch information
ggazzo committed Aug 2, 2024
1 parent f445f9d commit cb9ee54
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 53 deletions.
1 change: 1 addition & 0 deletions packages/model-typings/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,4 @@ export * from './models/IAuditLogModel';
export * from './models/ICronHistoryModel';
export * from './models/IMigrationsModel';
export * from './models/IModerationReportsModel';
export * from './updater';
21 changes: 13 additions & 8 deletions packages/model-typings/src/updater.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
/* eslint-disable @typescript-eslint/naming-convention */
import type { Join, NestedPaths, PropertyType, ArrayElement, NestedPathsOfType } from 'mongodb';
import type { Join, NestedPaths, PropertyType, ArrayElement, NestedPathsOfType, Filter } from 'mongodb';

export interface Updater<T extends { _id: string }> {
set<P extends SetProps<T>, K extends keyof P>(key: K, value: P[K]): Updater<T>;
unset<K extends keyof UnsetProps<T>>(key: K): Updater<T>;
// inc<K extends keyof T>(key: K, value: number): Updater<T>;
// dec<K extends keyof T>(key: K, value: number): Updater<T>;
persist(): Promise<void>;
inc<K extends keyof IncProps<T>>(key: K, value: number): Updater<T>;
addToSet<K extends keyof AddToSetProps<T>>(key: K, value: AddToSetProps<T>[K]): Updater<T>;
persist(query: Filter<T>): Promise<void>;
hasChanges(): boolean;
}

type SetProps<TSchema extends { _id: string }> = Readonly<
export type SetProps<TSchema extends { _id: string }> = Readonly<
{
[Property in Join<NestedPaths<TSchema, []>, '.'>]: PropertyType<TSchema, Property>;
} & {
Expand All @@ -21,11 +22,15 @@ type SetProps<TSchema extends { _id: string }> = Readonly<
}
>;

type GetNullables<T> = {
[Key in keyof T]: undefined extends T[Key] ? 1 : never;
type GetType<T, K> = {
[Key in keyof T]: K extends T[Key] ? T[Key] : never;
};

type OmitNever<T> = { [K in keyof T as T[K] extends never ? never : K]: T[K] };

// only allow optional properties
type UnsetProps<TSchema extends { _id: string }> = OmitNever<GetNullables<SetProps<TSchema>>>;
export type UnsetProps<TSchema extends { _id: string }> = OmitNever<GetType<SetProps<TSchema>, undefined>>;

export type IncProps<TSchema extends { _id: string }> = OmitNever<GetType<SetProps<TSchema>, number>>;

export type AddToSetProps<TSchema extends { _id: string }> = OmitNever<GetType<SetProps<TSchema>, any[]>>;
81 changes: 36 additions & 45 deletions packages/models/src/updater.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,9 @@
/* eslint-disable @typescript-eslint/naming-convention */
import type { IBaseModel } from '@rocket.chat/model-typings';
import type { UpdateFilter, Join, NestedPaths, PropertyType, ArrayElement, NestedPathsOfType, Filter } from 'mongodb';
import type { IBaseModel, Updater, SetProps, UnsetProps, IncProps, AddToSetProps } from '@rocket.chat/model-typings';
import type { UpdateFilter, Filter } from 'mongodb';

type ArrayElementType<T> = T extends (infer E)[] ? E : T;

export interface Updater<T extends { _id: string }> {
set<P extends SetProps<T>, K extends keyof P>(key: K, value: P[K]): Updater<T>;
unset<K extends keyof UnsetProps<T>>(key: K): Updater<T>;
inc<K extends keyof IncProps<T>>(key: K, value: number): Updater<T>;
addToSet<K extends keyof AddToSetProps<T>>(key: K, value: AddToSetProps<T>[K]): Updater<T>;
persist(query: Filter<T>): Promise<void>;
}

type SetProps<TSchema extends { _id: string }> = Readonly<
{
[Property in Join<NestedPaths<TSchema, []>, '.'>]: PropertyType<TSchema, Property>;
} & {
[Property in `${NestedPathsOfType<TSchema, any[]>}.$${`[${string}]` | ''}`]: ArrayElement<
PropertyType<TSchema, Property extends `${infer Key}.$${string}` ? Key : never>
>;
} & {
[Property in `${NestedPathsOfType<TSchema, Record<string, any>[]>}.$${`[${string}]` | ''}.${string}`]: any;
}
>;

type GetType<T, K> = {
[Key in keyof T]: K extends T[Key] ? T[Key] : never;
};

type OmitNever<T> = { [K in keyof T as T[K] extends never ? never : K]: T[K] };

// only allow optional properties
type UnsetProps<TSchema extends { _id: string }> = OmitNever<GetType<SetProps<TSchema>, undefined>>;

type IncProps<TSchema extends { _id: string }> = OmitNever<GetType<SetProps<TSchema>, number>>;

type AddToSetProps<TSchema extends { _id: string }> = OmitNever<GetType<SetProps<TSchema>, any[]>>;

type Keys<T extends { _id: string }> = keyof SetProps<T>;

export class UpdaterImpl<T extends { _id: string }> implements Updater<T> {
Expand Down Expand Up @@ -89,24 +56,48 @@ export class UpdaterImpl<T extends { _id: string }> implements Updater<T> {
throw new Error('Updater is not dirty');
}

const update = {
...(this._set && { $set: Object.fromEntries(this._set) }),
...(this._unset && { $unset: Object.fromEntries([...this._unset.values()].map((k) => [k, 1])) }),
...(this._inc && { $inc: Object.fromEntries(this._inc) }),
...(this._addToSet && { $addToSet: { $each: Object.fromEntries(this._addToSet) } }),
} as unknown as UpdateFilter<T>;

if ((process.env.NODE_ENV === 'development' || process.env.TEST_MODE) && Object.keys(update).length === 0) {
if ((process.env.NODE_ENV === 'development' || process.env.TEST_MODE) && !this.hasChanges()) {
throw new Error('Nothing to update');
}

this.dirty = true;

try {
await this.model.updateOne(query, update);
await this.model.updateOne(query, {
...(this._set && { $set: Object.fromEntries(this._set) }),
...(this._unset && { $unset: Object.fromEntries([...this._unset.values()].map((k) => [k, 1])) }),
...(this._inc && { $inc: Object.fromEntries(this._inc) }),
...(this._addToSet && { $addToSet: { $each: Object.fromEntries(this._addToSet) } }),
} as unknown as UpdateFilter<T>);
} catch (error) {
console.error('Failed to update', JSON.stringify(query), JSON.stringify(update, null, 2));
console.error(
'Failed to update',
JSON.stringify(query),
JSON.stringify(
{
...(this._set && { $set: Object.fromEntries(this._set) }),
...(this._unset && { $unset: Object.fromEntries([...this._unset.values()].map((k) => [k, 1])) }),
...(this._inc && { $inc: Object.fromEntries(this._inc) }),
...(this._addToSet && { $addToSet: { $each: Object.fromEntries(this._addToSet) } }),
},
null,
2,
),
);
throw error;
}
}

hasChanges() {
const update = {
...(this._set && { $set: Object.fromEntries(this._set) }),
...(this._unset && { $unset: Object.fromEntries([...this._unset.values()].map((k) => [k, 1])) }),
...(this._inc && { $inc: Object.fromEntries(this._inc) }),
...(this._addToSet && { $addToSet: { $each: Object.fromEntries(this._addToSet) } }),
} as unknown as UpdateFilter<T>;

return Object.keys(update).length > 0;
}
}

export { Updater };

0 comments on commit cb9ee54

Please sign in to comment.