@unchainedshop/core-events
Version:
This package defines the event module which ensures the emitted events are written to the database as an Event History and offers a core module to search for events.
157 lines (140 loc) • 4.46 kB
text/typescript
import {
mongodb,
generateDbFilterById,
buildSortOptions,
generateDbObjectId,
ModuleInput,
assertDocumentDBCompatMode,
} from '@unchainedshop/mongodb';
import { getRegisteredEvents } from '@unchainedshop/events';
import { SortDirection, SortOption, DateFilterInput } from '@unchainedshop/utils';
import { EventsCollection, Event } from '../db/EventsCollection.js';
import { configureEventHistoryAdapter } from './configureEventHistoryAdapter.js';
export interface EventReport {
emitCount: number;
type: string;
}
export interface EventQuery {
types?: string[];
queryString?: string;
created?: Date;
}
export const buildFindSelector = ({ types, queryString, created }: EventQuery) => {
const selector: { type?: any; $text?: any; created?: any } = {};
if (types && Array.isArray(types)) selector.type = { $in: types };
if (queryString) {
assertDocumentDBCompatMode();
selector.$text = { $search: queryString };
}
if (created) selector.created = { $gte: created };
return selector;
};
export const configureEventsModule = async ({ db }: ModuleInput<Record<string, never>>) => {
const Events = await EventsCollection(db);
const create = async (doc: Event) => {
const result = await Events.insertOne({
_id: generateDbObjectId(),
created: new Date(),
...doc,
});
return result.insertedId;
};
await configureEventHistoryAdapter(create);
return {
create,
findEvent: async (
{ eventId, ...rest }: mongodb.Filter<Event> & { eventId: string },
options?: mongodb.FindOptions,
): Promise<Event> => {
const selector = eventId ? generateDbFilterById<Event>(eventId) : rest;
return Events.findOne(selector, options);
},
findEvents: async ({
limit,
offset,
sort,
...query
}: EventQuery & {
limit?: number;
offset?: number;
sort?: SortOption[];
}): Promise<Event[]> => {
const defaultSort = [{ key: 'created', value: SortDirection.DESC }] as SortOption[];
return Events.find(buildFindSelector(query), {
skip: offset,
limit,
sort: buildSortOptions(sort || defaultSort),
}).toArray();
},
type: (event: Event) => {
if (getRegisteredEvents().includes(event.type)) {
return event.type;
}
return 'UNKNOWN';
},
count: async (query: EventQuery) => {
const count = await Events.countDocuments(buildFindSelector(query));
return count;
},
getReport: async (
{ dateRange, types }: { dateRange?: DateFilterInput; types?: string[] } = {
dateRange: {},
types: null,
},
): Promise<EventReport[]> => {
const pipeline = [];
const matchConditions = [];
// build date filter based on provided values it can be a range if both to and from is supplied
// a upper or lowe limit if either from or to is provided
// or all if none is provided
if (dateRange?.start || dateRange?.end) {
const dateConditions = [];
if (dateRange?.start) {
const fromDate = new Date(dateRange?.start);
dateConditions.push({
$or: [{ created: { $gte: fromDate } }, { updated: { $gte: fromDate } }],
});
}
if (dateRange?.end) {
const toDate = new Date(dateRange?.end);
dateConditions.push({
$or: [{ created: { $lte: toDate } }, { updated: { $lte: toDate } }],
});
}
if (dateConditions.length > 0) {
matchConditions.push({ $and: dateConditions });
}
}
// build types filter if type is provided or ignore types if it is not provided
if (types && Array.isArray(types) && types.length) {
matchConditions.push({ type: { $in: types } });
}
if (matchConditions.length > 0) {
pipeline.push({
$match: {
$and: matchConditions,
},
});
}
pipeline.push(
...[
{
$group: {
_id: '$type',
emitCount: { $sum: 1 },
},
},
{
$project: {
_id: 0,
type: '$_id',
emitCount: 1,
},
},
],
);
return Events.aggregate(pipeline).toArray() as Promise<EventReport[]>;
},
};
};
export type EventsModule = Awaited<ReturnType<typeof configureEventsModule>>;