@emartech/program-executor 3.11.0 → 3.12.0

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 (30) hide show
  1. package/.env.example +9 -1
  2. package/.github/CODEOWNERS +1 -1
  3. package/.github/dependabot.yml +0 -7
  4. package/.github/workflows/main.yaml +17 -5
  5. package/docker-compose.yml +11 -0
  6. package/eslint.config.js +1 -5
  7. package/jest.config.js +13 -0
  8. package/package.json +13 -16
  9. package/src/create-test-databases.js +84 -0
  10. package/src/execution-time-exceeded-error/{index.spec.js → index.test.js} +3 -3
  11. package/src/graphql/{schema.spec.js → schema.test.js} +69 -24
  12. package/src/ignorable-error/{index.spec.js → index.test.js} +5 -5
  13. package/src/index-pubsub-e2e.test.js +94 -0
  14. package/src/index-pubsub.js +78 -0
  15. package/src/index-pubsub.test.js +154 -0
  16. package/src/{index.spec.js → index.test.js} +42 -38
  17. package/src/job-data-handler/{index.spec.js → index.test.js} +15 -21
  18. package/src/program-executor-processor/{index.spec.js → index.test.js} +62 -49
  19. package/src/program-handler/index.js +7 -1
  20. package/src/program-handler/{index.spec.js → index.test.js} +58 -34
  21. package/src/queue-manager/{index.spec.js → index.test.js} +16 -19
  22. package/src/queue-manager-pubsub/index.js +33 -0
  23. package/src/queue-manager-pubsub/index.test.js +44 -0
  24. package/src/repositories/{programs.spec.js → programs.test.js} +54 -37
  25. package/src/retryable-error/{index.spec.js → index.test.js} +5 -5
  26. package/src/runid-generator/{index.spec.js → index.test.js} +2 -2
  27. package/src/test-helper/get-test-db-config.js +29 -0
  28. package/src/testSetup.js +23 -0
  29. package/src/testTeardown.js +13 -0
  30. package/src/setup.spec.js +0 -50
package/.env.example CHANGED
@@ -1,3 +1,11 @@
1
1
  DEBUG=program-executor*
2
2
 
3
- DATABASE_URL=postgres://developer:development_secret@localhost:5435/programexecutor
3
+ POSTGRES_USER=developer
4
+ POSTGRES_HOST=localhost
5
+ POSTGRES_PASSWORD=development_secret
6
+ POSTGRES_PORT=5435
7
+ POSTGRES_DATABASE=programexecutor
8
+
9
+ PUBSUB_EMULATOR_HOST=0.0.0.0:8085
10
+ PUBSUB_PROJECT_ID=some-project-id
11
+ PUBSUB_TIMEOUT=1000
@@ -1,4 +1,4 @@
1
1
  # Each line is a file pattern followed by one or more owners.
2
2
  # Order is important. The last matching pattern has the most precedence.
3
3
 
4
- *.js oliver.weisenburger@sap.com nikolay.dimitrov@sap.com ian.helmrich@sap.com mauro.greco@sap.com
4
+ * @emartech/octopus-shopify
@@ -19,13 +19,6 @@ updates:
19
19
  interval: "weekly"
20
20
  day: "monday"
21
21
  time: "09:00"
22
- # Add assignees
23
- reviewers:
24
- - "IvanFroehlich"
25
- - "MauroGreco"
26
- - "oliverweisenburger"
27
- - "dimirovn"
28
- - "ianhelmrich"
29
22
  commit-message:
30
23
  # Prefix all commit messages with "npm: "
31
24
  prefix: "[dependabot]npm"
@@ -11,8 +11,6 @@ env:
11
11
  jobs:
12
12
  test:
13
13
  runs-on: ubuntu-latest
14
- container: node:18-alpine3.16
15
-
16
14
  services:
17
15
  postgres:
18
16
  image: postgres:14-alpine
@@ -29,12 +27,19 @@ jobs:
29
27
  - 5432:5432
30
28
 
31
29
  steps:
