@tmlmobilidade/writers 20260320.1741.37 → 20260320.1746.41

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.
@@ -5,7 +5,7 @@ type ClickHouseWriterParams<T> = {
5
5
  /**
6
6
  * The maximum number of items to hold in memory
7
7
  * before flushing to the database.
8
- * @default 10000
8
+ * @default 10_000
9
9
  */
10
10
  batch_size?: number;
11
11
  /**
@@ -15,6 +15,12 @@ type ClickHouseWriterParams<T> = {
15
15
  * @default disabled
16
16
  */
17
17
  batch_timeout?: number;
18
+ /**
19
+ * If enabled, starts an async one-time table ensure operation in the constructor.
20
+ * Use `await writer.init()` if you need to block startup until it is completed.
21
+ * @default false
22
+ */
23
+ ensure_table_on_init?: boolean;
18
24
  /**
19
25
  * How long to wait, in milliseconds, after the last write operation
20
26
  * before flushing the data to the database. This can be used to prevent
@@ -52,55 +58,39 @@ export declare class ClickHouseWriter<T> {
52
58
  private batchTimeoutTimer;
53
59
  private idleTimeoutTimer;
54
60
  private sessionTimer;
61
+ private isInitialized;
55
62
  constructor(params: ClickHouseWriterParams<T>);
56
63
  close(): Promise<void>;
64
+ /**
65
+ * Initializes the writer by ensuring the table exists.
66
+ * Safe to call multiple times.
67
+ */
68
+ init(): Promise<void>;
57
69
  /**
58
70
  * Ensures the table exists in ClickHouse by creating it if it doesn't exist.
59
71
  * Uses the tableSchema provided in the constructor, or an optional schema parameter.
60
- *
61
72
  * @param schema Optional schema to use instead of the constructor-provided tableSchema
62
- * @param engine The ClickHouse table engine to use (default: MergeTree)
73
+ * @param engine The ClickHouse table engine to use (default: ReplicatedMergeTree('/clickhouse/tables/{shard}/{table}', '{replica}'))
63
74
  * @param orderBy The ORDER BY clause for the table (default: tuple())
64
75
  */
65
76
  ensureTable(schema?: ClickHouseColumn<T>[], engine?: string, orderBy?: string): Promise<void>;
66
77
  /**
67
- * Builds a WHERE clause from a filter object.
68
- * Supports simple equality, range queries ($gte, $lte, $gt, $lt), and $in queries.
69
- *
70
- * @param filter Filter object with column names as keys
71
- * @returns WHERE clause string (without the WHERE keyword)
72
- */
73
- private buildWhereClause;
74
- /**
75
- * Formats a value for use in a SQL query.
76
- */
77
- private formatValue;
78
- /**
79
- * Counts the number of documents in the table that match the given filter.
80
- * Supports simple equality and range queries ($gte, $lte, $gt, $lt).
81
- *
82
- * @param filter Optional WHERE clause conditions (e.g., { operational_date: '2024-01-01' } or { received_at: { $gte: 1234567890 } })
83
- * @returns The count of matching documents
84
- */
85
- countDocuments(filter?: Record<string, unknown>): Promise<number>;
86
- /**
87
- * Returns distinct values for a given column, optionally filtered.
88
- * Supports simple equality and range queries ($gte, $lte, $gt, $lt).
89
- *
90
- * @param column The column name to get distinct values for
91
- * @param filter Optional WHERE clause conditions
92
- * @returns Array of distinct values
78
+ * Flushes the current batch of data to ClickHouse.
79
+ * This method is called internally when the batch size or timeouts are reached,
80
+ * but can also be called manually if needed.
81
+ * @param callback Optional callback to execute after the flush is complete, receiving the flushed data as a parameter
93
82
  */
94
- distinct<K = string>(column: string, filter?: Record<string, unknown>): Promise<K[]>;
95
83
  flush(callback?: (data?: T[]) => Promise<void>): Promise<void>;
96
84
  /**
97
85
  * Write data to the ClickHouse table.
98
- *
99
86
  * @param data The data to write
100
87
  * @param options Options for the write operation (reserved for future use)
101
88
  * @param writeCallback Callback function to call after the write operation is complete
102
89
  * @param flushCallback Callback function to call after the flush operation is complete
103
90
  */
104
- write(data: T | T[], writeCallback?: () => Promise<void>, flushCallback?: (data?: T[]) => Promise<void>): Promise<void>;
91
+ write(data: T | T[], { flushCallback, writeCallback }?: {
92
+ flushCallback?: (data?: T[]) => Promise<void>;
93
+ writeCallback?: () => Promise<void>;
94
+ }): Promise<void>;
105
95
  }
