Skip to content

Commit

Permalink
Refactor Model event types
Browse files Browse the repository at this point in the history
  • Loading branch information
NoelDeMartin committed Jan 23, 2024
1 parent beca6f3 commit 18ad2da
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 21 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

### Added

- Augmenting the `ModelEvents` interface it is now possible to extend model events with custom events.

### Deprecated

- `ModelEvent` and `ModelEventValue` have been deprecated in favour of the `ModelEvents` interface.

## [v0.5.2](https://github.com/NoelDeMartin/soukai/releases/tag/v0.5.2) - 2023-12-17

### Added
Expand Down
59 changes: 38 additions & 21 deletions src/models/Model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,27 +36,39 @@ import type { TimestampFieldValue, TimestampsDefinition } from './timestamps';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Key = any;
export type ModelListener<T extends Model = Model> = (model: T) => unknown;
export type ModelListener<
TModel extends Model = Model,
TEvent extends keyof ModelEvents = keyof ModelEvents
> = (...args: ModelListenerArgs<TModel, TEvent>) => unknown;

export type ModelCloneOptions = Partial<{
clean: boolean;
clones: WeakMap<Model, Model>;
constructors: WeakMap<typeof Model, typeof Model> | [typeof Model, typeof Model][];
}>;

export const ModelEvent = {
Created: 'created' as const,
Updated: 'updated' as const,
Deleted: 'deleted' as const,
};

export type ModelCastAttributeOptions = {
field?: string;
definition?: BootedFieldDefinition;
malformedAttributes?: Record<string, string[]>;
};

export type ModelEventValue = typeof ModelEvent[keyof typeof ModelEvent];
export interface ModelEvents {
created: void;
deleted: void;
'relation-loaded': Relation;
updated: void;
}

export type ModelEmitArgs<T extends keyof ModelEvents> =
ModelEvents[T] extends void
? [event: T]
: [event: T, payload: ModelEvents[T]];

export type ModelListenerArgs<TModel extends Model, TEvent extends keyof ModelEvents> =
ModelEvents[TEvent] extends void
? [model: TModel]
: [model: TModel, payload: ModelEvents[TEvent]];

export class Model {

Expand Down Expand Up @@ -254,15 +266,15 @@ export class Model {
return this.instance().newInstance(...params);
}

public static on<T extends Model>(
this: ModelConstructor<T>,
event: ModelEventValue,
listener: ModelListener<T>,
public static on<TModel extends Model, TEvent extends keyof ModelEvents>(
this: ModelConstructor<TModel>,
event: TEvent,
listener: ModelListener<TModel, TEvent>,
): () => void {
if (!this.listeners.has(this))
this.listeners.set(this, {});

const modelListeners = this.listeners.get(this) as Record<string, ModelListener<T>[]>;
const modelListeners = this.listeners.get(this) as Record<string, ModelListener<TModel, TEvent>[]>;
const listeners = modelListeners[event] ??= [];

listeners.push(listener);
Expand Down Expand Up @@ -452,15 +464,18 @@ export class Model {
return extractFinalEngine(this.requireEngine()) as T;
}

public loadRelation<T extends Model | null | Model[] = Model | null | Model[]>(
public async loadRelation<T extends Model | null | Model[] = Model | null | Model[]>(
relation: string,
): Promise<T> {
const relationInstance = this.requireRelation(relation);

return relationInstance.relatedClass.withEngine(
const related = await relationInstance.relatedClass.withEngine(
this.requireEngine(),
() => relationInstance.load(),
) as Promise<T>;
);

this.emit('relation-loaded', relationInstance);

return related as T;
}

public async loadRelationIfUnloaded<T extends Model | null | Model[] = Model | null | Model[]>(
Expand Down Expand Up @@ -725,7 +740,7 @@ export class Model {
}

await this.performDelete();
await this.emit(ModelEvent.Deleted);
await this.emit('deleted');

return this;
}
Expand All @@ -741,7 +756,7 @@ export class Model {
await this.beforeSave();
await this.performSave();
await this.afterSave();
await this.emit(existed ? ModelEvent.Updated : ModelEvent.Created);
await this.emit(existed ? 'updated' : 'created');

return this;
}
Expand Down Expand Up @@ -980,13 +995,15 @@ export class Model {
}
}

protected async emit(event: ModelEventValue): Promise<void> {
protected async emit<T extends keyof ModelEvents>(...args: ModelEmitArgs<T>): Promise<void> {
const event = args[0];
const payload = args[1];
const listeners = this.static().listeners.get(this.static())?.[event];

if (!listeners)
return;

await Promise.all(listeners.map(listener => listener(this)));
await Promise.all(listeners.map(listener => listener(this, payload)));
}

protected async syncDirty(): Promise<string> {
Expand Down
13 changes: 13 additions & 0 deletions src/models/deprecated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* @deprecated Use ModelEvents keys instead.
*/
export const ModelEvent = {
Created: 'created' as const,
Deleted: 'deleted' as const,
Updated: 'updated' as const,
};

/**
* @deprecated Use ModelEvents keys instead.
*/
export type ModelEventValue = typeof ModelEvent[keyof typeof ModelEvent];
1 change: 1 addition & 0 deletions src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { Model } from '@/models/Model';
import type { ModelConstructor } from '@/models/inference';

export { default as ModelKey } from './ModelKey';
export * from './deprecated';
export * from './inference';
export * from './Model';
export * from './relations/index';
Expand Down

0 comments on commit 18ad2da

Please sign in to comment.