ffc-pay-etl-framework 1.4.2-alpha.6 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/.npmignore +26 -0
  2. package/app/database-connections/index.js +7 -0
  3. package/app/database-connections/postgres-database-connection.js +47 -0
  4. package/app/database-connections/provided-connection.js +19 -0
  5. package/app/destinations/README.md +107 -0
  6. package/app/destinations/console-destination.js +38 -0
  7. package/app/destinations/csv-file-destination.js +94 -0
  8. package/app/destinations/index.js +12 -0
  9. package/app/destinations/postgres-destination.js +141 -0
  10. package/app/destinations/sql-file-destination.js +68 -0
  11. package/app/lib/index.js +103 -0
  12. package/app/lib/row-meta-data.js +24 -0
  13. package/app/loaders/csv-loader.js +58 -0
  14. package/app/loaders/index.js +5 -0
  15. package/app/misc/README.md +55 -0
  16. package/app/misc/index.js +5 -0
  17. package/app/misc/postgres-sql-task.js +65 -0
  18. package/app/transformers/README.md +102 -0
  19. package/app/transformers/faker-transformer.js +48 -0
  20. package/app/transformers/index.js +9 -0
  21. package/app/transformers/string-replace-transformer.js +38 -0
  22. package/app/transformers/to-upper-case-transformer.js +29 -0
  23. package/app/validators/README.md +81 -0
  24. package/app/validators/index.js +9 -0
  25. package/app/validators/multi-tool-validator.js +50 -0
  26. package/app/validators/required-validator.js +35 -0
  27. package/app/validators/unique-validator.js +46 -0
  28. package/azure-pipelines.yml +45 -0
  29. package/examples/additional-tasks.js +127 -0
  30. package/examples/csv-faker.js +30 -0
  31. package/examples/csv-find-replace.js +97 -0
  32. package/examples/csv-multi-tool-validator.js +38 -0
  33. package/examples/csv-required-validator.js +27 -0
  34. package/examples/csv-to-csv.js +24 -0
  35. package/examples/csv-to-postgres.js +36 -0
  36. package/examples/csv-to-sqlfile.js +31 -0
  37. package/examples/csv-to-upper-case.js +25 -0
  38. package/examples/csv-unique-validator.js +27 -0
  39. package/examples/index.js +26 -0
  40. package/examples/sql-return-values.js +37 -0
  41. package/index.js +3 -1
  42. package/jest.config.js +42 -0
  43. package/jest.setup.js +2 -0
  44. package/package-lock.json +7992 -0
  45. package/package.json +22 -66