106
96
  export {};
@@ -16,6 +16,7 @@ export class ClickHouseWriter {
16
16
  batchTimeoutTimer = null;
17
17
  idleTimeoutTimer = null;
18
18
  sessionTimer = new Timer();
19
+ isInitialized = false;
19
20
  /* * */
20
21
  constructor(params) {
21
22
  if (!params.table)
@@ -36,21 +37,32 @@ export class ClickHouseWriter {
36
37
  throw new Error('CLICKHOUSEWRITER: Either client or clientConfig is required.');
37
38
  }
38
39
  }
39
- /* * */
40
+ /*
41
+ * Closes the ClickHouse client connection
42
+ * and clears any active timers.
43
+ */
40
44
  async close() {
41
45
  await this.client.close();
42
46
  Logger.info(`CLICKHOUSEWRITER [${this.params.table}]: Connection closed.`);
43
47
  }
44
- /* * */
48
+ /**
49
+ * Initializes the writer by ensuring the table exists.
50
+ * Safe to call multiple times.
51
+ */
52
+ async init() {
53
+ if (this.isInitialized)
54
+ return;
55
+ await this.ensureTable();
56
+ this.isInitialized = true;
57
+ }
45
58
  /**
46
59
  * Ensures the table exists in ClickHouse by creating it if it doesn't exist.
47
60
  * Uses the tableSchema provided in the constructor, or an optional schema parameter.
48
- *
49
61
  * @param schema Optional schema to use instead of the constructor-provided tableSchema
50
- * @param engine The ClickHouse table engine to use (default: MergeTree)
62
+ * @param engine The ClickHouse table engine to use (default: ReplicatedMergeTree('/clickhouse/tables/{shard}/{table}', '{replica}'))
51
63
  * @param orderBy The ORDER BY clause for the table (default: tuple())
52
64
  */
53
- async ensureTable(schema, engine = 'MergeTree', orderBy = 'tuple()') {
65
+ async ensureTable(schema, engine = 'ReplicatedMergeTree(\'/clickhouse/tables/{shard}/{table}\', \'{replica}\')', orderBy = 'tuple()') {
54
66
  const tableSchemaToUse = schema ?? this.params.tableSchema;
55
67
  const tableSchema = tableSchemaToUse?.map(column => `${column.name} ${column.type}`).join(', ');
56
68
  if (!tableSchema) {
@@ -58,7 +70,7 @@ export class ClickHouseWriter {
58
70
  }
59
71
  try {
60
72
  const createTableQuery = `
61
- CREATE TABLE IF NOT EXISTS ${this.params.table} (
73
+ CREATE TABLE IF NOT EXISTS ${this.params.table} ON CLUSTER 'clickhouse-replica' (
62
74
  ${tableSchema}
63
75
  ) ENGINE = ${engine}
64
76
  ORDER BY ${orderBy}
@@ -71,123 +83,15 @@ export class ClickHouseWriter {
71
83
  throw error;
72
84
  }
73
85
  }
74
- /* * */
75
86
  /**
76
- * Builds a WHERE clause from a filter object.
77
- * Supports simple equality, range queries ($gte, $lte, $gt, $lt), and $in queries.
78
- *
79
- * @param filter Filter object with column names as keys
80
- * @returns WHERE clause string (without the WHERE keyword)
87
+ * Flushes the current batch of data to ClickHouse.
88
+ * This method is called internally when the batch size or timeouts are reached,
89
+ * but can also be called manually if needed.
90
+ * @param callback Optional callback to execute after the flush is complete, receiving the flushed data as a parameter
81
91
  */
82
- buildWhereClause(filter) {
83
- if (!filter || Object.keys(filter).length === 0) {
84
- return null;
85
- }
86
- const conditions = [];
87
- for (const [key, value] of Object.entries(filter)) {
88
- if (value === null || value === undefined) {
89
- continue;
90
- }
91
- // Handle range queries (MongoDB-style operators)
92
- if (typeof value === 'object' && !Array.isArray(value)) {
93
- const operators = value;
94
- for (const [op, opValue] of Object.entries(operators)) {
95
- switch (op) {
96
- case '$gt':
97
- conditions.push(`${key} > ${this.formatValue(opValue)}`);
98
- break;
99
- case '$gte':
100
- conditions.push(`${key} >= ${this.formatValue(opValue)}`);
101
- break;
102
- case '$in':
103
- if (Array.isArray(opValue)) {
104
- const values = opValue.map(v => this.formatValue(v)).join(', ');
105
- conditions.push(`${key} IN (${values})`);
106
- }
107
- break;
108
- case '$lt':
109
- conditions.push(`${key} < ${this.formatValue(opValue)}`);
110
- break;
111
- case '$lte':
112
- conditions.push(`${key} <= ${this.formatValue(opValue)}`);
113
- break;
114
- }
115
- }
116
- }
117
- else {
118
- // Simple equality
119
- conditions.push(`${key} = ${this.formatValue(value)}`);
120
- }
121
- }
122
- return conditions.length > 0 ? conditions.join(' AND ') : null;
123
- }
124
- /**
125
- * Formats a value for use in a SQL query.
126
- */
127
- formatValue(value) {
128
- if (typeof value === 'string') {
129
- return `'${value}'`;
130
- }
131
- return String(value);
132
- }
133
- /* * */
134
- /**
135
- * Counts the number of documents in the table that match the given filter.
136
- * Supports simple equality and range queries ($gte, $lte, $gt, $lt).
137
- *
138
- * @param filter Optional WHERE clause conditions (e.g., { operational_date: '2024-01-01' } or { received_at: { $gte: 1234567890 } })
139
- * @returns The count of matching documents
140
- */
141
- async countDocuments(filter) {
142
- try {
143
- let query = `SELECT count() as count FROM ${this.params.table}`;
144
- const whereClause = this.buildWhereClause(filter);
145
- if (whereClause) {
146
- query += ` WHERE ${whereClause}`;
147
- }
148
- const result = await this.client.query({
149
- format: 'JSONEachRow',
150
- query,
151
- });
152
- const data = await result.json();
153
- return parseInt(data[0]?.count ?? '0', 10);
154
- }
155
- catch (error) {
156
- Logger.error(`CLICKHOUSEWRITER [${this.params.table}]: Error @ countDocuments(): ${error.message}`);
157
- throw error;
158
- }
159
- }
160
- /* * */
161
- /**
162
- * Returns distinct values for a given column, optionally filtered.
163
- * Supports simple equality and range queries ($gte, $lte, $gt, $lt).
164
- *
165
- * @param column The column name to get distinct values for
166
- * @param filter Optional WHERE clause conditions
167
- * @returns Array of distinct values
168
- */
169
- async distinct(column, filter) {
170
- try {
171
- let query = `SELECT DISTINCT ${column} FROM ${this.params.table}`;
172
- const whereClause = this.buildWhereClause(filter);
173
- if (whereClause) {
174
- query += ` WHERE ${whereClause}`;
175
- }
176
- const result = await this.client.query({
177
- format: 'JSONEachRow',
178
- query,
179
- });
180
- const data = await result.json();
181
- return data.map(row => row[column]);
182
- }
183
- catch (error) {
184
- Logger.error(`CLICKHOUSEWRITER [${this.params.table}]: Error @ distinct(): ${error.message}`);
185
- throw error;
186
- }
187
- }
188
- /* * */
189
92
  async flush(callback) {
190
93
  try {
94
+ await this.init();
191
95
  //
192
96
  const flushTimer = new Timer();
193
97
  const sessionTimerResult = this.sessionTimer.get();
@@ -249,17 +153,16 @@ export class ClickHouseWriter {
249
153
  throw error; // Re-throw to allow retry logic at higher level
250
154
  }
251
155
  }
252
- /* * */
253
156
  /**
254
157
  * Write data to the ClickHouse table.
255
- *
256
158
  * @param data The data to write
257
159
  * @param options Options for the write operation (reserved for future use)
258
160
  * @param writeCallback Callback function to call after the write operation is complete
259
161
  * @param flushCallback Callback function to call after the flush operation is complete
260
162
  */
261
- async write(data, writeCallback, flushCallback) {
163
+ async write(data, { flushCallback, writeCallback } = {}) {
262
164
  //
165
+ await this.init();
263
166
  //
264
167
  // Invalidate the previously set idle timeout timer
265
168
  // since we are performing a write operation again.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmlmobilidade/writers",
3
- "version": "20260320.1741.37",
3
+ "version": "20260320.1746.41",
4
4
  "author": {
5
5
  "email": "iso@tmlmobilidade.pt",
6
6
  "name": "TML-ISO"