Skip to content

Commit

Permalink
feat(csi-106): added iso-20022 headers support (#402)
Browse files Browse the repository at this point in the history
* feat(csi-106): added iso-20022 headers support

* chore(snapshot): 18.10.0-snapshot.0

* feat(csi-106): cleaned up

* feat(csi-106): added API_TYPES

* chore(snapshot): 18.10.0-snapshot.1

* feat(csi-106): exported checkApiType

* chore(snapshot): 18.10.0-snapshot.2

* feat(csi-106): improved typings

* chore(snapshot): 18.10.0-snapshot.3

* feat(csi-106): updated transformHeaders to support ISO

* chore(snapshot): 18.10.0-snapshot.4

---------

Co-authored-by: Kevin Leyow <kleyow@gmail.com>
  • Loading branch information
geka-evk and kleyow authored Oct 24, 2024
1 parent bcc79a8 commit 290cad3
Show file tree
Hide file tree
Showing 15 changed files with 399 additions and 220 deletions.
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mojaloop/central-services-shared",
"version": "18.10.0",
"version": "18.10.0-snapshot.4",
"description": "Shared code for mojaloop central services",
"license": "Apache-2.0",
"author": "ModusBox",
Expand Down Expand Up @@ -81,7 +81,7 @@
"audit-ci": "^7.1.0",
"base64url": "3.0.1",
"chance": "1.1.12",
"npm-check-updates": "17.1.3",
"npm-check-updates": "17.1.4",
"nyc": "17.1.0",
"pre-commit": "1.2.2",
"proxyquire": "2.1.3",
Expand Down Expand Up @@ -113,7 +113,7 @@
},
"peerDependencies": {
"@mojaloop/central-services-error-handling": ">=13.x.x",
"@mojaloop/central-services-logger": ">=11.x.x",
"@mojaloop/central-services-logger": ">=11.5.x",
"@mojaloop/central-services-metrics": ">=12.x.x",
"@mojaloop/event-sdk": ">=14.1.1",
"ajv": "8.x.x",
Expand Down
11 changes: 11 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const API_TYPES = Object.freeze({
fspiop: 'fspiop',
iso20022: 'iso20022'
})

const ISO_HEADER_PART = 'iso20022'

module.exports = {
API_TYPES,
ISO_HEADER_PART
}
37 changes: 35 additions & 2 deletions src/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Utils as HapiUtil } from '@hapi/hapi'
import { Utils as HapiUtil, Server } from '@hapi/hapi'

