@getanthill/datastore
Version:
Event-Sourced Datastore
1,289 lines (1,191 loc) • 34.2 kB
text/typescript
import type { Models } from '../../models';
import type {
GenericType,
Links,
SpecFragment,
BuiltLinks,
} from '../../typings';
import filter from 'lodash/filter';
import get from 'lodash/get';
import has from 'lodash/has';
import isObject from 'lodash/isObject';
import merge from 'lodash/merge';
import omit from 'lodash/omit';
import * as c from '../../constants';
import components from './components';
const MIME_APPLICATION_JSON = 'application/json';
export function build(origin: SpecFragment, models: Models) {
let spec: SpecFragment = merge(
{
paths: {
[`/stream/{model}/{source}`]: {
post: stream(models),
},
[`/stream/{model}/{source}/sse`]: {
get: stream(models, true),
},
},
},
origin,
);
for (const [modelName, model] of new Map<string, GenericType>(
[...models.MODELS.entries()].sort(),
)) {
if (models.isInternalModel(modelName) === false) {
const links = findLinks(model, models);
spec = buildFromModel(spec, model, links);
}
}
return spec;
}
function walkKeysDeep(obj: any, cb: (k: any, p: any) => any, p: any[] = []) {
for (const k in obj) {
cb(k, p);
if (isObject(obj[k])) {
walkKeysDeep(obj[k], cb, [...p, k]);
}
}
}
function showHandler(event: any) {
if (event['x-show-handler'] !== true) {
return '';
}
let description = '';
if (has(event, 'handler')) {
description += `
### Handler
\`\`\`
function handler(state, event) {
${get(event, 'handler')}
}
\`\`\``;
}
return description;
}
export function findLinks(model: GenericType, models: Models): Links {
const links: Links = {};
walkKeysDeep(model.getSchema().model.properties, (k: any, p: any) => {
if (k.endsWith('_id') && k !== model.getCorrelationField()) {
const linkedModel = models.getModelByCorrelationField(k);
/* istanbul ignore next */
if (linkedModel !== null) {
links[k] = {
path: [
...p.filter(
(fragment: any) => !['items', 'properties'].includes(fragment),
),
k,
].join('/'),
model: linkedModel,
};
}
}
});
return links;
}
export function eventNametoCamelCase(str: string) {
return str
.toLowerCase()
.replace(/([-_][a-zA-Z])/g, (group) =>
group.toUpperCase().replace('-', '').replace('_', ''),
);
}
export function buildFromModel(
spec: SpecFragment,
model: GenericType,
links: Links,
) {
const collection = model.getCollectionName();
const schema = model.getSchema();
const modelConfig = model.getModelConfig();
const tagName = getEntityName(model);
const entityName = getEntityName(model, true);
const events = Object.keys(schema.events);
const additionnalPaths = buildAdditionalPaths(model);
const component = schema.model;
const processings = modelConfig.processings ?? [];
const processingsStr = processings
.map((processing, i) => {
const title =
'Activity ' +
tagName[0] +
'.' +
(i + 1) +
' - `' +
processing.field +
'` - ' +
processing.name;
let str = '### ' + title + '\n\n';
str += processing.purpose + '\n';
if (Array.isArray(processing.tokens)) {
str += '#### tokens\n';
str += processing.tokens.map((p) => `- \`${p}\``).join('\n');
str += '\n\n';
}
str += '#### Persons\n';
str += processing.persons.map((p) => `- ${p}`).join('\n');
str += '\n\n';
str += '#### Recipients\n';
str += processing.recipients.map((p) => `- ${p}`).join('\n');
str += '\n\n';
return str;
})
.join('\n');
return {
...spec,
components: {
...spec.components,
schemas: {
...spec.components.schemas,
[entityName]: {
...component,
required: component.required ?? undefined,
},
},
},
tags: [
...(spec.tags || []),
{
name: tagName,
description: `${
modelConfig.description ??
'Routes available for the model <code>' + entityName + '</code>'
}
Events available to this model: ${
events.length > 0
? events.map((event) => '<code>' + event + '</code>').join(', ')
: 'none'
}.
<details>
<summary>JSON Schema</summary>
\`\`\`json
${JSON.stringify(schema.model, null, 2)}
\`\`\`
</summary>
</details>
<details>
<summary>Data Processings (${processings.length})</summary>
${processingsStr}
</summary>
</details>
`,
},
],
paths: merge(
{},
spec.paths,
{
[`/${collection}`]: {
get: getModels(model, links),
},
[`/${collection}/encrypt`]: {
post: encrypt(model, links),
},
[`/${collection}/decrypt`]: {
post: decrypt(model, links),
},
[`/${collection}/events`]: {
post: getModelEvents(model, links, { skipCorrelationField: true }),
},
[`/${collection}/{${model.getCorrelationField()}}`]: {
get: getModel(model, links),
},
[`/${collection}/{${model.getCorrelationField()}}/events`]: {
get: getModelEvents(model, links),
},
[`/${collection}/{${model.getCorrelationField()}}/snapshot`]: {
post: createModelSnapshot(model, links),
},
[`/${collection}/{${model.getCorrelationField()}}/{version}`]: {
get: getModelAtVersion(model, links),
},
},
events.includes(c.EVENT_TYPE_CREATED) && {
[`/${collection}`]: {
post: createModel(model, links),
},
},
events.includes(c.EVENT_TYPE_UPDATED) && {
[`/${collection}/{${model.getCorrelationField()}}`]: {
post: updateModel(model, links),
},
},
events.includes(c.EVENT_TYPE_PATCHED) && {
[`/${collection}/{${model.getCorrelationField()}}`]: {
patch: patchModel(model, links),
},
},
events.includes(c.EVENT_TYPE_RESTORED) && {
[`/${collection}/{${model.getCorrelationField()}}/{version}/restore`]: {
post: restoreVersion(model, links),
},
},
additionnalPaths,
),
};
}
export function getEntityName(model: GenericType | string, singular = false) {
let collection =
typeof model === 'string' ? model : model.getCollectionName();
if (singular === true) {
collection = collection.replace(/s$/, '');
}
return collection.charAt(0).toUpperCase() + collection.slice(1);
}
function buildLinks(links: Links) {
const _links: BuiltLinks = {};
for (const key in links) {
_links[getEntityName(links[key].model, true)] = {
operationId: eventNametoCamelCase(
`GET_${getEntityName(links[key].model, true)}`,
),
parameters: {
/**
* @fixme Apply the camelCase only for GraphQL because in the current
* implementation, the link on REST is invalid
*/
[key]: `$response.body#/${eventNametoCamelCase(links[key].path)}`, // For GraphQL
},
};
}
return _links;
}
export function defaultSchema(model: GenericType) {
return {
tags: [getEntityName(model)],
security: [
{
apiKey: [],
},
],
responses: {
default: {
$ref: '#/components/responses/default',
},
},
};
}
export function buildAdditionalPaths(model: GenericType) {
const additionnalPaths: { [key: string]: {} } = {};
const collection = model.getCollectionName();
const schema = model.getSchema();
const originalSchema = model.getOriginalSchema();
const events = schema.events;
const entityName = getEntityName(model, true);
for (const eventType in events) {
if (
![
c.EVENT_TYPE_CREATED,
c.EVENT_TYPE_UPDATED,
c.EVENT_TYPE_RESTORED,
c.EVENT_TYPE_ROLLBACKED,
c.EVENT_TYPE_PATCHED,
c.EVENT_TYPE_ARCHIVED,
c.EVENT_TYPE_DELETED,
].includes(eventType)
) {
const eventTypeLowered = eventType.toLocaleLowerCase();
for (const eventVersion in events[eventType]) {
const event = events[eventType][eventVersion];
const required = filter(
event.required,
(r) => !['v', 'type'].includes(r),
);
const routeSpec = merge({}, defaultSchema(model), {
operationId: eventNametoCamelCase(`${entityName}_${eventType}`),
summary: event.title || `${eventTypeLowered}:v${eventVersion}`,
description:
event.description ||
`Business event <code>${eventType}</code> at version <code>${eventVersion}</code>`,
parameters: [
{
in: 'path',
name: model.getCorrelationField(),
description: 'Correlation id',
schema: c.COMPONENT_CORRELATION_ID,
required: true,
},
],
requestBody: {
content: {
[MIME_APPLICATION_JSON]: {
schema: {
...omit(
event,
'handler',
'handlers',
'x-responses',
'x-show-handler',
'is_created',
'is_fhe',
'upsert',
),
properties: {
...omit(
event.properties,
'v',
'type',
'version',
'json_patch',
'created_at',
'updated_at',
),
},
required: required.length ? required : undefined,
},
},
},
},
responses: {
200: {
description: 'Entity successfully updated',
content: {
[MIME_APPLICATION_JSON]: {
schema: {
$ref: `#/components/schemas/${entityName}`,
},
},
},
},
400: {
...components.responses[400],
description: 'Invalid Model / Event schema validation error',
},
409: components.responses[409],
422: components.responses[422],
},
});
routeSpec.description += showHandler(event);
const additionalResponses: any = {};
(event['x-responses'] || []).forEach((response: any) => {
additionalResponses[response.status] = {
description: response.description,
content: {
'application/json': {
example: {
status: response.status,
message: response.description,
details: response.details,
},
schema: {
$ref: '#/components/schemas/error',
},
},
},
};
});
routeSpec.responses = {
...routeSpec.responses,
...additionalResponses,
};
const originalEvent = get(
originalSchema,
`events.${eventType}.${eventVersion}`,
{},
);
routeSpec.requestBody.content[MIME_APPLICATION_JSON].schema.properties =
{
...routeSpec.requestBody.content[MIME_APPLICATION_JSON].schema
.properties,
...omit(
originalEvent.properties,
'v',
'type',
'version',
'json_patch',
'created_at',
'updated_at',
),
};
additionnalPaths[
`/${collection}/{${model.getCorrelationField()}}/${eventTypeLowered}/${eventVersion}`
] = {
post: routeSpec,
};
}
}
}
return additionnalPaths;
}
export function stream(models: Models, isSSE: boolean = false) {
const entityName = 'all';
return merge(
{},
{
operationId: eventNametoCamelCase(
`stream_${entityName}${isSSE === true ? '_sse' : ''}`,
),
summary: `Stream ${entityName} changes${isSSE === true ? ' SSE' : ''}`,
description: `Stream <code>${entityName}</code> changes in the database such as
creation or update${isSSE === true ? ' with Server Sent Events' : ''}.`,
parameters: [
{
in: 'path',
name: 'model',
description: 'Model name',
schema: {
type: 'string',
example: 'users',
enum: [
'all',
...Array.from(models.MODELS.keys()).filter(
(m) => models.isInternalModel(m) === false,
),
],
},
required: true,
},
{
in: 'path',
name: 'source',
description: 'Data source to stream',
schema: {
type: 'string',
enum: ['entities', 'events'],
example: 'entities',
},
required: true,
},
{
in: 'header',
name: 'output',
description: 'Expected output',
schema: {
type: 'string',
enum: ['entity', 'raw'],
example: 'entity',
},
required: false,
},
],
...(isSSE === false && {
requestBody: {
content: {
[MIME_APPLICATION_JSON]: {
schema: {
type: 'array',
items: {
type: 'object',
},
default: [],
example: [
{
$match: {
'fullDocument.firstname': 'John',
},
},
],
},
},
},
},
}),
responses: {
200: {
description: 'Stream of JSON objects',
content: {
[isSSE === true ? 'text/event-stream' : MIME_APPLICATION_JSON]: {
schema: {
type: 'array',
items: {
type: 'object',
},
example: [
{
firstname: 'John',
},
],
},
},
},
},
400: components.responses[400],
404: components.responses[404],
},
},
);
}
export function createModel(model: GenericType, links: Links) {
const schema = model.getSchema();
const event = schema.events[c.EVENT_TYPE_CREATED]['0_0_0'];
const required = filter(event.required, (r) => !['v', 'type'].includes(r));
const entityName = getEntityName(model, true);
const routeSpec = merge({}, defaultSchema(model), {
operationId: eventNametoCamelCase(`create_${entityName}`),
summary: event.title || `Create a new ${entityName}`,
description:
event.description ||
`Create a new <code>${entityName}</code> in the database.`,
requestBody: {
content: {
[MIME_APPLICATION_JSON]: {
schema: {
...omit(event, 'handler'),
properties: {
...omit(
event.properties,
'v',
'type',
'version',
'json_patch',
'created_at',
'updated_at',
model.getIsReadonlyProperty(model.getModelConfig()),
model.getIsArchivedProperty(model.getModelConfig()),
model.getIsDeletedProperty(model.getModelConfig()),
),
},
required: required.length ? required : undefined,
},
},
},
},
responses: {
200: {
description: 'Entity successfully created',
content: {
[MIME_APPLICATION_JSON]: {
schema: {
$ref: `#/components/schemas/${entityName}`,
},
},
},
links: buildLinks(links),
},
400: {
...components.responses[400],
description: 'Invalid Model / Event schema validation error',
},
409: components.responses[409],
},
});
routeSpec.description += showHandler(event);
const originalSchema = model.getOriginalSchema();
if (originalSchema) {
const originalEvent = get(
originalSchema,
`events.${c.EVENT_TYPE_CREATED}.0_0_0`,
{},
);
routeSpec.requestBody.content[MIME_APPLICATION_JSON].schema.properties = {
...routeSpec.requestBody.content[MIME_APPLICATION_JSON].schema.properties,
...omit(
originalEvent.properties,
'v',
'type',
'version',
'json_patch',
'created_at',
'updated_at',
),
};
}
return routeSpec;
}
export function updateModel(model: GenericType, links: Links) {
const schema = model.getSchema();
const event = schema.events[c.EVENT_TYPE_UPDATED]['0_0_0'];
const required = filter(event.required, (r) => !['v', 'type'].includes(r));
const entityName = getEntityName(model, true);
const routeSpec = merge({}, defaultSchema(model), {
operationId: eventNametoCamelCase(`update_${entityName}`),
summary: event.title || `Update a ${entityName}`,
description:
event.description ||
`Update an existing <code>${entityName}</code> already present in the database.`,
parameters: [
{
in: 'path',
name: model.getCorrelationField(),
description: 'Correlation id',
schema: c.COMPONENT_CORRELATION_ID,
required: true,
},
],
requestBody: {
content: {
[MIME_APPLICATION_JSON]: {
schema: {
...omit(event, 'handler'),
properties: {
...omit(
event.properties,
'v',
'type',
'version',
'json_patch',
'created_at',
'updated_at',
model.getIsReadonlyProperty(model.getModelConfig()),
model.getIsArchivedProperty(model.getModelConfig()),
model.getIsDeletedProperty(model.getModelConfig()),
),
},
required: required.length ? required : undefined,
},
},
},
},
responses: {
200: {
description: 'Entity successfully updated',
content: {
[MIME_APPLICATION_JSON]: {
schema: {
$ref: `#/components/schemas/${entityName}`,
},
},
},
links: buildLinks(links),
},
400: {
...components.responses[400],
description: 'Invalid Model / Event schema validation error',
},
409: components.responses[409],
422: components.responses[422],
},
});
routeSpec.description += showHandler(event);
const originalSchema = model.getOriginalSchema();
if (originalSchema) {
const originalEvent = get(
originalSchema,
`events.${c.EVENT_TYPE_UPDATED}.0_0_0`,
{},
);
routeSpec.requestBody.content[MIME_APPLICATION_JSON].schema.properties = {
...routeSpec.requestBody.content[MIME_APPLICATION_JSON].schema.properties,
...omit(
originalEvent.properties,
'v',
'type',
'version',
'json_patch',
'created_at',
'updated_at',
),
};
}
return routeSpec;
}
export function patchModel(model: GenericType, links: Links) {
const schema = model.getSchema();
const event = schema.events[c.EVENT_TYPE_PATCHED]['0_0_0'];
const entityName = getEntityName(model, true);
const routeSpec = merge({}, defaultSchema(model), {
operationId: eventNametoCamelCase(`patch_${entityName}`),
summary: event.title || `Patch a ${entityName}`,
description:
event.description ||
`Patch an existing <code>${entityName}</code> already present in the database.`,
parameters: [
{
in: 'path',
name: model.getCorrelationField(),
description: 'Correlation id',
schema: c.COMPONENT_CORRELATION_ID,
required: true,
},
],
requestBody: {
content: {
[MIME_APPLICATION_JSON]: {
schema: {
type: 'object',
required: ['json_patch'],
additionalProperties: false,
properties: {
json_patch: event.properties.json_patch,
},
},
},
},
},
responses: {
200: {
description: 'Entity successfully patched',
content: {
[MIME_APPLICATION_JSON]: {
schema: {
$ref: `#/components/schemas/${entityName}`,
},
},
},
links: buildLinks(links),
},
400: {
...components.responses[400],
description: 'Invalid Model / Event schema validation error',
},
409: components.responses[409],
422: components.responses[422],
},
});
routeSpec.description += showHandler(event);
return routeSpec;
}
export function getModels(model: GenericType, links: Links) {
const modelConfig = model.getModelConfig();
const originalSchema = model.getOriginalSchema();
const schema = model.getSchema();
const entityName = getEntityName(model);
const indexedFields = modelConfig.indexes?.reduce(
(current, index) => [...current, ...Object.keys(index.fields)],
[modelConfig.correlation_field],
);
const parameters: any[] = Object.keys(schema.model.properties)
.filter((key) => {
return ![
model.getIsArchivedProperty(modelConfig),
model.getIsDeletedProperty(modelConfig),
].includes(key);
})
.map((key) => {
const isIndexed = indexedFields?.includes(key);
const paramSchema = {
// Remove default value in the GET find query parameters:
...omit(schema.model.properties[key], 'default'),
description: `${isIndexed ? '<code>index</code> ' : ''}${
schema.model.properties[key].description ?? ''
}`,
};
const _schema: any = {
anyOf: [
paramSchema,
{
type: 'array',
items: paramSchema,
},
{
type: 'object',
},
],
};
return {
in: 'query',
name: key,
description: `${isIndexed ? '<code>index</code> ' : ''}${
schema.model.properties[key].description ?? ''
}`,
schema: _schema,
required: false,
};
});
// Raw queries support:
parameters.push(
{
in: 'query',
name: '_q',
description: 'MongoDb query in JSON stringified',
schema: {
type: 'string',
},
},
{
in: 'query',
name: '_must_hash',
description:
'Do we need to hash query values before find results if needed?',
schema: {
type: 'boolean',
},
},
{
in: 'header',
name: 'page',
...components.headers['pagination-page'],
},
{
in: 'header',
name: 'page-size',
...components.headers['pagination-size'],
},
{
in: 'header',
name: 'decrypt',
description: 'should we decipher the value',
schema: {
type: 'string',
enum: ['true', 'false'],
},
},
);
return merge({}, defaultSchema(model), {
operationId: eventNametoCamelCase(`GET_${entityName}`),
summary: `Find ${entityName}`,
description: `Find <code>${entityName}</code> present in the database.`,
parameters,
responses: {
200: {
description: 'List of entities',
content: {
[MIME_APPLICATION_JSON]: {
schema: {
type: 'array',
items: {
$ref: `#/components/schemas/${getEntityName(model, true)}`,
},
},
},
},
headers: {
'correlation-field': {
description: 'Correlation field of the model',
schema: {
type: 'string',
},
},
page: {
...components.headers['pagination-page'],
},
'page-size': {
...components.headers['pagination-size'],
},
'page-count': {
...components.headers['pagination-count'],
},
count: {
description: 'Total number of items',
schema: {
type: 'integer',
minimum: 0,
},
},
},
links: buildLinks(links),
},
400: components.responses[400],
},
});
}
export function getModel(model: GenericType, links: Links) {
const entityName = getEntityName(model, true);
return merge({}, defaultSchema(model), {
operationId: eventNametoCamelCase(`GET_${entityName}`),
summary: `Get a ${entityName}`,
description: `Get a specific <code>${entityName}</code> uniquely identified
by its <code>${model.getCorrelationField()}</code>.`,
parameters: [
{
in: 'path',
name: model.getCorrelationField(),
description: 'Correlation id',
schema: c.COMPONENT_CORRELATION_ID,
required: true,
},
{
in: 'header',
name: 'decrypt',
description: 'should we decipher the value',
schema: {
type: 'string',
enum: ['true', 'false'],
},
},
],
responses: {
200: {
description: 'Entity successfully fetched',
content: {
[MIME_APPLICATION_JSON]: {
schema: {
$ref: `#/components/schemas/${entityName}`,
},
},
},
links: buildLinks(links),
},
400: components.responses[400],
404: components.responses[404],
},
});
}
export function getModelAtVersion(model: GenericType, links: Links) {
const entityName = getEntityName(model, true);
return merge({}, defaultSchema(model), {
operationId: eventNametoCamelCase(`${entityName}_AT_VERSION`),
summary: `Get ${entityName} at version`,
description: `Get a specific version for a <code>${entityName}</code>.`,
parameters: [
{
in: 'path',
name: model.getCorrelationField(),
description: 'Correlation id',
schema: c.COMPONENT_CORRELATION_ID,
required: true,
},
{
in: 'path',
name: 'version',
schema: {
type: 'string',
default: '0',
},
required: true,
description: 'Version',
},
],
responses: {
200: {
description: 'The model at the given version',
content: {
[MIME_APPLICATION_JSON]: {
schema: {
$ref: `#/components/schemas/${entityName}`,
},
},
},
links: buildLinks(links),
},
400: components.responses[400],
404: components.responses[404],
},
});
}
export function restoreVersion(model: GenericType, links: Links) {
const entityName = getEntityName(model, true);
return merge({}, defaultSchema(model), {
operationId: eventNametoCamelCase(`RESTORE_${entityName}_AT_VERSION`),
summary: `Restore ${entityName} at version`,
description: `Restore a specific version of the <code>${entityName}</code>.`,
parameters: [
{
in: 'path',
name: model.getCorrelationField(),
description: 'Correlation id',
schema: c.COMPONENT_CORRELATION_ID,
required: true,
},
{
in: 'path',
name: 'version',
schema: {
type: 'integer',
default: 0,
},
required: true,
description: 'Version',
},
],
responses: {
200: {
description: 'The model at the given version',
content: {
[MIME_APPLICATION_JSON]: {
schema: {
$ref: `#/components/schemas/${entityName}`,
},
},
},
links: buildLinks(links),
},
400: components.responses[400],
404: components.responses[404],
409: components.responses[409],
},
});
}
export function getModelEvents(
model: GenericType,
links: Links,
options: { skipCorrelationField: boolean } = { skipCorrelationField: false },
) {
const entityName = getEntityName(model, true);
let operationId = eventNametoCamelCase(`${entityName}_EVENTS`);
let summary = `Get a ${entityName} events`;
let description = `Get all events attached to a <code>${entityName}</code>.`;
let parameters = [
{
in: 'path',
name: model.getCorrelationField(),
description: 'Correlation id',
schema: c.COMPONENT_CORRELATION_ID,
required: true,
},
];
if (options.skipCorrelationField === true) {
operationId = eventNametoCamelCase(`${entityName}_ALL_EVENTS`);
summary = 'Get all events';
description = `Get all events created for this kind of entities whatever
the value of the correlation ID`;
parameters = [];
}
return merge({}, defaultSchema(model), {
operationId,
summary,
description,
parameters: [
...parameters,
{
in: 'header',
name: 'page',
...components.headers['pagination-page'],
},
{
in: 'header',
name: 'page-size',
...components.headers['pagination-size'],
},
],
responses: {
200: {
description: 'Entity events successfully fetched',
content: {
[MIME_APPLICATION_JSON]: {
schema: {
type: 'array',
items: {
type: 'object',
required: ['v', 'type', 'version'],
properties: {
type: c.COMPONENT_EVENT_TYPE,
v: c.COMPONENT_EVENT_TYPE_VERSION,
version: c.COMPONENT_EVENT_VERSION,
},
},
},
},
},
links: buildLinks(links),
},
400: components.responses[400],
404: components.responses[404],
},
});
}
export function createModelSnapshot(model: GenericType, links: Links) {
const entityName = getEntityName(model, true);
return merge({}, defaultSchema(model), {
operationId: eventNametoCamelCase(`CREATE_${entityName}_SNAPSHOT`),
summary: `Create snapshot`,
description: `Create a snapshot of a <code>${entityName}</code> to keep
a frozen version in database.`,
parameters: [
{
in: 'path',
name: model.getCorrelationField(),
description: 'Correlation id',
schema: c.COMPONENT_CORRELATION_ID,
required: true,
},
{
in: 'query',
name: 'version',
description: 'State version to snapshot',
schema: {
type: 'string',
},
},
{
in: 'query',
name: 'clean',
description:
'Remove events with version less than or equal the provided version',
schema: {
type: 'boolean',
},
},
],
responses: {
200: {
description: 'Snapshot successfully created',
content: {
[MIME_APPLICATION_JSON]: {
schema: {
$ref: `#/components/schemas/${entityName}`,
},
},
},
links: buildLinks(links),
},
400: components.responses[400],
422: components.responses[422],
},
});
}
export function encrypt(model: GenericType, links: Links) {
const originalSchema = model.getOriginalSchema();
const schema = model.getSchema();
const entityName = getEntityName(model, true);
return merge({}, defaultSchema(model), {
operationId: eventNametoCamelCase(`ENCRYPT_${entityName}`),
summary: `Encrypt fields`,
description: `Encrypt fields in <code>${entityName}</code>.`,
requestBody: {
content: {
[MIME_APPLICATION_JSON]: {
schema: {
type: 'array',
items: {
type: 'object',
properties: schema.model.properties,
},
},
},
},
},
responses: {
200: {
description: 'Encrypted fields',
content: {
[MIME_APPLICATION_JSON]: {
schema: {
type: 'array',
items: {
...omit(get(originalSchema, 'model'), 'handler'),
required: undefined,
},
},
},
},
links: buildLinks(links),
},
400: components.responses[400],
422: components.responses[422],
},
});
}
export function decrypt(model: GenericType, links: Links) {
const originalSchema = model.getOriginalSchema();
const schema = model.getSchema();
const entityName = getEntityName(model, true);
return merge({}, defaultSchema(model), {
operationId: eventNametoCamelCase(`DECRYPT_${entityName}`),
summary: `Decrypt encrypted fields`,
description: `Decrypt fields in <code>${entityName}</code>.`,
requestBody: {
content: {
[MIME_APPLICATION_JSON]: {
schema: {
type: 'array',
items: {
type: 'object',
properties: schema.model.properties,
},
},
},
},
},
responses: {
200: {
description: 'Decrypted fields',
content: {
[MIME_APPLICATION_JSON]: {
schema: {
type: 'array',
items: {
...omit(get(originalSchema, 'model'), 'handler'),
additionalProperties: true,
required: undefined,
},
},
},
},
links: buildLinks(links),
},
400: components.responses[400],
422: components.responses[422],
},
});
}