UNPKG

@getanthill/datastore

Version:

Event-Sourced Datastore

369 lines (347 loc) 11.4 kB
import type { AnyObject, ModelConfig, ModelSchema, Services } from '../typings'; import _ from 'lodash'; import * as c from '../constants'; import { unique } from '../utils'; const PROPERTIES_MODEL_CONFIG_PATH = 'schema.model.properties'; const MERGE_ARRAY_KEYS = Object.freeze(['required', 'indexes']); export function mergeWithArrays(objValue: any, srcValue: any, key: string) { if (Array.isArray(objValue) && MERGE_ARRAY_KEYS.includes(key)) { return objValue.concat(srcValue).filter(unique); } } export function merge(...args: AnyObject[]) { // @ts-ignore return _.mergeWith.call(null, ...args); } function getDefaultEvents( services: Services, modelConfig: ModelConfig, properties: any, ) { if (modelConfig.with_default_events === false) { return {}; } return { [c.EVENT_TYPE_CREATED]: { '0_0_0': { type: 'object', additionalProperties: modelConfig.schema?.model?.additionalProperties ?? false, required: ['type', 'v', ...(modelConfig.schema?.model?.required ?? [])], properties: { ...properties, ...c.COMPONENT_STATE_PROPERTIES, ...c.COMPONENT_EVENT_PROPERTIES, [services.config.features.properties.is_readonly]: c.COMPONENTS.is_readonly, }, }, }, [c.EVENT_TYPE_UPDATED]: { '0_0_0': { type: 'object', additionalProperties: modelConfig.schema?.model?.additionalProperties ?? false, required: ['type', 'v'], properties: { ...properties, ...c.COMPONENT_STATE_PROPERTIES, ...c.COMPONENT_EVENT_PROPERTIES, [services.config.features.properties.is_readonly]: c.COMPONENTS.is_readonly, }, }, }, [c.EVENT_TYPE_PATCHED]: { '0_0_0': { type: 'object', additionalProperties: modelConfig.schema?.model?.additionalProperties ?? false, required: ['json_patch'], properties: { json_patch: c.COMPONENT_JSON_PATCH, }, }, }, [c.EVENT_TYPE_ARCHIVED]: { '0_0_0': { type: 'object', additionalProperties: modelConfig.schema?.model?.additionalProperties ?? false, properties: { ...properties, [services.config.features.properties.is_readonly]: c.COMPONENTS.is_readonly, [services.config.features.properties.is_archived]: c.COMPONENTS.is_archived, [services.config.features.properties.is_deleted]: c.COMPONENTS.is_deleted, }, }, }, [c.EVENT_TYPE_DELETED]: { '0_0_0': { type: 'object', additionalProperties: modelConfig.schema?.model?.additionalProperties ?? false, properties: { ...properties, [services.config.features.properties.is_readonly]: c.COMPONENTS.is_readonly, [services.config.features.properties.is_archived]: c.COMPONENTS.is_archived, [services.config.features.properties.is_deleted]: c.COMPONENTS.is_deleted, }, }, }, [c.EVENT_TYPE_RESTORED]: { '0_0_0': { type: 'object', additionalProperties: modelConfig.schema?.model?.additionalProperties ?? false, required: ['type', 'v'], properties: { ...properties, ...c.COMPONENT_STATE_PROPERTIES, ...c.COMPONENT_EVENT_PROPERTIES, [services.config.features.properties.is_readonly]: c.COMPONENTS.is_readonly, [services.config.features.properties.is_archived]: c.COMPONENTS.is_archived, [services.config.features.properties.is_deleted]: c.COMPONENTS.is_deleted, }, }, }, [c.EVENT_TYPE_ROLLBACKED]: { '0_0_0': { type: 'object', additionalProperties: modelConfig.schema?.model?.additionalProperties ?? false, required: ['type', 'v'], properties: { ...properties, ...c.COMPONENT_STATE_PROPERTIES, ...c.COMPONENT_EVENT_PROPERTIES, [services.config.features.properties.is_readonly]: c.COMPONENTS.is_readonly, [services.config.features.properties.is_archived]: c.COMPONENTS.is_archived, [services.config.features.properties.is_deleted]: c.COMPONENTS.is_deleted, }, }, }, }; } function extendEventsWithDefaultFields(jsonSchema: AnyObject): ModelSchema { for (const eventType in jsonSchema.events) { for (const eventVersion in jsonSchema.events[eventType]) { _.set( jsonSchema, ['events', eventType, eventVersion], _.mergeWith( {}, { type: 'object', additionalProperties: false, required: ['type', 'v'], properties: { type: c.COMPONENTS.type, v: c.COMPONENTS.v, version: c.COMPONENTS.version, json_patch: c.COMPONENT_JSON_PATCH, created_at: c.COMPONENTS.created_at, }, }, jsonSchema.events[eventType][eventVersion], mergeWithArrays, ), ); } } return jsonSchema as ModelSchema; } function replaceEventsEncryptedFields( modelConfig: ModelConfig, clonedModelConfig: ModelConfig, ) { for (const eventName in clonedModelConfig.schema?.events) { for (const eventVersion in clonedModelConfig.schema.events[eventName]) { for (const field of modelConfig.encrypted_fields ?? []) { if ( _.has( clonedModelConfig.schema.events[eventName][eventVersion].properties, field, ) ) { _.set( clonedModelConfig.schema.events[eventName][eventVersion].properties, field, { description: _.get( clonedModelConfig.schema.events[eventName][eventVersion] .properties, field, { description: '', }, ).description, anyOf: [ { type: 'object', description: `\`encrypted\` ${ _.get( clonedModelConfig.schema.events[eventName][eventVersion] .properties, field, {}, ).description ?? '' }`, properties: { hash: { type: 'string', description: '`sha512` value', example: 'b8cccea15437aef415090bda6acb3b0ad3d4cf7d3e4cf816772e4b43e8f9d08af392bb98b8d532e07249f0d1304e6d65e007205c39913ee5db95578be398f4bd', }, encrypted: { type: 'string', description: 'Encrypted value', example: '03d72e:1e9f1a960adfd0815530f7133c97dcd2:648b63622bb5bc3145de4d3f15fe05651ebdedbeab1d11986538214c4a25b834', }, }, }, _.get( clonedModelConfig.schema.events[eventName][eventVersion] .properties, field, ), ], }, ); } } } } } export function replaceEntityEncryptedFields( modelConfig: ModelConfig, clonedModelConfig: ModelConfig, ): ModelConfig { for (const field of modelConfig.encrypted_fields ?? []) { _.set(clonedModelConfig.schema.model.properties, field, { description: _.get(clonedModelConfig.schema.model.properties, field, { description: '', }).description, anyOf: [ { type: 'object', description: `\`encrypted\` ${ _.get(clonedModelConfig.schema.model.properties, field, {}) .description ?? '' }`, properties: { hash: { type: 'string', description: '`sha512` value', example: 'b8cccea15437aef415090bda6acb3b0ad3d4cf7d3e4cf816772e4b43e8f9d08af392bb98b8d532e07249f0d1304e6d65e007205c39913ee5db95578be398f4bd', }, encrypted: { type: 'string', description: 'Encrypted value', example: '03d72e:1e9f1a960adfd0815530f7133c97dcd2:648b63622bb5bc3145de4d3f15fe05651ebdedbeab1d11986538214c4a25b834', }, }, }, _.get(clonedModelConfig.schema.model.properties, field), ], }); } return clonedModelConfig; } export function replaceEncryptedFields(modelConfig: ModelConfig): ModelConfig { const clonedModelConfig = _.cloneDeep(modelConfig); replaceEntityEncryptedFields(modelConfig, clonedModelConfig); replaceEventsEncryptedFields(modelConfig, clonedModelConfig); return clonedModelConfig; } export default function buildJsonSchema( services: Services, modelConfig: ModelConfig, ): ModelSchema { const extendedModelConfig = replaceEncryptedFields(modelConfig); const correlationField = extendedModelConfig.correlation_field || c.DEFAULT_CORRELATION_FIELD; const properties = { [correlationField]: c.COMPONENT_CORRELATION_ID, ..._.get(extendedModelConfig, PROPERTIES_MODEL_CONFIG_PATH, {}), }; if (extendedModelConfig.with_blockchain_hash === true) { properties[extendedModelConfig.current_hash_field ?? 'hash'] = { type: 'string', description: 'Entity hash', }; properties[extendedModelConfig.previous_hash_field ?? 'prev'] = { type: 'string', description: 'Previous entity hash', }; properties[extendedModelConfig.nonce_field ?? 'nonce'] = { type: 'integer', description: 'Nonce value', }; } const modelJsonSchema = { $id: 'events', components: c.COMPONENTS, retry_duration: 0, model: { type: 'object', additionalProperties: extendedModelConfig.schema?.model?.additionalProperties ?? false, required: extendedModelConfig.schema?.model?.required, properties: { ...properties, version: c.COMPONENT_EVENT_VERSION, created_at: c.COMPONENTS.created_at, updated_at: c.COMPONENTS.updated_at, [services.config.features.properties.is_readonly]: c.COMPONENTS.is_readonly, [services.config.features.properties.is_archived]: c.COMPONENTS.is_archived, [services.config.features.properties.is_deleted]: c.COMPONENTS.is_deleted, }, }, events: getDefaultEvents(services, extendedModelConfig, properties), }; return extendEventsWithDefaultFields( _.mergeWith( {}, modelJsonSchema, extendedModelConfig.schema, mergeWithArrays, ), ); } export function mapDateTimeFormatToEitherStringOrObject( schema: AnyObject, ): AnyObject { return _.mergeWith({}, schema, (src, obj) => { if (obj?.type && obj?.format?.startsWith('date')) { return { oneOf: [ obj, { ...obj, type: 'object', }, ], }; } }); }