@getanthill/datastore
Version:
Event-Sourced Datastore
238 lines (189 loc) • 5.96 kB
text/typescript
import type { ModelInstance, Services } from '../../typings';
import type { Request } from 'express';
import omit from 'lodash/omit';
import { getDate } from '../../utils';
const LAST_FRAGMENT_REGEXP = /\/[^/]+$/;
export async function publish(services: Services, topic: string, event: any) {
if (services.config.features.mqtt.isEnabled === true) {
await services.mqtt.publish(topic, event);
// Required to listen on deterministic event names:
services.mqtt.emit(topic.replace(LAST_FRAGMENT_REGEXP, ''), event);
}
if (services.config.features.amqp.isEnabled === true) {
await services.amqp.publish(topic, event);
// Required to listen on deterministic event names:
services.amqp.emit(topic.replace(LAST_FRAGMENT_REGEXP, ''), event);
}
}
export async function publishEntityUpdates(
services: Services,
modelName: string,
eventName: string,
entity: ModelInstance,
) {
const topic =
`${modelName}/${eventName}/success/${entity.correlationId}`.toLowerCase();
await Promise.all([
publish(services, topic, entity.state),
...entity.latestHandledEvents.map((e) =>
publish(
services,
`${modelName}/${e.type}/events/${entity.correlationId}`.toLowerCase(),
e,
),
),
]);
}
export async function created(
services: Services,
req: Partial<Request>,
event: any,
) {
const entity = services.models.factory(req.params!.model);
const body = event;
if (req.headers?.['created-at']) {
body.created_at = getDate(req.headers!['created-at'] as string);
}
await entity.create(body);
services.telemetry.logger.debug('[events#created] Entity created', {
entity: entity.state,
});
await publishEntityUpdates(services, req.params!.model, 'created', entity);
return entity;
}
export async function updated(
services: Services,
req: Partial<Request>,
event: any,
) {
const entity = services.models.factory(
req.params!.model,
req.params!.correlation_id,
);
const upsert: boolean = req.headers?.['upsert'] === 'true';
const imperativeVersion: number | undefined =
req.headers?.['version'] !== undefined
? parseInt(req.headers?.['version'] as string, 10)
: undefined;
const body = event;
if (req.headers?.['created-at']) {
body.created_at = getDate(req.headers!['created-at'] as string);
}
if (upsert === true) {
await entity.upsert(body, { imperativeVersion });
} else {
await entity.update(body, { imperativeVersion });
}
services.telemetry.logger.debug('[api/models#update] Entity updated', {
model: req.params!.model,
entity: entity.state,
});
await publishEntityUpdates(services, req.params!.model, 'updated', entity);
return entity;
}
export async function patched(
services: Services,
req: Partial<Request>,
event: any,
) {
const entity = services.models.factory(
req.params!.model,
req.params!.correlation_id,
);
const imperativeVersion: number | undefined = req.headers?.['version']
? parseInt(req.headers?.['version'] as string, 10)
: undefined;
const body = event;
if (req.headers?.['created-at']) {
body.created_at = getDate(req.headers['created-at'] as string);
}
await entity.patch(body, { imperativeVersion });
services.telemetry.logger.debug('[api/models#update] Entity patched', {
model: req.params!.model,
});
await publishEntityUpdates(services, req.params!.model, 'patched', entity);
return entity;
}
export async function applied(
services: Services,
req: Partial<Request>,
event: any,
) {
const entity = services.models.factory(
req.params!.model,
req.params!.correlation_id,
);
const imperativeVersion: number | undefined =
req.headers!['version'] !== undefined
? parseInt(req.headers!['version'] as string, 10)
: undefined;
const isReplay: boolean = req.headers!['replay'] === 'true';
const retryDuration: number | undefined =
req.headers!['retry-duration'] !== undefined
? parseInt(req.headers!['retry-duration'] as string, 10)
: undefined;
let body = event;
if (req.headers!['created-at']) {
body.created_at = getDate(req.headers!['created-at'] as string);
}
if (isReplay === true) {
const Model = services.models.getModel(req.params!.model);
const modelConfig = Model.getModelConfig();
const correlationField: string = modelConfig.correlation_field;
const foundEvent = await Model.getEventsCollection(
Model.db(services.mongodb),
).findOne({
[correlationField]: req.params!.correlation_id,
type: body.type,
created_at: {
$gte: body.created_at,
$lte: body.created_at,
},
});
if (!!foundEvent === true) {
entity.state = await entity.getStateAtVersion(foundEvent.version);
return entity;
}
const originalPayload = omit(body, 'type', 'v', '_id', 'version');
body = await Model.decrypt(originalPayload);
}
await entity.apply(
req.params!.event_type.toUpperCase(),
body,
{
imperativeVersion,
retryDuration,
},
(req.headers!['event-version'] as string) || req.params!.event_version,
);
if (isReplay === false) {
await publishEntityUpdates(
services,
req.params!.model,
req.params!.event_type,
entity,
);
}
return entity;
}
export async function restored(
services: Services,
req: Request & { locals: any },
event: any,
) {
const entity = services.models.factory(
req.params.model,
req.params.correlation_id,
);
req.locals = req.locals || {};
req.locals.model = entity;
const version = parseInt(req.params.version, 10);
await entity.getState();
if (entity.state === null) {
throw new Error('Not Found');
}
req.locals.currentVersion = entity.state.version;
await entity.restore(version);
await publishEntityUpdates(services, req.params.model, 'restored', entity);
return entity;
}