30
+ - name: 'Install Cloud SDK'
31
+ uses: google-github-actions/setup-gcloud@v2.1.4
32
+ with:
33
+ install_components: 'beta,pubsub-emulator'
34
+ - name: 'start pubsub emulator'
35
+ run: |
36
+ gcloud beta emulators pubsub start --project=some-project-id --host-port=0.0.0.0:8085 &
32
37
  - name: Check out repository
33
38
  uses: actions/checkout@v4
34
- - name: Use newest Node version
39
+ - name: Use Node 20.18.0 version
35
40
  uses: actions/setup-node@v3
36
41
  with:
37
- node-version: "lts/*"
42
+ node-version: "20.18.0"
38
43
  - name: Set NPM token
39
44
  run: npm config set '//registry.npmjs.org/:_authToken' "${{ env.NPM_TOKEN }}"
40
45
  - name: npm dependencies
@@ -42,7 +47,14 @@ jobs:
42
47
  - name: npm test
43
48
  run: npm test
44
49
  env:
45
- DATABASE_URL: 'postgres://developer:development_secret@postgres:5432/programexecutor'
50
+ POSTGRES_USER: developer
51
+ POSTGRES_HOST: localhost
52
+ POSTGRES_PASSWORD: development_secret
53
+ POSTGRES_PORT: 5432
54
+ POSTGRES_DATABASE: programexecutor
55
+ PUBSUB_EMULATOR_HOST: 0.0.0.0:8085
56
+ PUBSUB_PROJECT_ID: some-project-id
57
+ PUBSUB_TIMEOUT: 2000
46
58
 
47
59
  deploy:
48
60
  name: deploy
@@ -7,3 +7,14 @@ services:
7
7
  - POSTGRES_DB=programexecutor
8
8
  ports:
9
9
  - 5435:5432
