@yarkivaev/source-to-sink 1.0.2 → 1.0.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yarkivaev/source-to-sink",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Generic library for building data streaming pipelines",
5
5
  "repository": {
6
6
  "type": "git",
@@ -5,7 +5,8 @@ import pg from 'pg';
5
5
  *
6
6
  * Creates a pg.Pool internally and implements the Sink
7
7
  * interface for use with batch collectors. Supports optional
8
- * conflict resolution via ON CONFLICT DO NOTHING.
8
+ * conflict resolution via ON CONFLICT DO NOTHING or
9
+ * ON CONFLICT DO UPDATE SET.
9
10
  *
10
11
  * @example
11
12
  * const sink = postgresSink('postgresql://localhost:5432/db', 'metrics', ['ts', 'value']);
@@ -20,9 +21,26 @@ import pg from 'pg';
20
21
  * @param {string} table - Target table name
21
22
  * @param {Array<string>} columns - Column names for insertion
22
23
  * @param {object} [options] - Optional configuration
23
- * @param {Array<string>} [options.conflict] - Columns for ON CONFLICT DO NOTHING clause
24
+ * @param {Array<string>} [options.conflict] - Columns for ON CONFLICT clause
25
+ * @param {Array<string>} [options.update] - Columns for DO UPDATE SET clause
24
26
  * @returns {object} Sink with write(records) method
25
27
  */
28
+ /**
29
+ * Builds the ON CONFLICT SQL suffix from options.
30
+ *
31
+ * @param {object} options - Sink options with conflict and update arrays
32
+ * @returns {string} SQL suffix or empty string
33
+ */
34
+ function buildSuffix(options) {
35
+ if (!Array.isArray(options.conflict) || options.conflict.length === 0) return '';
36
+ const cols = options.conflict.join(', ');
37
+ if (Array.isArray(options.update) && options.update.length > 0) {
38
+ const sets = options.update.map(c => `${c} = EXCLUDED.${c}`).join(', ');
39
+ return ` ON CONFLICT (${cols}) DO UPDATE SET ${sets}`;
40
+ }
41
+ return ` ON CONFLICT (${cols}) DO NOTHING`;
42
+ }
43
+
26
44
  export default function postgresSink(url, table, columns, options = {}) {
27
45
  if (typeof url !== 'string' || url.length === 0) {
28
46
  throw new Error('URL must be a non-empty string');
@@ -34,9 +52,7 @@ export default function postgresSink(url, table, columns, options = {}) {
34
52
  throw new Error('Columns must be a non-empty array');
35
53
  }
36
54
  const pool = new pg.Pool({ connectionString: url });
37
- const suffix = Array.isArray(options.conflict) && options.conflict.length > 0
38
- ? ` ON CONFLICT (${options.conflict.join(', ')}) DO NOTHING`
39
- : '';
55
+ const suffix = buildSuffix(options);
40
56
  return {
41
57
  /**
42
58
  * Writes records to PostgreSQL table.
@@ -73,4 +73,11 @@ describe('postgresSink', () => {
73
73
  ['ts', 'value'], {});
74
74
  assert.strictEqual(typeof sink.write, 'function', 'Should have write method without conflict key');
75
75
  });
76
+
77
+ it('returns sink with write method when conflict and update options are provided', () => {
78
+ const sink = postgresSink('postgresql://localhost:5432/db', 'segments',
79
+ ['machine', 'name', 'start_time', 'end_time', 'duration'],
80
+ { conflict: ['machine', 'start_time'], update: ['name', 'end_time', 'duration'] });
81
+ assert.strictEqual(typeof sink.write, 'function', 'Should have write method with update option');
82
+ });
76
83
  });