@tmlmobilidade/databases 20260511.1606.49 → 20260513.1439.37

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.
@@ -0,0 +1,44 @@
1
+ export declare const SERVERDB_KEYS: Readonly<{
2
+ FACILITIES: {
3
+ BOAT_STATIONS: string;
4
+ HELPDESKS: string;
5
+ LIGHT_RAIL_STATIONS: string;
6
+ PIPS: string;
7
+ SCHOOLS: string;
8
+ STORES: string;
9
+ SUBWAY_STATIONS: string;
10
+ TRAIN_STATIONS: string;
11
+ };
12
+ LOCATIONS: {
13
+ DISTRICTS: string;
14
+ LOCALITIES: string;
15
+ MUNICIPALITIES: string;
16
+ PARISHES: string;
17
+ REGIONS: string;
18
+ };
19
+ NETWORK: {
20
+ ALERTS: {
21
+ ALL: string;
22
+ PROTOBUF: string;
23
+ SENT_NOTIFICATIONS: string;
24
+ };
25
+ DATES: string;
26
+ LINES: string;
27
+ PATTERNS: {
28
+ BASE: string;
29
+ ID: (id: string) => string;
30
+ };
31
+ PERIODS: string;
32
+ PLANS: string;
33
+ ROUTES: string;
34
+ SHAPES: {
35
+ BASE: string;
36
+ ID: (id: string) => string;
37
+ };
38
+ STOPS: string;
39
+ VEHICLES: {
40
+ ALL: string;
41
+ PROTOBUF: string;
42
+ };
43
+ };
44
+ }>;
@@ -0,0 +1,45 @@
1
+ /* * */
2
+ export const SERVERDB_KEYS = Object.freeze({
3
+ FACILITIES: {
4
+ BOAT_STATIONS: 'facilities:boat_stations',
5
+ HELPDESKS: 'facilities:helpdesks',
6
+ LIGHT_RAIL_STATIONS: 'facilities:light_rail_stations',
7
+ PIPS: 'facilities:pips',
8
+ SCHOOLS: 'facilities:schools',
9
+ STORES: 'facilities:stores',
10
+ SUBWAY_STATIONS: 'facilities:subway_stations',
11
+ TRAIN_STATIONS: 'facilities:train_stations',
12
+ },
13
+ LOCATIONS: {
14
+ DISTRICTS: 'locations:districts',
15
+ LOCALITIES: 'locations:localities',
16
+ MUNICIPALITIES: 'locations:municipalities',
17
+ PARISHES: 'locations:parishes',
18
+ REGIONS: 'locations:regions',
19
+ },
20
+ NETWORK: {
21
+ ALERTS: {
22
+ ALL: 'network:alerts:all',
23
+ PROTOBUF: 'network:alerts:protobuf',
24
+ SENT_NOTIFICATIONS: 'network:alerts:sent_notifications',
25
+ },
26
+ DATES: 'network:dates',
27
+ LINES: 'network:lines',
28
+ PATTERNS: {
29
+ BASE: 'network:patterns',
30
+ ID: (id) => `network:patterns:${id}`,
31
+ },
32
+ PERIODS: 'network:periods',
33
+ PLANS: 'network:plans',
34
+ ROUTES: 'network:routes',
35
+ SHAPES: {
36
+ BASE: 'network:shapes',
37
+ ID: (id) => `network:shapes:${id}`,
38
+ },
39
+ STOPS: 'network:stops',
40
+ VEHICLES: {
41
+ ALL: 'network:vehicles:all',
42
+ PROTOBUF: 'network:vehicles:protobuf',
43
+ },
44
+ },
45
+ });
@@ -1,2 +1,3 @@
1
+ export * from './hub-serverdb-keys.js';
1
2
  export * from './interface.js';
2
3
  export * from './keys.js';
@@ -1,2 +1,3 @@
1
+ export * from './hub-serverdb-keys.js';
1
2
  export * from './interface.js';
2
3
  export * from './keys.js';
@@ -1,4 +1,4 @@
1
- import { type ApiCacheKey } from './keys.js';
1
+ import { type ApiCacheKey, type ApiCacheKeyParams } from './keys.js';
2
2
  import { type RedisClientType } from 'redis';
