@getanthill/datastore
Version:
Event-Sourced Datastore
202 lines (163 loc) • 4.5 kB
text/typescript
import type { JSONSchemaType } from 'ajv';
import type { AnyObject } from '../typings';
import assert from 'node:assert';
import crypto from 'node:crypto';
import util from 'node:util';
import _ from 'lodash';
import Ajv from 'ajv';
export function unique(value: any, index: any, self: any) {
return self.indexOf(value) === index;
}
export function random(): number {
return crypto.getRandomValues(new Uint8Array(1))[0] / 256;
}
export function getDate(str?: string | number | Date) {
return !!str || str === 0 ? new Date(str) : new Date();
}
export function getDateNow() {
return Date.now();
}
export function mergeWithReplacedArrays(
_srcValue: AnyObject,
objValue: AnyObject,
) {
return Array.isArray(objValue) ? objValue : undefined;
}
export function mapValuesDeep(
obj: AnyObject,
handler: (obj: AnyObject, key: string) => AnyObject = (v) => v,
) {
Object.keys(obj).forEach((key) => {
obj[key] = handler(obj[key], key);
if (typeof obj[key] === 'object' && obj[key] !== null) {
obj[key] = mapValuesDeep(obj[key], handler);
}
});
return obj;
}
export function deepCoerceProp(
obj: AnyObject,
schema: JSONSchemaType<AnyObject>,
fn: (v: AnyObject, s: JSONSchemaType<AnyObject>, p: string[]) => AnyObject,
path: string[],
name: string,
) {
const prop = obj[name];
const _path = [...path, name];
const _schema = _.get(schema.properties, name, schema);
const { type, format } = _schema;
if (type === 'array') {
obj[name] = deepCoerce(prop, _schema.items || {}, fn, _path);
return;
}
if (typeof prop === 'object' && prop !== null) {
obj[name] = deepCoerce(prop, _schema, fn, _path);
return;
}
if (type === 'integer') {
const _value = Number.parseInt(obj[name], 10);
if (isNaN(_value) === false) {
obj[name] = _value;
}
return;
}
if (type === 'number') {
const _value = parseFloat(obj[name]);
if (isNaN(_value) === false) {
obj[name] = _value;
}
return;
}
if (type === 'boolean') {
obj[name] = Boolean(JSON.parse(obj[name]));
return;
}
if (
(format === 'date' || format === 'date-time') &&
(typeof obj[name] === 'string' || typeof obj[name] === 'number')
) {
obj[name] = new Date(obj[name]);
return;
}
obj[name] = fn(prop, _schema, _path);
}
/**
* Deep coerce object against its JSON Schema
*
* @param {object} obj The object to freeze
* @returns {object} The freezed object
*/
export function deepCoerce(
obj: AnyObject,
schema: JSONSchemaType<AnyObject>,
fn: (v: AnyObject, s: JSONSchemaType<AnyObject>, p: string[]) => AnyObject = (
v,
) => v,
path: string[] = [],
) {
if (Object.isFrozen(obj)) {
return obj;
}
let propNames = Object.getOwnPropertyNames(obj);
if (Array.isArray(obj)) {
propNames = propNames.filter((p) => p !== 'length');
}
for (const name of propNames) {
deepCoerceProp(obj, schema, fn, path, name);
}
return obj;
}
export function processExplanationPlan(raw: any) {
const winningPlan = _.get(raw, 'queryPlanner.winningPlan');
const executionStats = _.get(raw, 'executionStats');
return {
stage: winningPlan?.inputStage?.stage ?? winningPlan?.stage,
executation_time_ms: executionStats?.executionTimeMillis,
index_name: winningPlan?.inputStage?.indexName,
keys_examined: executionStats?.totalKeysExamined,
docs_examined: executionStats?.totalDocsExamined,
winning_plan: winningPlan,
raw,
};
}
export function validateEntity(
validator: Ajv,
entity: AnyObject,
schema: AnyObject,
throwOnInvalidEvent: boolean = true,
): boolean {
const schemaId = `entity`;
let validate = validator.getSchema(schemaId);
if (!validate) {
validator.addSchema(schema, schemaId);
validate = validator.getSchema(schemaId);
}
const isValid = validate!(entity) as boolean;
if (isValid === false && throwOnInvalidEvent === true) {
const assertionErrorPayload = {
message: 'State schema validation error',
expected: null,
actual: util.inspect(
{
schema,
entity,
errors: validate!.errors,
},
false,
null,
false,
),
};
const err = new assert.AssertionError(assertionErrorPayload);
// @ts-ignore
err.details = validate!.errors;
// @ts-ignore
err.details.push({
entity,
});
// @ts-ignore
err.entity = entity;
throw err;
}
return isValid;
}