10
+ pub-sub-emulator:
11
+ image: google/cloud-sdk:526.0.0-emulators
12
+ command: ["gcloud", "beta", "emulators", "pubsub", "start", "--host-port=0.0.0.0:8085", "--project=some-project-id"]
13
+ ports:
14
+ - "8085:8085"
15
+ healthcheck:
16
+ test: ["CMD", "curl", "-f", "http://localhost:8085/v1/projects/test/schemas"]
17
+ interval: 1m30s
18
+ timeout: 10s
19
+ retries: 3
20
+ start_period: 40s
package/eslint.config.js CHANGED
@@ -14,14 +14,10 @@ const compat = new FlatCompat({
14
14
  module.exports = [
15
15
  ...compat.extends('emarsys'),
16
16
  {
17
- plugins: {
18
- mocha
19
- },
20
-
21
17
  languageOptions: {
22
18
  globals: {
23
19
  ...globals.node,
24
- ...globals.mocha,
20
+ ...globals.jest,
25
21
  inject: true,
26
22
  onmessage: true,
27
23
  expect: true
package/jest.config.js ADDED
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ testEnvironment: 'node',
5
+ rootDir: './',
6
+ testPathIgnorePatterns: ['/node_modules/'],
7
+ testRegex: '\\.(test|e2e)\\.js$',
8
+ clearMocks: true,
9
+ collectCoverage: false,
10
+ coverageReporters: ['json', 'html', 'text', 'lcov'],
11
+ globalSetup: "./src/testSetup.js",
12
+ globalTeardown: './src/testTeardown.js',
13
+ };
package/package.json CHANGED
@@ -3,7 +3,9 @@
3
3
  "description": "",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
- "test": "mocha --exit --reporter spec 'src/**/*.spec.js'",
6
+ "test": "npm run create-test-dbs && jest",
7
+ "jest-watch": "jest --watch",
8
+ "create-test-dbs": "NODE_ENV=test node ./src/create-test-databases.js",
7
9
  "code-style": "eslint '**/*.js' --ignore-pattern node_modules/",
8
10
  "semantic-release": "semantic-release --branches main"
9
11
  },
@@ -17,30 +19,25 @@
17
19
  "author": "team-shopify@emarsys.com",
18
20
  "license": "ISC",
19
21
  "dependencies": {
20
- "@emartech/rabbitmq-client": "^3.5.2",
22
+ "@emartech/json-logger": "3.4.0",
23
+ "@emartech/pubsub-client-js": "^1.2.5",
24
+ "@emartech/rabbitmq-client": "5.9.1",
21
25
  "camelcase-keys": "^6.2.2",
22
- "graphql": "^15.5.0"
26
+ "graphql": "^16.11.0"
23
27
  },
24
28
  "engines": {
25
29
  "node": "20.18.0"
26
30
  },
27
31
  "devDependencies": {
28
- "chai": "^4.5.0",
29
- "chai-as-promised": "7.1.1",
30
- "chai-string": "1.5.0",
31
- "chai-subset": "1.6.0",
32
- "dotenv": "^16.4.7",
33
- "eslint": "^9.17.0",
32
+ "dotenv": "^17.2.1",
33
+ "eslint": "^9.34.0",
34
34
  "eslint-config-emarsys": "5.1.0",
35
- "eslint-plugin-mocha": "^10.5.0",
36
35
  "eslint-plugin-no-only-tests": "^3.3.0",
37
36
  "eslint-plugin-security": "^3.0.1",
37
+ "jest": "29.0.0",
38
38
  "knex": "^3.1.0",
39
- "mocha": "^11.0.1",
40
- "pg": "^8.13.1",
41
- "semantic-release": "^24.2.0",
42
- "sinon": "19.0.2",
43
- "sinon-chai": "^3.7.0"
39
+ "pg": "^8.16.3",
40
+ "semantic-release": "^24.2.0"
44
41
  },
45
- "version": "3.11.0"
42
+ "version": "3.12.0"
46
43
  }
@@ -0,0 +1,84 @@
1
+ 'use strict';
2
+
3
+ require('dotenv').config({ silent: true });
4
+
5
+ const os = require('os');
6
+
7
+ const { db } = require('./test-helper/get-test-db-config');
8
+
9
+ const { Client } = require('pg');
10
+
11
+ const cpuCount = os.cpus() ? os.cpus().length : 1;
12
+
13
+ const defaultDbName = db.connection.database;
14
+
15
+ const getConnection = function (databaseName) {
16
+ return {
17
+ ...db.connection,
18
+ database: databaseName
19
+ };
20
+ };
21
+
22
+ const recreateTestDbs = async () => {
23
+ const client = new Client(getConnection('template1'));
24
+
25
+ await client.connect();
26
+
27
+ for (let index = 0; index <= cpuCount; index++) {
28
+ console.log(`Creating test database ${index}`);
29
+ try {
30
+ const pgUser = process.env.POSTGRES_USER || 'developer';
31
+ await client.query(`DROP DATABASE IF EXISTS ${defaultDbName}_${index}`);
32
+ await client.query(`CREATE DATABASE ${defaultDbName}_${index} TEMPLATE ${defaultDbName}`);
33
+ await client.query(`ALTER DATABASE ${defaultDbName}_${index} OWNER TO ${pgUser}`);
34
+ } catch (error) {
35
+ console.log(error.message);
36
+ await client.end();
37
+ process.exit(1);
38
+ }
39
+ }
40
+ await client.end();
41
+ };
42
+
43
+ const getLatestMigrationId = async (databaseName) => {
44
+ const client = new Client(getConnection(databaseName));
45
+ await client.connect();
46
+
47
+ let result;
48
+
49
+ try {
50
+ result = await client.query('SELECT MAX(id) FROM knex_migrations');
51
+ } catch (error) {
52
+ console.log(`Migrations not found in ${databaseName ? databaseName : defaultDbName}...`);
53
+ await client.end();
54
+ throw error;
55
+ }
56
+
57
+ await client.end();
58
+ return result.rows[0].max;
59
+ };
60
+
61
+ const isRecreationNeeded = async () => {
62
+ try {
63
+ const [mainMigrationId, secondaryMigrationId] = await Promise.all([
64
+ getLatestMigrationId(defaultDbName),
65
+ getLatestMigrationId(`${defaultDbName}_${cpuCount}`)
66
+ ]);
67
+
68
+ return mainMigrationId !== secondaryMigrationId;
69
+ // eslint-disable-next-line no-unused-vars
70
+ } catch (error) {
71
+ return true;
72
+ }
73
+ };
74
+
75
+ (async () => {
76
+ const recreate = await isRecreationNeeded();
77
+
78
+ if (recreate) {
79
+ await recreateTestDbs();
80
+ } else {
81
+ console.log('Everything is fine, using previously generated dbs...');
82
+ }
83
+ process.exit(0);
84
+ })();
@@ -1,13 +1,13 @@
1
1
  'use strict';
2
2
 
3
- const ExecutionTimeExceededError = require('./');
3
+ const ExecutionTimeExceededError = require('.');
4
4
 
5
5
  describe('ExecutionTimeExceededError', () => {
6
6
  it('should have a executionTimeExceeded property set to true', () => {
7
7
  try {
8
8
  throw new ExecutionTimeExceededError();
9
9
  } catch (error) {
10
- expect(error.executionTimeExceeded).to.be.true;
10
+ expect(error.executionTimeExceeded).toBe(true);
11
11
  }
12
12
  });
13
13
 
@@ -15,7 +15,7 @@ describe('ExecutionTimeExceededError', () => {
15
15
  try {
16
16
  throw new ExecutionTimeExceededError('Something bad happened!');
17
17
  } catch (error) {
18
- expect(error.message).to.eql('Something bad happened!');
18
+ expect(error.message).toEqual('Something bad happened!');
19
19
  }
20
20
  });
21
21
  });
@@ -3,8 +3,28 @@
3
3
  const { graphql } = require('graphql');
4
4
  const ProgramsRepository = require('../repositories/programs');
5
5
  const schema = require('./schema');
6
+ const knex = require('knex');
7
+ const DbCleaner = require('../test-helper/db-cleaner');
8
+ const { db } = require('../test-helper/get-test-db-config');
6
9
 
7
10
  describe('schema', function () {
11
+
12
+ let dbConnection;
13
+
14
+ beforeAll(async function () {
15
+ dbConnection = knex({
16
+ client: 'pg',
17
+ connection: db.connection.connString
18
+ });
19
+ await DbCleaner.create(dbConnection).tearDown();
20
+ ProgramsRepository.create(dbConnection, 'programs');
21
+ });
22
+
23
+ afterAll(async function () {
24
+ await DbCleaner.create(dbConnection).tearDown();
25
+ dbConnection.destroy();
26
+ });
27
+
8
28
  describe('with empty database', () => {
9
29
  it('returns with empty array when table does not exist (error code 42P01)', async function () {
10
30
  const query = `
@@ -14,8 +34,9 @@ describe('schema', function () {
14
34
  }
15
35
  }`;
16
36
 
17
- const { data } = await graphql(schema, query, {}, { knex: this.db, tableName: 'programs' });
18
- expect(data).to.eql({ programs: [] });
37
+ const { data } = await graphql({ schema, source: query, contextValue: { knex: dbConnection,
38
+ tableName: 'programs' } });
39
+ expect(data).toEqual({ programs: [] });
19
40
  });
20
41
  });
21
42
 
@@ -48,8 +69,11 @@ describe('schema', function () {
48
69
  }
49
70
  ];
50
71
 
72
+
73
+
51
74
  beforeEach(async function () {
52
- const programsRepository = new ProgramsRepository(this.db, 'programs');
75
+ await DbCleaner.create(dbConnection).tearDown();
76
+ const programsRepository = ProgramsRepository.create(dbConnection, 'programs');
53
77
  for (const program of programs) {
54
78
  await programsRepository.save(program);
55
79
  }
@@ -73,33 +97,34 @@ describe('schema', function () {
73
97
  }
74
98
  }`;
75
99
 
76
- const { data } = await graphql(schema, query, {}, { knex: this.db, tableName: 'programs' });
100
+ const { data } = await graphql({ schema, source: query, contextValue: { knex: dbConnection,
101
+ tableName: 'programs' } });
77
102
 
78
- expect(data.programs[0]).to.containSubset({
103
+ expect(data.programs[0]).toEqual(expect.objectContaining({
79
104
  ...programs[0],
80
105
  jobData: JSON.stringify({}),
81
106
  programData: JSON.stringify(programs[0].programData)
82
- });
83
- expect(data.programs[0].createdAt).not.to.be.undefined;
84
- expect(data.programs[0].updatedAt).not.to.be.undefined;
107
+ }));
108
+ expect(data.programs[0].createdAt).not.toBe(undefined);
109
+ expect(data.programs[0].updatedAt).not.toBe(undefined);
85
110
 
86
- expect(data.programs[1]).to.containSubset({
111
+ expect(data.programs[1]).toEqual(expect.objectContaining({
87
112
  ...programs[1],
88
113
  jobData: JSON.stringify(programs[1].jobData),
89
114
  programData: JSON.stringify(programs[1].programData),
90
115
  erroredAt: programs[1].erroredAt.getTime().toString()
91
- });
92
- expect(data.programs[1].createdAt).not.to.be.undefined;
93
- expect(data.programs[1].updatedAt).not.to.be.undefined;
116
+ }));
117
+ expect(data.programs[1].createdAt).not.toBe(undefined);
118
+ expect(data.programs[1].updatedAt).not.toBe(undefined);
94
119
 
95
- expect(data.programs[2]).to.containSubset({
120
+ expect(data.programs[2]).toEqual(expect.objectContaining({
96
121
  ...programs[2],
97
122
  jobData: JSON.stringify(programs[2].jobData),
98
123
  programData: JSON.stringify(programs[2].programData),
99
124
  finishedAt: programs[2].finishedAt.getTime().toString()
100
- });
101
- expect(data.programs[2].createdAt).not.to.be.undefined;
102
- expect(data.programs[2].updatedAt).not.to.be.undefined;
125
+ }));
126
+ expect(data.programs[2].createdAt).not.toBe(undefined);
127
+ expect(data.programs[2].updatedAt).not.toBe(undefined);
103
128
  });
104
129
 
105
130
  describe('accepts orderBy input for `id` field', () => {
@@ -110,10 +135,15 @@ describe('schema', function () {
110
135
  }
111
136
  }`;
112
137
 
113
- const { data } = await graphql(schema, query, {}, { knex: this.db, tableName: 'programs' });
138
+ const { data } = await graphql({
139
+ schema,
140
+ source: query,
141
+ contextValue: { knex: dbConnection,
142
+ tableName: 'programs' }
143
+ });
114
144
  const ids = data.programs.map((program) => program.id);
115
145
 
116
- expect(ids).to.eql(['1', '2', '3']);
146
+ expect(ids).toEqual(['1', '2', '3']);
117
147
  });
118
148
 
119
149
  it('supports ordering by id DESC', async function () {
@@ -123,10 +153,15 @@ describe('schema', function () {
123
153
  }
124
154
  }`;
125
155
 
126
- const { data } = await graphql(schema, query, {}, { knex: this.db, tableName: 'programs' });
156
+ const { data } = await graphql({
157
+ schema,
158
+ source: query,
159
+ contextValue: { knex: dbConnection,
160
+ tableName: 'programs' }
161
+ });
127
162
  const ids = data.programs.map((program) => program.id);
128
163
 
129
- expect(ids).to.eql(['3', '2', '1']);
164
+ expect(ids).toEqual(['3', '2', '1']);
130
165
  });
131
166
  });
132
167
 
@@ -138,10 +173,15 @@ describe('schema', function () {
138
173
  }
139
174
  }`;
140
175
 
141
- const { data } = await graphql(schema, query, {}, { knex: this.db, tableName: 'programs' });
176
+ const { data } = await graphql({
177
+ schema,
178
+ source: query,
179
+ contextValue: { knex: dbConnection,
180
+ tableName: 'programs' }
181
+ });
142
182
  const ids = data.programs.map((program) => program.id);
143
183
 
144
- expect(ids).to.eql(['1']);
184
+ expect(ids).toEqual(['1']);
145
185
  });
146
186
 
147
187
  it('supports filtering for programs with step retry count greater than a given input', async function () {
@@ -151,10 +191,15 @@ describe('schema', function () {
151
191
  }
152
192
  }`;
153
193
 
154
- const { data } = await graphql(schema, query, {}, { knex: this.db, tableName: 'programs' });
194
+ const { data } = await graphql({
195
+ schema,
196
+ source: query,
197
+ contextValue: { knex: dbConnection,
198
+ tableName: 'programs' }
199
+ });
155
200
  const ids = data.programs.map((program) => program.id);
156
201
 
157
- expect(ids).to.eql(['3']);
202
+ expect(ids).toEqual(['3']);
158
203
  });
159
204
  });
160
205
  });
@@ -1,13 +1,13 @@
1
1
  'use strict';
2
2
 
3
- const IgnorableError = require('./');
3
+ const IgnorableError = require('.');
4
4
 
5
5
  describe('IgnorableError', () => {
6
6
  it('should have a retryable property set to true', () => {
7
7
  try {
8
8
  throw new IgnorableError();
9
9
  } catch (error) {
10
- expect(error.ignorable).to.be.true;
10
+ expect(error.ignorable).toBe(true);
11
11
  }
12
12
  });
13
13
 
@@ -15,8 +15,8 @@ describe('IgnorableError', () => {
15
15
  try {
16
16
  throw new IgnorableError('Something bad happened!', 200);
17
17
  } catch (error) {
18
- expect(error.message).to.eql('Something bad happened!');
19
- expect(error.code).to.eql(200);
18
+ expect(error.message).toEqual('Something bad happened!');
19
+ expect(error.code).toEqual(200);
20
20
  }
21
21
  });
22
22
 
@@ -24,7 +24,7 @@ describe('IgnorableError', () => {
24
24
  try {
25
25
  throw new IgnorableError('Something bad happened!');
26
26
  } catch (error) {
27
- expect(IgnorableError.isIgnorable(error)).to.eql(true);
27
+ expect(IgnorableError.isIgnorable(error)).toEqual(true);
28
28
  }
29
29
  });
30
30
  });
@@ -0,0 +1,94 @@
1
+ 'use strict';
2
+
3
+ const ProgramExecutor = require('./index-pubsub');
4
+ const ProgramsRepository = require('./repositories/programs');
5
+ const e2eTestReuse = require('@emartech/pubsub-client-js/e2e/e2e-test-resuse.js');
6
+
7
+ const knex = require('knex');
8
+ const DbCleaner = require('./test-helper/db-cleaner');
9
+ const { db } = require('./test-helper/get-test-db-config');
10
+
11
+ const customerId = 1234;
12
+ const topicName = 'program-executor';
13
+
14
+ const testJobLibrary = {
15
+ firstJob: {},
16
+ secondJob: {}
17
+ };
18
+
19
+ const waitForMessages = (timeout) => {
20
+ let timeOutLength = timeout;
21
+ return new Promise((resolve) => setTimeout(resolve, timeOutLength));
22
+ };
23
+
24
+ describe('ProgramExecutor', function () {
25
+ let config;
26
+ let dbConnection;
27
+ let mockConstructorSpy;
28
+ let publisher;
29
+
30
+ beforeAll(async function () {
31
+ jest.clearAllMocks();
32
+ dbConnection = knex({
33
+ client: 'pg',
34
+ connection: db.connection.connString
35
+ });
36
+ await DbCleaner.create(dbConnection).tearDown();
37
+ ProgramsRepository.create(dbConnection, 'programs');
38
+ });
39
+
40
+ afterAll(async function () {
41
+ await DbCleaner.create(dbConnection).tearDown();
42
+ dbConnection.destroy();
43
+ });
44
+
45
+
46
+ afterEach(async () => {
47
+ await e2eTestReuse.afterEach(mockConstructorSpy, publisher);
48
+
49
+ publisher = undefined;
50
+ jest.clearAllMocks();
51
+ jest.resetModules();
52
+ });
53
+
54
+
55
+ beforeEach(async function () {
56
+
57
+ config = {
58
+ knex: dbConnection,
59
+ tableName: 'programs',
60
+ topicName: topicName,
61
+ projectId: 'some-project-id'
62
+ };
63
+ const result = await e2eTestReuse.beforeEach(topicName);
64
+ mockConstructorSpy = result.mockConstructorSpy;
65
+ publisher = result.publisher;
66
+
67
+ });
68
+
69
+ describe('#Program Executor', function () {
70
+ it('should execute program and maintain status in DB', async function () {
71
+ const runId = await ProgramExecutor.create(config).createProgram(
72
+ {
73
+ programData: {
74
+ customerId: customerId
75
+ },
76
+ jobs: ['current_program', 'next_program']
77
+ }
78
+ );
79
+ await ProgramExecutor.create(config).processPrograms(testJobLibrary);
80
+
81
+ await waitForMessages(1000);
82
+
83
+ const result = await dbConnection('programs').where({ run_id: runId }).first();
84
+
85
+ expect(result.run_id).toBe(runId);
86
+ expect(result.jobs).toEqual(['current_program', 'next_program']);
87
+ expect(result.step).toEqual(1);
88
+ expect(result.errored_at).toEqual(null);
89
+ expect(result.finished_at).not.toEqual(null);
90
+ expect(result.program_data).toEqual({ customerId: 1234 });
91
+ });
92
+ });
93
+
94
+ });
@@ -0,0 +1,78 @@
1
+ 'use strict';
2
+
3
+ const consumer = require('@emartech/pubsub-client-js').Consumer;
4
+ const EventEmitter = require('events');
5
+
6
+ const ProgramHandler = require('./program-handler');
7
+ const ProgramsRepository = require('./repositories/programs');
8
+ const QueueManager = require('./queue-manager-pubsub');
9
+
10
+ class ProgramExecutor extends EventEmitter {
11
+ /**
12
+ * @param {object} config
13
+ * @param {object} config.knex - Connected Knex instance
14
+ * @param {string} config.projectId - GCP Project ID
15
+ * @param {string} config.tableName - Table name for bookkeeping
16
+ * @param {string} config.topicName - Topic name to publish to
17
+ */
18
+ constructor(config) {
19
+ super();
20
+ this._config = config;
21
+ this._programsRepository = ProgramsRepository.create(config.knex, config.tableName);
22
+ this._queueManager = QueueManager.create(config.topicName, config.projectId);
23
+ this._programHandler = ProgramHandler.create(this._programsRepository, this._queueManager);
24
+ }
25
+
26
+ /**
27
+ * @param {object} data
28
+ * @param {object} data.programData
29
+ * @param {array} data.jobs
30
+ * @param {object} data.jobsData
31
+ */
32
+ createProgram(data) {
33
+ return this._programHandler.createProgram(data);
34
+ }
35
+
36
+ processPrograms(jobLibrary) {
37
+ const eventEmitter = this; // eslint-disable-line consistent-this
38
+
39
+ const programExecutorProcessor = require('./program-executor-processor').create(
40
+ this._programHandler,
41
+ this._queueManager,
42
+ jobLibrary
43
+ );
44
+
45
+ consumer
46
+ .create(this._config.topicName,
47
+ {
48
+ logger: `${this._config.topicName}-consumer`,
49
+ MaxStreams: 1,
50
+ MaxMessages: 1,
51
+ onMessage: async (message) => {
52
+ try {
53
+ await programExecutorProcessor.process(message);
54
+ } catch (error) {
55
+ eventEmitter.emit('programError', { message, error });
56
+ error.message = error.message.substring(0, 255);
57
+ throw error;
58
+ }
59
+ }
60
+ },
61
+ { projectId: this._config.projectId }
62
+ )
63
+ .process();
64
+ }
65
+
66
+ /**
67
+ * @param {object} config
68
+ * @param {object} config.knex - Connected Knex instance
69
+ * @param {string} config.projectId - GCP Project ID
70
+ * @param {string} config.tableName - Table name for bookkeeping
71
+ * @param {string} config.topicName - Topic name to publish to
72
+ */
73
+ static create(config) {
74
+ return new ProgramExecutor(config);
75
+ }
76
+ }
77
+
78
+ module.exports = ProgramExecutor;