diff --git a/customTypings/fast-luhn/index.d.ts b/customTypings/fast-luhn/index.d.ts new file mode 100644 index 0000000..53cc8e4 --- /dev/null +++ b/customTypings/fast-luhn/index.d.ts @@ -0,0 +1,4 @@ +declare module 'fast-luhn' { + const luhn: (el: string) => string; + export = luhn +} diff --git a/package.json b/package.json index b81d17d..fe03845 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "dependencies": { "@qiwi/substrate": "^1.18.11", "@types/lodash.once": "^4.1.6", + "fast-luhn": "^1.0.4", "lodash.once": "^4.1.1", "safe-json-stringify": "^1.2.0", "tslib": "^1.11.1" diff --git a/src/main/ts/index.ts b/src/main/ts/index.ts index 97ae6ce..e5bc156 100644 --- a/src/main/ts/index.ts +++ b/src/main/ts/index.ts @@ -1,4 +1,5 @@ export { createHttpPipe } from './pipes/http' +export { createMaskerPipe, panMaskerPipe } from './pipes/masker' export { createTransmitter, createTransmittable } from './transmitter' export * from './interfaces' diff --git a/src/main/ts/pipes/masker.ts b/src/main/ts/pipes/masker.ts new file mode 100644 index 0000000..6c1e509 --- /dev/null +++ b/src/main/ts/pipes/masker.ts @@ -0,0 +1,28 @@ +import luhn from 'fast-luhn' +import { IPipe } from '../interfaces' +import { deepMap } from '../utils' +const type = 'masker' + +export const createMaskerPipe = ( + fn: (el: any, key?: string) => {} +): IPipe => ({ + type, + async execute ({ data }) { + try { + const maskedData = deepMap(data, fn) + return [null, maskedData] + } catch (e) { + return [e, null] + } + } +}) + +export const panMaskerFn = (input: string | number): string => { + return (input + '').replace(/\d{13,19}/g, v => + luhn(v) + ? `${v.slice(0, 4)} **** **** ${v.slice(-4)}` + : '' + input + ) +} + +export const panMaskerPipe = createMaskerPipe(panMaskerFn) diff --git a/src/main/ts/utils/deepmap.ts b/src/main/ts/utils/deepmap.ts new file mode 100644 index 0000000..c61e3ff --- /dev/null +++ b/src/main/ts/utils/deepmap.ts @@ -0,0 +1,22 @@ +export function deepMap ( + input: any, + fn: (input: Record, key?: string) => any, + refs = new WeakMap(), + key?: string +) { + if (typeof input === 'object' && input !== null) { + const ref = refs.get(input) + if (ref) { + return ref + } + const n: Record = Array.isArray(input) ? [] : {} + refs.set(input, n) + for (const i in input) { + if (Object.prototype.hasOwnProperty.call(input, i)) { + n[i] = deepMap(input[i], fn, refs, i) + } + } + return n + } + return fn(input, key) +} diff --git a/src/main/ts/utils/index.ts b/src/main/ts/utils/index.ts new file mode 100644 index 0000000..ee6eece --- /dev/null +++ b/src/main/ts/utils/index.ts @@ -0,0 +1 @@ +export { deepMap } from './deepmap' diff --git a/src/test/ts/pipes/http.ts b/src/test/ts/pipes/http.ts index 8fc3261..11381a4 100644 --- a/src/test/ts/pipes/http.ts +++ b/src/test/ts/pipes/http.ts @@ -15,9 +15,9 @@ describe('httpPipe', () => { it('returns remote data if succeeds', () => { const httpPipe = createHttpPipe({ url: 'https://reqres.in/api/users/2', method: HttpMethod.GET }) - const transittable: ITransmittable = { data: null, meta: { history: [] } } + const transmittable: ITransmittable = { data: null, meta: { history: [] } } - return expect(httpPipe.execute(transittable, noop)) + return expect(httpPipe.execute(transmittable, noop)) .resolves.toEqual([null, { data: { id: 2, diff --git a/src/test/ts/pipes/index.ts b/src/test/ts/pipes/index.ts index 647d0d7..1b7dc52 100644 --- a/src/test/ts/pipes/index.ts +++ b/src/test/ts/pipes/index.ts @@ -48,13 +48,6 @@ describe('pipes', () => { } } - /* const pipeline1: TPipeline = [foo, [upper]] - - const [err, data] = await execute(createTransmittable({ - message: 'bar' - }), pipeline1) - console.log(err, data) */ - const pipeline2: TPipeline = [upper, foo] const [err, data] = await execute(createTransmittable({ diff --git a/src/test/ts/pipes/masker.ts b/src/test/ts/pipes/masker.ts new file mode 100644 index 0000000..c405f82 --- /dev/null +++ b/src/test/ts/pipes/masker.ts @@ -0,0 +1,47 @@ +import { + createMaskerPipe, + panMaskerPipe, + ITransmittable +} from '../../../main/ts' + +const noop = () => { /* noop */ } + +describe('maskerPipe', () => { + it('factory returns IPipe', () => { + const maskerPipe = createMaskerPipe(el => el) + + expect(maskerPipe.type).toBe('masker') + expect(maskerPipe.execute).toEqual(expect.any(Function)) + }) + + it('return masked elements', async () => { + const maskerPipe = createMaskerPipe(el => el.toString() + 'masked') + const transmittable: ITransmittable = { + data: ['foo', 'bar', ['foo2', ['foo3']]], + meta: { history: [] } + } + expect(await maskerPipe.execute(transmittable, noop)).toStrictEqual([ + null, + ['foomasked', 'barmasked', ['foo2masked', ['foo3masked']]] + ]) + }) + + it('return masked elements', async () => { + const transmittable: ITransmittable = { + data: [ + '4111111111111111', + 'bar', + ['4111111111111111', ['foo3', '0000000000000000']] + ], + meta: { history: [] } + } + expect(await panMaskerPipe.execute(transmittable, noop)).toStrictEqual([ + null, + [ + '4111 **** **** 1111', + 'bar', + ['4111 **** **** 1111', ['foo3', '0000 **** **** 0000']] + ] + ]) + }) +}) diff --git a/src/test/ts/utils/deepmap.ts b/src/test/ts/utils/deepmap.ts new file mode 100644 index 0000000..0ad39d4 --- /dev/null +++ b/src/test/ts/utils/deepmap.ts @@ -0,0 +1,51 @@ +import { deepMap } from '../../../main/ts/utils' + +describe('deepMap', () => { + it('handle object', () => { + const testObj = { + a: 1, + b: [ + 1, + 2, + { + c: 4, + d: { + e: 6, + f: [7, 8] + } + } + ] + } + + expect(deepMap(testObj, el => Number(el) * 10)).toMatchObject({ + a: 10, + b: [ + 10, + 20, + { + c: 40, + d: { + e: 60, + f: [70, 80] + } + } + ] + }) + }) + + it('handle circular deps', () => { + const testObj: Record = { + a: 1, + b: 2 + } + testObj.foo = testObj + + const resObj: Record = { + a: 10, + b: 20 + } + resObj.foo = resObj + + expect(deepMap(testObj, el => Number(el) * 10)).toMatchObject(resObj) + }) +}) diff --git a/tsconfig.json b/tsconfig.json index 4068ce1..004f828 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,7 +22,12 @@ "esModuleInterop": true, "incremental": true, - "tsBuildInfoFile": "./buildcache/.tsbuildinfo" + "tsBuildInfoFile": "./buildcache/.tsbuildinfo", + "typeRoots": [ + "./typings", + "customTypings", + "node_modules/@types" + ] }, "include": [ "src/main/**/*" diff --git a/yarn.lock b/yarn.lock index a349c35..f5d0226 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2749,6 +2749,11 @@ fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fast-luhn@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/fast-luhn/-/fast-luhn-1.0.4.tgz#7de2df0c6f868e4d23b4f3a162633d81fc5a50c1" + integrity sha512-TukISTtdVBSykwFKdXWHNRQWaezelkjpyx+pDkTOKM9r5vnYUlcblYpsA5GLfNpqSj9gXEOVNA43OwqPDK/7Og== + fastq@^1.6.0: version "1.6.1" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.6.1.tgz#4570c74f2ded173e71cf0beb08ac70bb85826791"