@@ -0,0 +1,58 @@
1
+ // @ts-nocheck
2
+ const fs = require('node:fs')
3
+ const { Transform } = require('node:stream')
4
+ const { parse } = require('csv-parse')
5
+
6
+ /**
7
+ *
8
+ * @param {Object} options
9
+ * @param {String} [options.path]
10
+ * @param {ReadableStream} [options.stream]
11
+ * @param {Number} [options.startingLine]
12
+ * @param {Boolean} [options.relax]
13
+ * @returns StreamReader
14
+ */
15
+ function csvLoader (options) {
16
+ let loader
17
+ if (options.path) {
18
+ loader = fs.createReadStream(options.path)
19
+ } else {
20
+ loader = options.stream
21
+ }
22
+ let lineCount = 1
23
+ const fromLine = options.startingLine ?? 2
24
+ const relaxQuotes = options.relax ?? false
25
+ loader._columns = options.columns
26
+ loader.pump = () => {
27
+ const parser = parse({ delimiter: ',', from_line: fromLine, relax_quotes: relaxQuotes })
28
+ const transformer = new Transform({
29
+ readableObjectMode: true,
30
+ writableObjectMode: true,
31
+ emitClose: true,
32
+ transform (chunk, _, callback) {
33
+ chunk['_columns'] = options.columns
34
+ chunk['_linecount'] = lineCount
35
+ lineCount += 1
36
+
37
+ // remove non-printable characters
38
+ options.columns.forEach((_column, index) => {
39
+ if (chunk[index]) {
40
+ // eslint-disable-next-line no-control-regex
41
+ chunk[index] = chunk[index].replaceAll(/[\x00-\x1F\x7F-\x9F]/g, '')
42
+ }
43
+ })
44
+
45
+ callback(null, chunk)
46
+ }
47
+ })
48
+ return loader
49
+ .pipe(parser)
50
+ .pipe(transformer)
51
+ }
52
+
53
+ return loader
54
+ }
55
+
56
+ module.exports = {
57
+ CSVLoader: csvLoader
58
+ }
@@ -0,0 +1,5 @@
1
+ const { CSVLoader } = require('./csv-loader')
2
+
3
+ module.exports = {
4
+ CSVLoader
5
+ }
@@ -0,0 +1,55 @@
1
+ # Miscellanous pipeline items
2
+
3
+ ## PostgresSQLTask
4
+
5
+ Executes an arbitrary sql task either as part of another transformation or as an event
6
+
7
+ ### Example
8
+
9
+ ```js
10
+ const {
11
+ Connections,
12
+ Destinations,
13
+ Etl,
14
+ Loaders,
15
+ Misc,
16
+ Validators,
17
+ Transformers,
18
+ } = require("ffc-pay-etl-framework");
19
+
20
+ const etl = new Etl.Etl();
21
+
22
+ etl
23
+ .connections(
24
+ new Connections.PostgresSQLConnection({
25
+ connectionname: "MyConnection",
26
+ database: "MyDB",
27
+ username: "MyUserName",
28
+ password: "MyPassword",
29
+ host: "localhost",
30
+ port: 5432,
31
+ })
32
+ )
33
+ .beforeETL(
34
+ new Misc.PostgresSQLTask({
35
+ connection: "MyConnection",
36
+ sql: "INSERT INTO MyTable (column1, column2) VALUES ('apples','oranges');",
37
+ })
38
+ )
39
+ .loader(new Loaders.CSVLoader({ path: csvFile, columns: columns }))
40
+ .destination(
41
+ new Destinations.PostgresDestination({
42
+ connection: "MyConnection",
43
+ table: "target",
44
+ mapping: [
45
+ {
46
+ column: "Dist Code",
47
+ targetColumn: "dist_code",
48
+ targetType: "string",
49
+ },
50
+ ],
51
+ includeErrors: false,
52
+ })
53
+ )
54
+ .pump();
55
+ ```
@@ -0,0 +1,5 @@
1
+ const { PostgresSQLTask } = require('./postgres-sql-task')
2
+
3
+ module.exports = {
4
+ PostgresSQLTask
5
+ }
@@ -0,0 +1,65 @@
1
+ // @ts-nocheck
2
+ const { PassThrough } = require('node:stream')
3
+ const startPosOffset = 3
4
+ const endPosOffset = 1
5
+
6
+ function getPlaceHolders (sql) {
7
+ const columnNamePlaceholderRegex = /(?<=\$\{)(.*?)(?=})/g
8
+ const matches = Array.from(sql.matchAll(columnNamePlaceholderRegex))
9
+ return matches.map(match => ({
10
+ match: match[0],
11
+ value: match[0].split('.')[1],
12
+ collection: match[0].split('.')[0],
13
+ startPos: match.index - startPosOffset,
14
+ endPos: match.index + match[0].length - endPosOffset
15
+ }))
16
+ }
17
+
18
+ function doPlaceHolderValueInterpolations (chunk, sql, placeholders) {
19
+ placeholders.forEach(placeholder => {
20
+ sql = sql.replace(new RegExp(`\\$\{${placeholder.match}}`),
21
+ chunk[chunk[`_${placeholder.collection}`].indexOf(placeholder.value)])
22
+ })
23
+ return sql
24
+ }
25
+
26
+ function postgresSQLTask (options) {
27
+ const passthrough = new PassThrough({
28
+ readableObjectMode: true,
29
+ writableObjectMode: true,
30
+ decodeStrings: false,
31
+ construct (callback) {
32
+ this.sql = options.sql
33
+ this.connectionname = options.connectionname
34
+ callback()
35
+ },
36
+ transform (chunk, _, callback) {
37
+ const placeholders = getPlaceHolders(this.sql)
38
+ if (placeholders.length === 0) {
39
+ this.connection.db.query(this.sql)
40
+ } else {
41
+ const interpolatedSql = doPlaceHolderValueInterpolations(chunk, this.sql, placeholders)
42
+ this.connection.db.query(interpolatedSql)
43
+ }
44
+ callback(null, chunk)
45
+ }
46
+ })
47
+ Object.assign(passthrough, {
48
+ setConnection: function (connection) {
49
+ this.connection = connection
50
+ }.bind(passthrough),
51
+ getConnectionName: function () {
52
+ return this.connection.name
53
+ }.bind(passthrough),
54
+ setETL: function (etl) {
55
+ this.etl = etl
56
+ }.bind(passthrough)
57
+ })
58
+ return passthrough
59
+ }
60
+
61
+ module.exports = {
62
+ PostgresSQLTask: postgresSQLTask,
63
+ getPlaceHolders,
64
+ doPlaceHolderValueInterpolations
65
+ }
@@ -0,0 +1,102 @@
1
+ # Transformers
2
+
3
+ Transformers manipulate row data and either replace the existing values or add new columns to each row depending on the type of transformer and the configuration.
4
+
5
+ Currently supported transformers are :
6
+
7
+ - ToUpperCaseTransformer
8
+ - FakerTransformer
9
+
10
+ ## FakerTransformer
11
+
12
+ ### Options
13
+
14
+ | option | description |
15
+ | ------- | ------------------------------------------------------------ |
16
+ | columns | Array of column transformations |
17
+ | locale | See https://fakerjs.dev/guide/localization for valid locales |
18
+
19
+ ### Usage
20
+
21
+ ```js
22
+ FakerTransformer({
23
+ columns: [
24
+ {
25
+ name: "column2",
26
+ faker: "company.name",
27
+ },
28
+ ],
29
+ });
30
+ ```
31
+
32
+ faker is a string value that can be any of the currently supported [apis](https://fakerjs.dev/api/)
33
+
34
+ e.g.
35
+
36
+ - finance.accountName
37
+ - person.firstName
38
+ - randomizer.next
39
+
40
+ To create UK post code
41
+
42
+ ```js
43
+ FakerTransformer({
44
+ columns: [
45
+ {
46
+ name: "column2",
47
+ faker: "location.zipCode",
48
+ },
49
+ ],
50
+ locale: "en_GB",
51
+ });
52
+ ```
53
+
54
+ ### Example
55
+
56
+ examples/csv-faker.js
57
+
58
+ ## ToUpperCaseTransformer
59
+
60
+ ### Options
61
+
62
+ | option | description |
63
+ | ------ | ------------------------------- |
64
+ | column | source column to be transformed |
65
+
66
+ ### Usage
67
+
68
+ ```js
69
+ ToUpperCaseTransformer({
70
+ column: "column2",
71
+ });
72
+ ```
73
+
74
+ ### Example
75
+
76
+ examples/csv-to-upper-case.js
77
+
78
+ ## StringReplaceTransformer
79
+
80
+ ### Options
81
+
82
+ | option | description |
83
+ | ------- | --------------------------------------- |
84
+ | column | source column to be transformed |
85
+ | find | search string to be replaced |
86
+ | replace | value to replace the search string with |
87
+
88
+ ### Usage
89
+
90
+ ```js
91
+ StringReplaceTransformer([
92
+ {
93
+ column: "column2",
94
+ find: "'",
95
+ replace: "''",
96
+ },
97
+ ]);
98
+ ```
99
+
100
+ ### Example
101
+
102
+ examples/csv-find-replace.js
@@ -0,0 +1,48 @@
1
+ const { Transform } = require('node:stream')
2
+
3
+ /**
4
+ *
5
+ * @param {Object} options
6
+ * @param {Array} options.columns {
7
+ * columns: [
8
+ * {
9
+ * name: "column-name",
10
+ * faker: "company.name"
11
+ * }
12
+ * ]
13
+ * }
14
+ * @param {String} options.locale
15
+ * @returns StreamReader
16
+ */
17
+ function fakerTransformer (options) {
18
+ const columns = options.columns
19
+ let faker
20
+ if (options.locale) {
21
+ faker = require(`@faker-js/faker/locale/${options.locale}`).faker
22
+ } else {
23
+ faker = require('@faker-js/faker').faker
24
+ }
25
+
26
+ function getFaker (fakerType) {
27
+ return fakerType.split('.').reduce((a, b) => {
28
+ return Object.keys(a).length === 0 ? faker[b] : a[b]
29
+ }, {})
30
+ }
31
+
32
+ return new Transform({
33
+ readableObjectMode: true,
34
+ writableObjectMode: true,
35
+ transform (chunk, _, callback) {
36
+ const { _columns } = chunk
37
+ columns.forEach(column => {
38
+ const colIndex = _columns.indexOf(column.name)
39
+ chunk[colIndex] = getFaker(column.faker)()
40
+ })
41
+ callback(null, chunk)
42
+ }
43
+ })
44
+ }
45
+
46
+ module.exports = {
47
+ FakerTransformer: fakerTransformer
48
+ }
@@ -0,0 +1,9 @@
1
+ const { ToUpperCaseTransformer } = require('./to-upper-case-transformer')
2
+ const { FakerTransformer } = require('./faker-transformer')
3
+ const { StringReplaceTransformer } = require('./string-replace-transformer')
4
+
5
+ module.exports = {
6
+ StringReplaceTransformer,
7
+ ToUpperCaseTransformer,
8
+ FakerTransformer
9
+ }
@@ -0,0 +1,38 @@
1
+ const { Transform } = require('node:stream')
2
+
3
+ /**
4
+ *
5
+ * @param {Object} options
6
+ * @param {String} options.column
7
+ * @returns Writable
8
+ */
9
+ function stringReplaceTransformer (options) {
10
+ const replacements = options
11
+
12
+ return new Transform({
13
+ readableObjectMode: true,
14
+ writableObjectMode: true,
15
+ transform (chunk, _, callback) {
16
+ const { _columns } = chunk
17
+ // @ts-ignore
18
+ replacements.forEach(r => {
19
+ const colIndex = _columns.indexOf(r.column)
20
+ if (colIndex === -1) {
21
+ // column not found, skip
22
+ return
23
+ }
24
+ if (r.all) {
25
+ chunk[colIndex] = chunk[colIndex].replaceAll(r.find, r.replace)
26
+ } else {
27
+ chunk[colIndex] = chunk[colIndex].replace(r.find, r.replace)
28
+ }
29
+ })
30
+
31
+ callback(null, chunk)
32
+ }
33
+ })
34
+ }
35
+
36
+ module.exports = {
37
+ StringReplaceTransformer: stringReplaceTransformer
38
+ }
@@ -0,0 +1,29 @@
1
+ const { Transform } = require('node:stream')
2
+
3
+ /**
4
+ *
5
+ * @param {Object} options
6
+ * @param {String} options.column
7
+ * @returns Writable
8
+ */
9
+ function toUpperCaseTransformer (options) {
10
+ const column = options.column
11
+
12
+ return new Transform({
13
+ readableObjectMode: true,
14
+ writableObjectMode: true,
15
+ transform (chunk, _, callback) {
16
+ const { _columns } = chunk
17
+ const colIndex = _columns.indexOf(column)
18
+
19
+ if (colIndex !== -1 && chunk[colIndex] != null) {
20
+ chunk[colIndex] = chunk[colIndex].toUpperCase()
21
+ }
22
+ callback(null, chunk)
23
+ }
24
+ })
25
+ }
26
+
27
+ module.exports = {
28
+ ToUpperCaseTransformer: toUpperCaseTransformer
29
+ }
@@ -0,0 +1,81 @@
1
+ # Validators
2
+
3
+ Validators are expected to check specific columns within each row against specific validation rules
4
+
5
+ Currently supported destinations are :
6
+
7
+ - RequiredValidator
8
+ - UniqueValidator
9
+ - MultiToolValidator
10
+
11
+ ## RequiredValidator
12
+
13
+ ### Options
14
+
15
+ | option | description |
16
+ | -------------- | ------------------------------------------------------------- |
17
+ | columns | Array of column indexes |
18
+
19
+ ### Usage
20
+
21
+ ```js
22
+ Validators.RequiredValidator({
23
+ columns: [0]
24
+ })
25
+ ```
26
+
27
+ ### Example
28
+
29
+ examples/csv-required-validator.js
30
+
31
+ ## UniqueValidator
32
+
33
+ ### Options
34
+
35
+ | option | description |
36
+ | -------------- | ------------------------------------------------------------- |
37
+ | column | Column to be tested for uniqueness |
38
+
39
+ ### Usage
40
+
41
+ ```js
42
+ Validators.UniqueValidator(
43
+ { column: "field_one" }
44
+ )
45
+ ```
46
+
47
+ ### Example
48
+
49
+ examples/csv-unique-validator.js
50
+
51
+ ## MultiToolValidator
52
+
53
+ ### Options
54
+
55
+ | option | description |
56
+ | -------------- | ------------------------------------------------------------- |
57
+ | column | Column to be tested for validity and validator specification |
58
+
59
+ NB Any validators specified [here](https://github.com/validatorjs/validator.js) can be used
60
+
61
+ ### Usage
62
+
63
+ ```js
64
+ Validators.MultiToolValidator( { columns: [
65
+ {
66
+ name: "SOIL TYPE PERCENT (Percent)",
67
+ validator: "isAlphanumeric",
68
+ args: {
69
+ locale: "en-GB",
70
+ options: {
71
+ ignore: "-;%/ "
72
+ }
73
+ }
74
+ }
75
+ ]
76
+ })
77
+ ```
78
+
79
+ ### Example
80
+
81
+ examples/csv-multi-tool-validator.js
@@ -0,0 +1,9 @@
1
+ const { RequiredValidator } = require('./required-validator')
2
+ const { UniqueValidator } = require('./unique-validator')
3
+ const { MultiToolValidator } = require('./multi-tool-validator')
4
+
5
+ module.exports = {
6
+ RequiredValidator,
7
+ UniqueValidator,
8
+ MultiToolValidator
9
+ }
@@ -0,0 +1,50 @@
1
+ // @ts-nocheck
2
+ const { Transform } = require('node:stream')
3
+ const validator = require('validator')
4
+
5
+ function getValidator (validatorType) {
6
+ return validator[validatorType]
7
+ }
8
+
9
+ /**
10
+ *
11
+ * @param {Object} options
12
+ * @param {Array} options.columns { columns: [
13
+ * {
14
+ * name: "column-name",
15
+ * validator: "isAlphanumeric",
16
+ * args: {
17
+ * locale: "en-GB",
18
+ * options: {
19
+ * ignore: "-;%/ "
20
+ * }
21
+ * }
22
+ * }
23
+ * ]
24
+ * }
25
+ * @returns Writable
26
+ */
27
+ function multiToolValidator (options) {
28
+ return new Transform({
29
+ readableObjectMode: true,
30
+ writableObjectMode: true,
31
+ decodeStrings: false,
32
+ construct (callback) {
33
+ this.columns = options.columns
34
+ callback()
35
+ },
36
+ transform (chunk, _, callback) {
37
+ const { _columns } = chunk
38
+ this.columns.forEach(column => {
39
+ const colIndex = _columns.indexOf(column.name)
40
+ const vtr = getValidator(column.validator)
41
+ if (!vtr(chunk[colIndex], ...Object.values(column.args))) { chunk.errors.push(`${column.validator} returned invalid status in column ${colIndex}`) }
42
+ })
43
+ callback(null, chunk)
44
+ }
45
+ })
46
+ }
47
+
48
+ module.exports = {
49
+ MultiToolValidator: multiToolValidator
50
+ }
@@ -0,0 +1,35 @@
1
+ // @ts-nocheck
2
+ const { Transform } = require('node:stream')
3
+
4
+ /**
5
+ *
6
+ * @param {Object} options
7
+ * @param {String} options.message
8
+ * @param {Array} options.columns
9
+ * @returns Transform
10
+ */
11
+ function requiredValidator (options) {
12
+ return new Transform({
13
+ readableObjectMode: true,
14
+ writableObjectMode: true,
15
+ decodeStrings: false,
16
+ construct (callback) {
17
+ this.message = options.message ? options.message : 'Required value is null'
18
+ this.requiredColumns = options.columns
19
+ callback()
20
+ },
21
+ transform (chunk, _, callback) {
22
+ this.requiredColumns.forEach(column => {
23
+ const requiredVal = chunk[column]
24
+ if (requiredVal === undefined || requiredVal === '') {
25
+ chunk.errors.push(`${this.message} in column ${column}`)
26
+ }
27
+ })
28
+ callback(null, chunk)
29
+ }
30
+ })
31
+ }
32
+
33
+ module.exports = {
34
+ RequiredValidator: requiredValidator
35
+ }
@@ -0,0 +1,46 @@
1
+ const { Transform } = require('node:stream')
2
+
3
+ /**
4
+ *
5
+ * @param {Object} options
6
+ * @param {String} options.message
7
+ * @param {String} options.column
8
+ * @returns Transform
9
+ */
10
+ function uniqueValidator (options) {
11
+ const column = options.column
12
+ const message = options.message || 'Expected unique value is not unique'
13
+ const uniqueColumnRows = new Set()
14
+
15
+ function checkIsUnique (data) {
16
+ return !uniqueColumnRows.has(data)
17
+ }
18
+
19
+ return new Transform({
20
+ readableObjectMode: true,
21
+ writableObjectMode: true,
22
+ decodeStrings: false,
23
+ construct (callback) {
24
+ callback()
25
+ },
26
+ transform (chunk, _, callback) {
27
+ const { _columns } = chunk
28
+ const colIndex = _columns.indexOf(column)
29
+ const uniqueValue = chunk[colIndex]
30
+
31
+ if (checkIsUnique(uniqueValue)) {
32
+ uniqueColumnRows.add(uniqueValue)
33
+ } else {
34
+ if (!chunk.errors) {
35
+ chunk.errors = []
36
+ }
37
+ chunk.errors.push(`${message} ${column}`)
38
+ }
39
+ callback(null, chunk)
40
+ }
41
+ })
42
+ }
43
+
44
+ module.exports = {
45
+ UniqueValidator: uniqueValidator
46
+ }
@@ -0,0 +1,45 @@
1
+ # Pipeline name convention
2
+ name: '$(Build.SourceBranchName)$(System.PullRequest.SourceBranch)$(Rev:.r)'
3
+
4
+ # Trigger configuration
5
+ trigger:
6
+ - main
7
+ - feature/*
8
+ - release/*
9
+
10
+ pr:
11
+ - main
12
+ - releases/*
13
+
14
+ # Parameters
15
+ parameters:
16
+ - name: prDeployment
17
+ type: boolean
18
+ default: true
19
+ - name: branchDeployment
20
+ type: boolean
21
+ default: true
22
+ - name: sndDeployment
23
+ type: boolean
24
+ default: true
25
+
26
+ # Resources
27
+ resources:
28
+ repositories:
29
+ - repository: templates
30
+ endpoint: DEFRA
31
+ type: github
32
+ name: defra/fcp-pipeline-common
33
+ # ref: pl-709 # Optional: build specific pipeline branch
34
+
35
+ # Pipeline template
36
+ extends:
37
+ template: fcp-common-pipeline.yaml@templates
38
+ parameters:
39
+ appFrameworkType: nodejs # Optional: nodejs (default), dotnet, java
40
+ Type: npm # Optional: kubernetes (default), npm, functions, nuget
41
+ npmLegacyPeerDeps: false # Optional: default is false
42
+ failOnThreshold: moderate # Optional: moderate (default), low, high, critical
43
+ prDeployment: ${{ parameters.prDeployment }}
44
+ branchDeployment: ${{ parameters.branchDeployment }}
45
+ sndDeployment: ${{ parameters.sndDeployment }}