3
3
  declare class ApiCacheClass {
4
4
  private static _instance;
@@ -8,7 +8,7 @@ declare class ApiCacheClass {
8
8
  */
9
9
  static getInstance(): Promise<ApiCacheClass>;
10
10
  /**
11
- * Deletes all keys from the cache that are not defined in `ApiCacheKeyValues`.
11
+ * Deletes all keys from the cache that are not allowed by {@link isAllowedHubApiCacheKey}.
12
12
  * This method is useful for maintaining a clean state free of stale
13
13
  * or irrelevant cache entries that consume storage and memory resources.
14
14
  * @returns A promise that resolves when the cleaning process is complete.
@@ -22,22 +22,38 @@ declare class ApiCacheClass {
22
22
  * @throws Will throw an error if the deletion process fails.
23
23
  */
24
24
  delete(key: ApiCacheKey): Promise<void>;
25
+ /**
26
+ * Deletes multiple cache entries.
27
+ * @param keys The list of keys to delete.
28
+ */
29
+ deleteMany(keys: string[]): Promise<void>;
25
30
  /**
26
31
  * Retrieves a cache entry by its key.
27
32
  * @param key The key of the cache entry to retrieve.
33
+ * @param params Optional params to replace `{named}` tokens in the key.
28
34
  * @returns A promise that resolves with the cache entry value,
29
35
  * or `null` if not found.
30
36
  * @throws Will throw an error if the retrieval process fails.
31
37
  */
32
- get(key: ApiCacheKey): Promise<null | string>;
38
+ get(key: ApiCacheKey, params?: ApiCacheKeyParams): Promise<null | string>;
39
+ /**
40
+ * Scans cache keys by pattern.
41
+ * @param pattern The redis pattern to match.
42
+ * @returns A promise resolving with all matching keys.
43
+ */
44
+ scan(pattern: string): Promise<string[]>;
33
45
  /**
34
46
  * Saves a cache entry with an optional time-to-live (TTL).
35
47
  * @param key The key of the cache entry to save.
36
48
  * @param value The value of the cache entry to save. Must be a string.
37
49
  * @param ttl Optional time-to-live for the cache entry in seconds.
38
50
  * If not provided, the entry will persist indefinitely.
51
+ * @param params Optional params to replace `{named}` tokens in the key.
39
52
  */
40
- set(key: ApiCacheKey, value: string, ttl?: number): Promise<void>;
53
+ set(key: ApiCacheKey, value: string, options: {
54
+ params?: ApiCacheKeyParams;
55
+ ttl?: number;
56
+ }): Promise<void>;
41
57
  protected connectToClient(): Promise<RedisClientType>;
42
58
  /**
43
59
  * Initializes the Redis client.
@@ -1,6 +1,6 @@
1
1
  /* * */
2
2
  import { GORedisClient } from '../../clients/go-redis.js';
3
- import { ApiCacheKeyValues } from './keys.js';
3
+ import { resolveApiCacheKey } from './keys.js';
4
4
  import { asyncSingletonProxy } from '@tmlmobilidade/utils';
5
5
  /* * */
6
6
  class ApiCacheClass {
@@ -28,7 +28,7 @@ class ApiCacheClass {
28
28
  return await this._instance;
29
29
  }
30
30
  /**
31
- * Deletes all keys from the cache that are not defined in `ApiCacheKeyValues`.
31
+ * Deletes all keys from the cache that are not allowed by {@link isAllowedHubApiCacheKey}.
32
32
  * This method is useful for maintaining a clean state free of stale
33
33
  * or irrelevant cache entries that consume storage and memory resources.
34
34
  * @returns A promise that resolves when the cleaning process is complete.
@@ -36,7 +36,7 @@ class ApiCacheClass {
36
36
  */
37
37
  async clean() {
38
38
  const allKeys = await this.client.keys('*');
39
- const keysToDelete = allKeys.filter(key => !ApiCacheKeyValues.includes(key));
39
+ const keysToDelete = allKeys.filter(key => key);
40
40
  if (keysToDelete.length)
41
41
  await this.client.del(keysToDelete);
42
42
  }
@@ -49,38 +49,60 @@ class ApiCacheClass {
49
49
  async delete(key) {
50
50
  await this.client.del(key);
51
51
  }
52
+ /**
53
+ * Deletes multiple cache entries.
54
+ * @param keys The list of keys to delete.
55
+ */
56
+ async deleteMany(keys) {
57
+ if (!keys.length)
58
+ return;
59
+ await this.client.del(keys);
60
+ }
52
61
  /**
53
62
  * Retrieves a cache entry by its key.
54
63
  * @param key The key of the cache entry to retrieve.
64
+ * @param params Optional params to replace `{named}` tokens in the key.
55
65
  * @returns A promise that resolves with the cache entry value,
56
66
  * or `null` if not found.
57
67
  * @throws Will throw an error if the retrieval process fails.
58
68
  */
59
- async get(key) {
60
- const result = await this.client.get(key);
69
+ async get(key, params) {
70
+ const parsedKey = resolveApiCacheKey(key, params);
71
+ const result = await this.client.get(parsedKey);
61
72
  if (typeof result !== 'string')
62
73
  return null;
63
74
  return result;
64
75
  }
76
+ /**
77
+ * Scans cache keys by pattern.
78
+ * @param pattern The redis pattern to match.
79
+ * @returns A promise resolving with all matching keys.
80
+ */
81
+ async scan(pattern) {
82
+ const keys = [];
83
+ for await (const key of this.client.scanIterator({ MATCH: pattern, TYPE: 'string' })) {
84
+ keys.push(String(key));
85
+ }
86
+ return keys;
87
+ }
65
88
  /**
66
89
  * Saves a cache entry with an optional time-to-live (TTL).
67
90
  * @param key The key of the cache entry to save.
68
91
  * @param value The value of the cache entry to save. Must be a string.
69
92
  * @param ttl Optional time-to-live for the cache entry in seconds.
70
93
  * If not provided, the entry will persist indefinitely.
94
+ * @param params Optional params to replace `{named}` tokens in the key.
71
95
  */
72
- async set(key, value, ttl) {
96
+ async set(key, value, options) {
97
+ const parsedKey = resolveApiCacheKey(key, options?.params);
73
98
  // Validate value type before setting cache
74
99
  if (typeof value !== 'string')
75
- throw new Error(`[ApiCache] Value must be a string. Got "${typeof value}" for key "${key}".`);
76
- // Check if key is valid before setting cache
77
- if (!ApiCacheKeyValues.includes(key))
78
- throw new Error(`[ApiCache] Invalid cache key "${key}". Allowed keys are: ${ApiCacheKeyValues.join(', ')}.`);
100
+ throw new Error(`[ApiCache] Value must be a string. Got "${typeof value}" for key "${parsedKey}".`);
79
101
  // Set cache with optional TTL
80
- if (ttl)
81
- await this.client.set(key, value, { expiration: { type: 'EX', value: ttl } });
102
+ if (options?.ttl)
103
+ await this.client.set(parsedKey, value, { expiration: { type: 'EX', value: options.ttl } });
82
104
  else
83
- await this.client.set(key, value);
105
+ await this.client.set(parsedKey, value);
84
106
  }
85
107
  connectToClient() {
86
108
  return GORedisClient.getClient();
@@ -1,2 +1,4 @@
1
- export declare const ApiCacheKeyValues: readonly ["hub:alerts:published:json", "hub:alerts:published:gtfs", "hub:alerts:published:rss", "hub:plans:approved:json", "hub:plans:gtfs", "hub:plans:gtfs:cm"];
1
+ export type ApiCacheKeyParams = Record<string, boolean | number | string>;
2
+ export declare const ApiCacheKeyValues: readonly ["hub:alerts:published:json", "hub:alerts:published:gtfs", "hub:alerts:published:rss", "hub:plans:approved:json", "hub:plans:gtfs", "hub:plans:gtfs:cm", "hub:facilities:json", "hub:facilities:helpdesks:json", "hub:facilities:boat_stations:json", "hub:facilities:light_rail_stations:json", "hub:facilities:subway_stations:json", "hub:facilities:train_stations:json", "hub:facilities:pips:json", "hub:facilities:schools:json", "hub:facilities:stores:json", "hub:locations:municipalities:json", "hub:locations:districts:json", "hub:locations:localities:json", "hub:locations:parishes:json", "hub:network:dates:json", "hub:network:periods:json", "hub:network:stops:json", "hub:network:lines:json", "hub:network:routes:json", "hub:network:plans:json", "hub:network:vehicles:json", "hub:network:vehicles:protobuf:json", "hub:network:patterns:{patternId}:json", "hub:network:shapes:{shapeId}:json"];
2
3
  export type ApiCacheKey = typeof ApiCacheKeyValues[number];
4
+ export declare function resolveApiCacheKey(key: ApiCacheKey, params?: ApiCacheKeyParams): string;
@@ -6,4 +6,41 @@ export const ApiCacheKeyValues = [
6
6
  'hub:plans:approved:json',
7
7
  'hub:plans:gtfs',
8
8
  'hub:plans:gtfs:cm',
9
+ 'hub:facilities:json',
10
+ 'hub:facilities:helpdesks:json',
11
+ 'hub:facilities:boat_stations:json',
12
+ 'hub:facilities:light_rail_stations:json',
13
+ 'hub:facilities:subway_stations:json',
14
+ 'hub:facilities:train_stations:json',
15
+ 'hub:facilities:pips:json',
16
+ 'hub:facilities:schools:json',
17
+ 'hub:facilities:stores:json',
18
+ 'hub:locations:municipalities:json',
19
+ 'hub:locations:districts:json',
20
+ 'hub:locations:localities:json',
21
+ 'hub:locations:parishes:json',
22
+ 'hub:network:dates:json',
23
+ 'hub:network:periods:json',
24
+ 'hub:network:stops:json',
25
+ 'hub:network:lines:json',
26
+ 'hub:network:routes:json',
27
+ 'hub:network:plans:json',
28
+ 'hub:network:vehicles:json',
29
+ 'hub:network:vehicles:protobuf:json',
30
+ 'hub:network:patterns:{patternId}:json',
31
+ 'hub:network:shapes:{shapeId}:json',
9
32
  ];
33
+ export function resolveApiCacheKey(key, params) {
34
+ const resolvedKey = key.replace(/\{([^{}]+)\}/g, (_match, paramName) => {
35
+ const value = params?.[paramName];
36
+ if (typeof value === 'undefined' || value === null) {
37
+ throw new Error(`[ApiCache] Missing key param "${paramName}" for key "${key}".`);
38
+ }
39
+ return String(value);
40
+ });
41
+ const unresolvedParam = resolvedKey.match(/\{([^{}]+)\}/);
42
+ if (unresolvedParam) {
43
+ throw new Error(`[ApiCache] Missing key param "${unresolvedParam[1]}" for key "${key}".`);
44
+ }
45
+ return resolvedKey;
46
+ }
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Raw Vehicle Events represent the raw vehicle events as they are received from the data sources.
3
+ * These are stored in MongoDB for persistence and later processing.
4
+ * These documents are also different depending on the data source and version of the vehicle event.
5
+ **/
1
6
  import { MongoInterfaceTemplate } from '../../templates/mongodb.js';
2
7
  import { type SimplifiedMongoIndex } from '../../types/mongo/index-description.js';
3
8
  import { type RawVehicleEvent } from '@tmlmobilidade/types';
@@ -1,4 +1,8 @@
1
- /* * */
1
+ /**
2
+ * Raw Vehicle Events represent the raw vehicle events as they are received from the data sources.
3
+ * These are stored in MongoDB for persistence and later processing.
4
+ * These documents are also different depending on the data source and version of the vehicle event.
5
+ **/
2
6
  import { PCGIRawClient } from '../../clients/pcgi-raw.js';
3
7
  import { MongoInterfaceTemplate } from '../../templates/mongodb.js';
4
8
  import { RawVehicleEventSchema } from '@tmlmobilidade/types';
@@ -1,11 +1,17 @@
1
+ /**
2
+ * Simplified Vehicle Events represent a simplified version of the raw vehicle events.
3
+ * These are stored in ClickHouse for performance and scalability reasons.
4
+ **/
1
5
  import { ClickHouseInterfaceTemplate } from '../../templates/clickhouse.js';
2
- import { type ClickHouseSchema } from '../../types/index.js';
6
+ import { type ClickHouseSchema, ClickHouseTableEngine } from '../../types/index.js';
3
7
  import { type SimplifiedVehicleEvent } from '@tmlmobilidade/types';
4
8
  declare class SimplifiedVehicleEventsNewClass extends ClickHouseInterfaceTemplate<SimplifiedVehicleEvent> {
5
9
  private static _instance;
6
10
  readonly databaseName = "operation";
7
- readonly orderBy = "(created_at, trip_id)";
8
- readonly partitionBy = "toYYYYMMDD(fromUnixTimestamp64Milli(created_at))";
11
+ readonly engine: ClickHouseTableEngine;
12
+ readonly orderBy = "(trip_id, vehicle_id, agency_id, created_at)";
13
+ readonly partitionBy = "toYYYYMM(fromUnixTimestamp64Milli(created_at))";
14
+ readonly primaryKey = "(trip_id, vehicle_id)";
9
15
  readonly schema: ClickHouseSchema<{
10
16
  _id: string;
11
17
  created_at: number & {
@@ -1,4 +1,7 @@
1
- /* * */
1
+ /**
2
+ * Simplified Vehicle Events represent a simplified version of the raw vehicle events.
3
+ * These are stored in ClickHouse for performance and scalability reasons.
4
+ **/
2
5
  import { GOClickHouseClient } from '../../clients/go-clickhouse.js';
3
6
  import { ClickHouseInterfaceTemplate } from '../../templates/clickhouse.js';
4
7
  import { asyncSingletonProxy } from '@tmlmobilidade/utils';
@@ -28,8 +31,10 @@ class SimplifiedVehicleEventsNewClass extends ClickHouseInterfaceTemplate {
28
31
  //
29
32
  static _instance = null;
30
33
  databaseName = 'operation';
31
- orderBy = '(created_at, trip_id)';
32
- partitionBy = 'toYYYYMMDD(fromUnixTimestamp64Milli(created_at))';
34
+ engine = 'ReplacingMergeTree';
35
+ orderBy = '(trip_id, vehicle_id, agency_id, created_at)';
36
+ partitionBy = 'toYYYYMM(fromUnixTimestamp64Milli(created_at))';
37
+ primaryKey = '(trip_id, vehicle_id)';
33
38
  schema = tableSchema;
34
39
  tableName = 'simplified_vehicle_events';
35
40
  /**
@@ -16,6 +16,7 @@ export declare abstract class ClickHouseInterfaceTemplate<T extends object> {
16
16
  protected readonly manageSchema: boolean;
17
17
  protected readonly orderBy: string;
18
18
  protected readonly partitionBy: null | string;
19
+ protected readonly primaryKey: null | string;
19
20
  private client;
20
21
  /**
21
22
  * Disallow direct instantiation of the service.
@@ -17,6 +17,7 @@ export class ClickHouseInterfaceTemplate {
17
17
  manageSchema = true;
18
18
  orderBy = '_id';
19
19
  partitionBy = null;
20
+ primaryKey = null;
20
21
  client;
21
22
  /**
22
23
  * Disallow direct instantiation of the service.
@@ -209,6 +210,7 @@ export class ClickHouseInterfaceTemplate {
209
210
  CREATE TABLE IF NOT EXISTS "${this.databaseName}"."${this.tableName}" (
210
211
  ${Object.entries(this.schema).map(([key, column]) => `${key} ${column.type}`).join(', ')}
211
212
  ) ENGINE = ${this.getEngineString()}
213
+ ${this.primaryKey ? `PRIMARY KEY (${this.primaryKey})` : ''}
212
214
  ${this.orderBy ? `ORDER BY (${this.orderBy})` : ''}
213
215
  ${this.partitionBy ? `PARTITION BY (${this.partitionBy})` : ''}
214
216
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmlmobilidade/databases",
3
- "version": "20260511.1606.49",
3
+ "version": "20260513.1439.37",
4
4
  "author": {
5
5
  "email": "iso@tmlmobilidade.pt",
6
6
  "name": "TML-ISO"