declare namespace CentralServicesShared {
interface ReturnCode {
CODE: number;
Expand Down Expand Up @@ -657,11 +658,43 @@ declare namespace CentralServicesShared {
convertSupportedVersionToExtensionList(supportedVersions: Array<number>): Array<{ key: string, value: string }>
}

type ProtocolResources = string[]
type ProtocolVersions = (string | symbol)[]
type ApiTypeValues = 'fspiop' | 'iso20022'

type HapiUtil = {
HapiRawPayload: {
plugin: {
name: string,
register: (server: Server) => void
}
};
FSPIOPHeaderValidation: {
plugin: {
name: string,
register: (
server: Server,
options: {
resources: ProtocolResources,
supportedProtocolContentVersions: ProtocolVersions,
supportedProtocolAcceptVersions: ProtocolVersions,
apiType: ApiTypeValues
}
) => void
},
errorMessages: Record<string, string>,
defaultProtocolResources: ProtocolResources
defaultProtocolVersions: ProtocolVersions
};
API_TYPES: Record<ApiTypeValues, ApiTypeValues>;
}
// todo: define the rest of the types

interface Util {
Endpoints: Endpoints;
Participants: Participants;
proxies: Proxies;
Hapi: any;
Hapi: HapiUtil;
Kafka: Kafka;
OpenapiBackend: any;
Request: Request;
Expand Down
8 changes: 8 additions & 0 deletions src/logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const { loggerFactory } = require('@mojaloop/central-services-logger/src/contextLogger')

const logger = loggerFactory('CSSh') // global logger

module.exports = {
logger,
loggerFactory
}
4 changes: 3 additions & 1 deletion src/util/hapi/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ const OpenapiBackendValidator = require('./plugins/openapiBackendValidator')
const FSPIOPHeaderValidation = require('./plugins/headerValidation')
const customCurrencyCodeValidation = require('./plugins/customCurrencyCodeExtension')
const APIDocumentation = require('./plugins/apiDocumentation')
const { API_TYPES } = require('../../constants')

module.exports = {
HapiRawPayload,
HapiEventPlugin,
OpenapiBackendValidator,
FSPIOPHeaderValidation,
customCurrencyCodeValidation,
APIDocumentation
APIDocumentation,
API_TYPES
}
18 changes: 14 additions & 4 deletions src/util/hapi/plugins/headerValidation.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@
// accuracy of this statement has not been thoroughly tested.

const { Factory: { createFSPIOPError }, Enums } = require('@mojaloop/central-services-error-handling')
const { parseAcceptHeader, parseContentTypeHeader, protocolVersions, convertSupportedVersionToExtensionList } = require('../../headerValidation')
const { API_TYPES } = require('../../../constants')
const {
checkApiType,
parseAcceptHeader,
parseContentTypeHeader,
protocolVersions,
convertSupportedVersionToExtensionList
} = require('../../headerValidation')

// Some defaults

Expand Down Expand Up @@ -54,8 +61,11 @@ const plugin = {
register: function (server, /* options: */ {
resources = defaultProtocolResources,
supportedProtocolContentVersions = defaultProtocolVersions,
supportedProtocolAcceptVersions = defaultProtocolVersions
supportedProtocolAcceptVersions = defaultProtocolVersions,
apiType = API_TYPES.fspiop
}) {
checkApiType(apiType)

server.ext('onPostAuth', (request, h) => {
// First, extract the resource type from the path
const resource = request.path.replace(/^\//, '').split('/')[0]
Expand All @@ -71,7 +81,7 @@ const plugin = {
if (request.headers.accept === undefined) {
throw createFSPIOPError(Enums.FSPIOPErrorCodes.MISSING_ELEMENT, errorMessages.REQUIRE_ACCEPT_HEADER)
}
const accept = parseAcceptHeader(resource, request.headers.accept)
const accept = parseAcceptHeader(resource, request.headers.accept, apiType)
if (!accept.valid) {
throw createFSPIOPError(
Enums.FSPIOPErrorCodes.MALFORMED_SYNTAX,
Expand All @@ -94,7 +104,7 @@ const plugin = {
if (request.headers['content-type'] === undefined) {
throw createFSPIOPError(Enums.FSPIOPErrorCodes.MISSING_ELEMENT, errorMessages.REQUIRE_CONTENT_TYPE_HEADER)
}
const contentType = parseContentTypeHeader(resource, request.headers['content-type'])
const contentType = parseContentTypeHeader(resource, request.headers['content-type'], apiType)
if (!contentType.valid) {
throw createFSPIOPError(
Enums.FSPIOPErrorCodes.MALFORMED_SYNTAX,
Expand Down
37 changes: 23 additions & 14 deletions src/util/headerValidation/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

const assert = require('assert').strict
const _ = require('lodash')
const { API_TYPES } = require('../../constants')
const { isoHeaderPart } = require('../helpers')

const protocolVersions = {
anyVersion: Symbol('Any'),
Expand All @@ -17,23 +19,30 @@ const protocolVersionsMap = [

// Some convenience functions for generating regexes for header matching

const generateContentTypeRegex = resource =>
new RegExp(`^application/vnd\\.interoperability\\.${resource}\\+json\\s{0,1};\\s{0,1}version=(\\d+\\.\\d+)$`)
const generateContentTypeRegex = (resource, apiType) =>
new RegExp(`^application/vnd\\.interoperability${isoHeaderPart(apiType)}\\.${resource}\\+json\\s{0,1};\\s{0,1}version=(\\d+\\.\\d+)$`)

const generateAcceptRegex = resource =>
new RegExp(`^${generateSingleAcceptRegexStr(resource)}(,${generateSingleAcceptRegexStr(resource)})*$`)
const generateAcceptRegex = (resource, apiType) =>
new RegExp(`^${generateSingleAcceptRegexStr(resource, apiType)}(,${generateSingleAcceptRegexStr(resource, apiType)})*$`)

const generateSingleAcceptRegex = resource =>
new RegExp(generateSingleAcceptRegexStr(resource))
const generateSingleAcceptRegex = (resource, apiType) =>
new RegExp(generateSingleAcceptRegexStr(resource, apiType))

const generateSingleAcceptRegexStr = resource =>
`application/vnd\\.interoperability\\.${resource}\\+json(\\s{0,1};\\s{0,1}version=\\d+(\\.\\d+)?)?`
const generateSingleAcceptRegexStr = (resource, apiType) =>
`application/vnd\\.interoperability${isoHeaderPart(apiType)}\\.${resource}\\+json(\\s{0,1};\\s{0,1}version=\\d+(\\.\\d+)?)?`

const parseContentTypeHeader = (resource, header) => {
const checkApiType = (apiType) => {
if (Object.values(API_TYPES).includes(apiType)) {
return true
}
throw new TypeError(`Invalid API type: ${apiType}`)
}

const parseContentTypeHeader = (resource, header, apiType = API_TYPES.fspiop) => {
assert(typeof header === 'string')

// Create the validation regex
const r = generateContentTypeRegex(resource)
const r = generateContentTypeRegex(resource, apiType)

// Test the header
const match = header.match(r)
Expand All @@ -47,11 +56,11 @@ const parseContentTypeHeader = (resource, header) => {
}
}

const parseAcceptHeader = (resource, header) => {
const parseAcceptHeader = (resource, header, apiType = API_TYPES.fspiop) => {
assert(typeof header === 'string')

// Create the validation regex
const r = generateAcceptRegex(resource)
const r = generateAcceptRegex(resource, apiType)

// Test the header
if (header.match(r) === null) {
Expand All @@ -61,8 +70,7 @@ const parseAcceptHeader = (resource, header) => {
// The header contains a comma-delimited set of versions, extract these
const versions = new Set(header
.split(',')
// @ts-ignore
.map(verStr => verStr.match(generateSingleAcceptRegex(resource))[1])
.map(verStr => verStr.match(generateSingleAcceptRegex(resource, apiType))[1])
.map(match => match === undefined ? protocolVersions.anyVersion : match.split('=')[1])
)

Expand Down Expand Up @@ -115,5 +123,6 @@ module.exports = {
generateContentTypeRegex,
parseAcceptHeader,
parseContentTypeHeader,
checkApiType,
convertSupportedVersionToExtensionList
}
18 changes: 12 additions & 6 deletions src/util/headers/transformer.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@

'use strict'

const ENUM = require('../../enums').Http
const ErrorHandler = require('@mojaloop/central-services-error-handling')

const resourceVersions = require('../helpers').resourceVersions
const ENUM = require('../../enums').Http
const { API_TYPES } = require('../../constants')
const { resourceVersions, isoHeaderPart } = require('../helpers')

const MISSING_FUNCTION_PARAMETERS = 'Missing parameters for function'

Expand Down Expand Up @@ -62,6 +62,11 @@ const getResourceInfoFromHeader = (headerValue) => {
return result
}

const makeAcceptContentTypeHeader = (resourceType, version, apiType = API_TYPES.fspiop) => {
const isoString = isoHeaderPart(apiType)
return `application/vnd.interoperability${isoString}.${resourceType}+json;version=${version}`
}

/**
* @function transformHeaders
*
Expand Down Expand Up @@ -190,7 +195,7 @@ const transformHeaders = (headers, config) => {
if (!resourceType) resourceType = getResourceInfoFromHeader(headers[headerKey]).resourceType
// Fall back to using the legacy approach to determine the resourceVersion
if (resourceType && !acceptVersion) acceptVersion = resourceVersions[resourceType].acceptVersion
normalizedHeaders[headerKey] = `application/vnd.interoperability.${resourceType}+json;version=${acceptVersion}`
normalizedHeaders[headerKey] = makeAcceptContentTypeHeader(resourceType, acceptVersion, config?.apiType)
break
case (ENUM.Headers.GENERAL.CONTENT_TYPE.value):
if (!config.hubNameRegex.test(config.sourceFsp)) {
Expand All @@ -200,7 +205,7 @@ const transformHeaders = (headers, config) => {
if (!resourceType) resourceType = getResourceInfoFromHeader(headers[headerKey]).resourceType
// Fall back to using the legacy approach to determine the resourceVersion
if (resourceType && !contentVersion) contentVersion = resourceVersions[resourceType].contentVersion
normalizedHeaders[headerKey] = `application/vnd.interoperability.${resourceType}+json;version=${contentVersion}`
normalizedHeaders[headerKey] = makeAcceptContentTypeHeader(resourceType, contentVersion, config?.apiType)
break
default:
normalizedHeaders[headerKey] = headerValue
Expand All @@ -222,5 +227,6 @@ const transformHeaders = (headers, config) => {

module.exports = {
getResourceInfoFromHeader,
transformHeaders
transformHeaders,
makeAcceptContentTypeHeader
}
4 changes: 4 additions & 0 deletions src/util/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

const RC = require('rc')('LIB')
const defaultVersions = require('../enums/http').Headers.DEFAULT_API_VERSIONS
const { API_TYPES, ISO_HEADER_PART } = require('../constants')

const getVersionFromConfig = (resourceString) => {
const resourceVersionMap = {}
Expand Down Expand Up @@ -71,7 +72,10 @@ const transpose = (obj) => {
return transposed
}

const isoHeaderPart = apiType => `${apiType === API_TYPES.iso20022 ? `.${ISO_HEADER_PART}` : ''}`

module.exports = {
isoHeaderPart,
transpose,
resourceVersions,
__parseResourceVersions: parseResourceVersions
Expand Down
Loading

0 comments on commit 290cad3

Please sign in to comment.