@emartech/program-executor 3.11.1 → 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.
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,31 +19,25 @@
17
19
  "author": "team-shopify@emarsys.com",
18
20
  "license": "ISC",
19
21
  "dependencies": {
20
- "@emartech/rabbitmq-client": "5.9.0",
21
22
  "@emartech/json-logger": "3.4.0",
23
+ "@emartech/pubsub-client-js": "^1.2.5",
24
+ "@emartech/rabbitmq-client": "5.9.1",
22
25
  "camelcase-keys": "^6.2.2",
23
- "graphql": "^16.10.0"
26
+ "graphql": "^16.11.0"
24
27
  },
25
28
  "engines": {
26
29
  "node": "20.18.0"
27
30
  },
28
31
  "devDependencies": {
29
- "chai": "^4.5.0",
30
- "chai-as-promised": "7.1.1",
31
- "chai-string": "1.5.0",
32
- "chai-subset": "1.6.0",
33
- "dotenv": "^16.4.7",
34
- "eslint": "^9.21.0",
32
+ "dotenv": "^17.2.1",
33
+ "eslint": "^9.34.0",
35
34
  "eslint-config-emarsys": "5.1.0",
36
- "eslint-plugin-mocha": "^10.5.0",
37
35
  "eslint-plugin-no-only-tests": "^3.3.0",
38
36
  "eslint-plugin-security": "^3.0.1",
37
+ "jest": "29.0.0",
39
38
  "knex": "^3.1.0",
40
- "mocha": "^11.1.0",
41
- "pg": "^8.13.3",
42
- "semantic-release": "^24.2.0",
43
- "sinon": "19.0.2",
44
- "sinon-chai": "^3.7.0"
39
+ "pg": "^8.16.3",
40
+ "semantic-release": "^24.2.0"
45
41
  },
46
- "version": "3.11.1"
42
+ "version": "3.12.0"
47
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, source: query, contextValue: { 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, source: query, contextValue: { 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', () => {
@@ -113,11 +138,12 @@ describe('schema', function () {
113
138
  const { data } = await graphql({
114
139
  schema,
115
140
  source: query,
116
- contextValue: { knex: this.db, tableName: 'programs' }
141
+ contextValue: { knex: dbConnection,
142
+ tableName: 'programs' }
117
143
  });
118
144
  const ids = data.programs.map((program) => program.id);
119
145
 
120
- expect(ids).to.eql(['1', '2', '3']);
146
+ expect(ids).toEqual(['1', '2', '3']);
121
147
  });
122
148
 
123
149
  it('supports ordering by id DESC', async function () {
@@ -130,11 +156,12 @@ describe('schema', function () {
130
156
  const { data } = await graphql({
131
157
  schema,
132
158
  source: query,
133
- contextValue: { knex: this.db, tableName: 'programs' }
159
+ contextValue: { knex: dbConnection,
160
+ tableName: 'programs' }
134
161
  });
135
162
  const ids = data.programs.map((program) => program.id);
136
163
 
137
- expect(ids).to.eql(['3', '2', '1']);
164
+ expect(ids).toEqual(['3', '2', '1']);
138
165
  });
139
166
  });
140
167
 
@@ -149,11 +176,12 @@ describe('schema', function () {
149
176
  const { data } = await graphql({
150
177
  schema,
151
178
  source: query,
152
- contextValue: { knex: this.db, tableName: 'programs' }
179
+ contextValue: { knex: dbConnection,
180
+ tableName: 'programs' }
153
181
  });
154
182
  const ids = data.programs.map((program) => program.id);
155
183
 
156
- expect(ids).to.eql(['1']);
184
+ expect(ids).toEqual(['1']);
157
185
  });
158
186
 
159
187
  it('supports filtering for programs with step retry count greater than a given input', async function () {
@@ -166,11 +194,12 @@ describe('schema', function () {
166
194
  const { data } = await graphql({
167
195
  schema,
168
196
  source: query,
169
- contextValue: { knex: this.db, tableName: 'programs' }
197
+ contextValue: { knex: dbConnection,
198
+ tableName: 'programs' }
170
199
  });
171
200
  const ids = data.programs.map((program) => program.id);
172
201
 
173
- expect(ids).to.eql(['3']);
202
+ expect(ids).toEqual(['3']);
174
203
  });
175
204
  });
176
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;