@tmlmobilidade/databases 20260330.1756.23 → 20260331.1620.53

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,6 +5,7 @@ declare class SimplifiedVehicleEventsNewClass extends ClickHouseInterfaceTemplat
5
5
  private static _instance;
6
6
  readonly databaseName = "operation";
7
7
  readonly orderBy = "(created_at, trip_id)";
8
+ readonly partitionBy = "toYYYYMMDD(fromUnixTimestamp64Milli(created_at))";
8
9
  readonly schema: ClickHouseSchema<{
9
10
  _id: string;
10
11
  created_at: number & {
@@ -13,10 +14,10 @@ declare class SimplifiedVehicleEventsNewClass extends ClickHouseInterfaceTemplat
13
14
  latitude: number;
14
15
  longitude: number;
15
16
  agency_id: string;
16
- pattern_id: string | null;
17
17
  received_at: number & {
18
18
  __brand: "UnixTimestamp";
19
19
  };
20
+ pattern_id: string | null;
20
21
  stop_id: string | null;
21
22
  trip_id: string;
22
23
  vehicle_id: string;
@@ -29,6 +30,22 @@ declare class SimplifiedVehicleEventsNewClass extends ClickHouseInterfaceTemplat
29
30
  extra_trip_id: string | null;
30
31
  }>;
31
32
  readonly tableName = "simplified_vehicle_events";
33
+ /**
34
+ * Returns the last event for a given vehicle.
35
+ * @param vehicleId - The ID of the vehicle.
36
+ * @returns The last event for the vehicle.
37
+ */
38
+ getLastEvent(vehicleId: string, agencyId: string): Promise<null | SimplifiedVehicleEvent>;
39
+ /**
40
+ * Retrieves the most recent event per vehicle within the given time window.
41
+ *
42
+ * @param secondsAgo - The number of seconds in the past to consider for retrieving recent positions. Defaults to 90 seconds.
43
+ * @returns A promise resolving to an array of SimplifiedVehicleEvent objects, each representing the latest event for a vehicle within the specified period.
44
+ *
45
+ * The method filters out events where any of vehicle_id, agency_id, or trip_id are empty or null,
46
+ * and also ensures latitude and longitude are not zero. Only the most recent event per vehicle is returned.
47
+ */
48
+ getPositions(secondsAgo?: number): Promise<SimplifiedVehicleEvent[]>;
32
49
  /**
33
50
  * Returns the singleton instance of the subclass.
34
51
  */
@@ -29,8 +29,46 @@ class SimplifiedVehicleEventsNewClass extends ClickHouseInterfaceTemplate {
29
29
  static _instance = null;
30
30
  databaseName = 'operation';
31
31
  orderBy = '(created_at, trip_id)';
32
+ partitionBy = 'toYYYYMMDD(fromUnixTimestamp64Milli(created_at))';
32
33
  schema = tableSchema;
33
34
  tableName = 'simplified_vehicle_events';
35
+ /**
36
+ * Returns the last event for a given vehicle.
37
+ * @param vehicleId - The ID of the vehicle.
38
+ * @returns The last event for the vehicle.
39
+ */
40
+ async getLastEvent(vehicleId, agencyId) {
41
+ const query = `
42
+ SELECT *
43
+ FROM "${this.databaseName}"."${this.tableName}"
44
+ WHERE vehicle_id = '${vehicleId}'
45
+ AND agency_id = '${agencyId}'
46
+ ORDER BY created_at DESC
47
+ LIMIT 1
48
+ `;
49
+ const result = await this.queryFromString(query);
50
+ return result.length > 0 ? result[0] : null;
51
+ }
52
+ /**
53
+ * Retrieves the most recent event per vehicle within the given time window.
54
+ *
55
+ * @param secondsAgo - The number of seconds in the past to consider for retrieving recent positions. Defaults to 90 seconds.
56
+ * @returns A promise resolving to an array of SimplifiedVehicleEvent objects, each representing the latest event for a vehicle within the specified period.
57
+ *
58
+ * The method filters out events where any of vehicle_id, agency_id, or trip_id are empty or null,
59
+ * and also ensures latitude and longitude are not zero. Only the most recent event per vehicle is returned.
60
+ */
61
+ async getPositions(secondsAgo = 90) {
62
+ const query = `
63
+ SELECT *
64
+ FROM "${this.databaseName}"."${this.tableName}"
65
+ WHERE created_at > toUnixTimestamp64Milli(now64(3) - INTERVAL ${secondsAgo} SECOND)
66
+ ORDER BY created_at DESC
67
+ LIMIT 1 BY vehicle_id, agency_id
68
+ `;
69
+ const result = await this.queryFromString(query);
70
+ return result;
71
+ }
34
72
  /**
35
73
  * Returns the singleton instance of the subclass.
36
74
  */
@@ -1,4 +1,6 @@
1
1
  import { type ClickHouseSchema, type ClickHouseTableEngine } from '../types/index.js';
2
+ import { queryFromFile } from '../utils/clickhouse/query-from-file.js';
3
+ import { queryFromString } from '../utils/clickhouse/query-from-string.js';
2
4
  import { type ClickHouseClient, type DataFormat } from '@clickhouse/client';
3
5
  export declare abstract class ClickHouseInterfaceTemplate<T> {
4
6
  protected readonly abstract databaseName: string;
@@ -6,6 +8,7 @@ export declare abstract class ClickHouseInterfaceTemplate<T> {
6
8
  protected readonly abstract tableName: string;
7
9
  protected readonly engine: ClickHouseTableEngine;
8
10
  protected readonly orderBy: string;
11
+ protected readonly partitionBy: null | string;
9
12
  private client;
10
13
  /**
11
14
  * Disallow direct instantiation of the service.
@@ -85,11 +88,32 @@ export declare abstract class ClickHouseInterfaceTemplate<T> {
85
88
  */
86
89
  protected postInit(): Promise<void>;
87
90
  /**
88
- * Ensures that the specified database exists in ClickHouse, creating it if it does not already exist.
89
- * This method performs input validation to prevent SQL injection and logs the outcome of the operation.
90
- * @throws Will throw an error if the database name is unsafe or if the database creation query fails.
91
- * @returns A promise that resolves when the database is ensured to exist.
92
- */
91
+ * Executes a query from a .sql file with optional parameter substitutions.
92
+ * @param filePath Absolute or relative path to the .sql file.
93
+ * @param params Optional key-value substitutions applied to the query (replaces {key} placeholders).
94
+ * @returns Query result rows typed as `T`.
95
+ * @example
96
+ * // Given a SQL file "get_users.sql" with the content:
97
+ * // SELECT * FROM users WHERE created_at >= {start_date} AND created_at <= {end_date}
98
+ * const users = await clickhouseService.queryFromFile<User>('get_users.sql', {
99
+ * start_date: '2024-01-01',
100
+ * end_date: '2024-12-31',
101
+ * });
102
+ */
103
+ queryFromFile<T>(filePath: string, params?: Record<string, number | string>): ReturnType<typeof queryFromFile<T>>;
104
+ /**
105
+ * Executes a query from a string.
106
+ * @param client The ClickHouse client to use for executing the query.
107
+ * @param query The SQL query to execute, with optional {key} placeholders for parameters.
108
+ * @param params Optional key-value substitutions applied to the query (replaces {key} placeholders).
109
+ * @returns Query result rows typed as `T`.
110
+ * @example
111
+ * const users = await queryFromString<User>(clickhouseClient,
112
+ * 'SELECT * FROM users WHERE created_at >= {start_date} AND created_at <= {end_date}',
113
+ * { start_date: '2024-01-01', end_date: '2024-12-31' }
114
+ * );
115
+ */
116
+ queryFromString<T>(query: string, params?: Record<string, number | string>): ReturnType<typeof queryFromString<T>>;
93
117
  private ensureDatabase;
94
118
  /**
95
119
  * Ensures that the specified table exists in ClickHouse, creating it if it does not already exist.
@@ -1,5 +1,6 @@
1
1
  /* * */
2
2
  import { preparePositionalQueryParams } from '../utils/clickhouse/prepare-positional-query-params.js';
3
+ import { queryFromFile } from '../utils/clickhouse/query-from-file.js';
3
4
  import { queryFromString } from '../utils/clickhouse/query-from-string.js';
4
5
  import { validateSqlParam } from '../utils/clickhouse/validate-sql-param.js';
5
6
  import { Logger } from '@tmlmobilidade/logger';
@@ -7,6 +8,7 @@ import { Logger } from '@tmlmobilidade/logger';
7
8
  export class ClickHouseInterfaceTemplate {
8
9
  engine = 'ReplicatedMergeTree';
9
10
  orderBy = '_id';
11
+ partitionBy = null;
10
12
  client;
11
13
  /**
12
14
  * Disallow direct instantiation of the service.
@@ -127,11 +129,36 @@ export class ClickHouseInterfaceTemplate {
127
129
  // no-op by default
128
130
  }
129
131
  /**
130
- * Ensures that the specified database exists in ClickHouse, creating it if it does not already exist.
131
- * This method performs input validation to prevent SQL injection and logs the outcome of the operation.
132
- * @throws Will throw an error if the database name is unsafe or if the database creation query fails.
133
- * @returns A promise that resolves when the database is ensured to exist.
134
- */
132
+ * Executes a query from a .sql file with optional parameter substitutions.
133
+ * @param filePath Absolute or relative path to the .sql file.
134
+ * @param params Optional key-value substitutions applied to the query (replaces {key} placeholders).
135
+ * @returns Query result rows typed as `T`.
136
+ * @example
137
+ * // Given a SQL file "get_users.sql" with the content:
138
+ * // SELECT * FROM users WHERE created_at >= {start_date} AND created_at <= {end_date}
139
+ * const users = await clickhouseService.queryFromFile<User>('get_users.sql', {
140
+ * start_date: '2024-01-01',
141
+ * end_date: '2024-12-31',
142
+ * });
143
+ */
144
+ async queryFromFile(filePath, params) {
145
+ return await queryFromFile(this.client, filePath, params);
146
+ }
147
+ /**
148
+ * Executes a query from a string.
149
+ * @param client The ClickHouse client to use for executing the query.
150
+ * @param query The SQL query to execute, with optional {key} placeholders for parameters.
151
+ * @param params Optional key-value substitutions applied to the query (replaces {key} placeholders).
152
+ * @returns Query result rows typed as `T`.
153
+ * @example
154
+ * const users = await queryFromString<User>(clickhouseClient,
155
+ * 'SELECT * FROM users WHERE created_at >= {start_date} AND created_at <= {end_date}',
156
+ * { start_date: '2024-01-01', end_date: '2024-12-31' }
157
+ * );
158
+ */
159
+ async queryFromString(query, params) {
160
+ return await queryFromString(this.client, query, params);
161
+ }
135
162
  async ensureDatabase() {
136
163
  // Validate the inputs are safe identifiers to prevent SQL injection
137
164
  if (!validateSqlParam(this.databaseName, false))
@@ -159,8 +186,6 @@ export class ClickHouseInterfaceTemplate {
159
186
  throw new Error(`CLICKHOUSE [${this.databaseName}]: Unsafe database name provided.`);
160
187
  if (!validateSqlParam(this.tableName, false))
161
188
  throw new Error(`CLICKHOUSE [${this.tableName}]: Unsafe table name provided.`);
162
- if (!validateSqlParam(this.engine, false))
163
- throw new Error(`CLICKHOUSE [${this.engine}]: Unsafe engine type provided.`);
164
189
  // Validate the schema columns are safe identifiers
165
190
  const unsafeColumns = Object.keys(this.schema).filter(key => !validateSqlParam(key, false));
166
191
  if (unsafeColumns.length > 0)
@@ -172,7 +197,8 @@ export class ClickHouseInterfaceTemplate {
172
197
  CREATE TABLE IF NOT EXISTS "${this.databaseName}"."${this.tableName}" ON CLUSTER default_cluster (
173
198
  ${Object.entries(this.schema).map(([key, column]) => `${key} ${column.type}`).join(', ')}
174
199
  ) ENGINE = ${this.getEngineString()}
175
- ORDER BY ${this.orderBy}
200
+ ${this.orderBy ? `ORDER BY ${this.orderBy}` : ''}
201
+ ${this.partitionBy ? `PARTITION BY ${this.partitionBy}` : ''}
176
202
  `;
177
203
  // Perform the query to create the table
178
204
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmlmobilidade/databases",
3
- "version": "20260330.1756.23",
3
+ "version": "20260331.1620.53",
4
4
  "author": {
5
5
  "email": "iso@tmlmobilidade.pt",
6
6
  "name": "TML-ISO"