@tmlmobilidade/utils 20250827.1228.57 → 20250827.1349.36
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/dist/src/databases/index.d.ts +1 -0
- package/dist/src/databases/index.js +1 -0
- package/dist/src/databases/sqlite-writer.d.ts +63 -0
- package/dist/src/databases/sqlite-writer.js +127 -0
- package/dist/src/random/generate-random-string.d.ts +1 -1
- package/dist/src/random/generate-random-string.js +4 -0
- package/package.json +2 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
interface SQLiteWriterColumn<T> {
|
|
2
|
+
indexed?: boolean;
|
|
3
|
+
name: Extract<keyof T, string>;
|
|
4
|
+
not_null?: boolean;
|
|
5
|
+
primary_key?: boolean;
|
|
6
|
+
type: 'BLOB' | 'INTEGER' | 'REAL' | 'TEXT';
|
|
7
|
+
}
|
|
8
|
+
export interface SQLiteWriterParams<T> {
|
|
9
|
+
/**
|
|
10
|
+
* The maximum number of items to hold in memory
|
|
11
|
+
* before flushing to the database.
|
|
12
|
+
* @default 3000
|
|
13
|
+
*/
|
|
14
|
+
batch_size?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Columns in the table.
|
|
17
|
+
* Order matters for INSERT.
|
|
18
|
+
* Must be keys of T or custom names.
|
|
19
|
+
*/
|
|
20
|
+
columns: SQLiteWriterColumn<T>[];
|
|
21
|
+
}
|
|
22
|
+
export declare class SQLiteWriter<T> {
|
|
23
|
+
/**
|
|
24
|
+
* Get the number of rows in the table.
|
|
25
|
+
* @returns The number of rows.
|
|
26
|
+
*/
|
|
27
|
+
get size(): number;
|
|
28
|
+
private batch;
|
|
29
|
+
private batchSize;
|
|
30
|
+
private columns;
|
|
31
|
+
private databaseInstance;
|
|
32
|
+
private insertStatement;
|
|
33
|
+
private instanceName;
|
|
34
|
+
constructor(params: SQLiteWriterParams<T>);
|
|
35
|
+
all(whereClause?: string, params?: (boolean | number | string)[]): T[];
|
|
36
|
+
/**
|
|
37
|
+
* Clears all entries from the map.
|
|
38
|
+
*/
|
|
39
|
+
clear(): void;
|
|
40
|
+
/**
|
|
41
|
+
* Flush current buffer into DB synchronously.
|
|
42
|
+
*/
|
|
43
|
+
flush(): void;
|
|
44
|
+
/**
|
|
45
|
+
* Get a single row by column value.
|
|
46
|
+
* @param col The column to filter by.
|
|
47
|
+
* @param value The value to match.
|
|
48
|
+
* @returns The matching row, or undefined if not found.
|
|
49
|
+
*/
|
|
50
|
+
get<K extends keyof T>(col: K, value: T[K]): T | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Check if a row exists by column value.
|
|
53
|
+
* @param col The column to filter by.
|
|
54
|
+
* @param value The value to match.
|
|
55
|
+
* @returns True if the row exists, false otherwise.
|
|
56
|
+
*/
|
|
57
|
+
has<K extends keyof T>(col: K, value: T[K]): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Add one item to buffer, flush automatically when batchSize reached.
|
|
60
|
+
*/
|
|
61
|
+
write(item: T): void;
|
|
62
|
+
}
|
|
63
|
+
export {};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
import { generateRandomString } from '../random/generate-random-string.js';
|
|
3
|
+
import BSQLite3 from 'better-sqlite3';
|
|
4
|
+
/* * */
|
|
5
|
+
export class SQLiteWriter {
|
|
6
|
+
//
|
|
7
|
+
/**
|
|
8
|
+
* Get the number of rows in the table.
|
|
9
|
+
* @returns The number of rows.
|
|
10
|
+
*/
|
|
11
|
+
get size() {
|
|
12
|
+
const sql = `SELECT COUNT(*) as count FROM ${this.instanceName}`;
|
|
13
|
+
const row = this.databaseInstance.prepare(sql).get();
|
|
14
|
+
return row.count;
|
|
15
|
+
}
|
|
16
|
+
batch = [];
|
|
17
|
+
batchSize = 3000;
|
|
18
|
+
columns;
|
|
19
|
+
databaseInstance;
|
|
20
|
+
insertStatement;
|
|
21
|
+
instanceName;
|
|
22
|
+
constructor(params) {
|
|
23
|
+
//
|
|
24
|
+
//
|
|
25
|
+
// Set up options
|
|
26
|
+
this.batchSize = params.batch_size ?? 3000;
|
|
27
|
+
this.columns = params.columns;
|
|
28
|
+
this.instanceName = generateRandomString({ type: 'alphabetic' });
|
|
29
|
+
//
|
|
30
|
+
// Create a new SQLite instance
|
|
31
|
+
this.databaseInstance = new BSQLite3(`/tmp/${this.instanceName}.db`);
|
|
32
|
+
//
|
|
33
|
+
// Setup table columns from params
|
|
34
|
+
const preparedColumns = [];
|
|
35
|
+
const preparedIndexes = [];
|
|
36
|
+
for (const columnSpec of this.columns) {
|
|
37
|
+
// Extract column definition
|
|
38
|
+
const parts = [`"${columnSpec.name}"`, columnSpec.type];
|
|
39
|
+
// Set column preferences
|
|
40
|
+
if (columnSpec.not_null)
|
|
41
|
+
parts.push('NOT NULL');
|
|
42
|
+
if (columnSpec.primary_key)
|
|
43
|
+
parts.push('PRIMARY KEY');
|
|
44
|
+
// Save the column definition
|
|
45
|
+
preparedColumns.push(parts.join(' '));
|
|
46
|
+
// Save the index definition
|
|
47
|
+
if (columnSpec.indexed)
|
|
48
|
+
preparedIndexes.push(`CREATE INDEX IF NOT EXISTS idx_${this.instanceName}_${columnSpec.name} ON ${this.instanceName}("${columnSpec.name}")`);
|
|
49
|
+
}
|
|
50
|
+
//
|
|
51
|
+
// Create the table with the prepared columns and indexes
|
|
52
|
+
this.databaseInstance.pragma('journal_mode = WAL');
|
|
53
|
+
this.databaseInstance.pragma('synchronous = OFF');
|
|
54
|
+
this.databaseInstance.pragma('temp_store = MEMORY');
|
|
55
|
+
this.databaseInstance
|
|
56
|
+
.prepare(`CREATE TABLE IF NOT EXISTS ${this.instanceName} (${preparedColumns.join(',\n')})`)
|
|
57
|
+
.run();
|
|
58
|
+
preparedIndexes.forEach(i => this.databaseInstance.exec(i));
|
|
59
|
+
//
|
|
60
|
+
// Prepare insert statement
|
|
61
|
+
const placeholders = this.columns.map(() => '?').join(', ');
|
|
62
|
+
const insertSQL = `INSERT INTO ${this.instanceName} (${this.columns.map(c => `"${c.name}"`).join(', ')}) VALUES (${placeholders})`;
|
|
63
|
+
this.insertStatement = this.databaseInstance.prepare(insertSQL);
|
|
64
|
+
//
|
|
65
|
+
}
|
|
66
|
+
all(whereClause = '', params = []) {
|
|
67
|
+
const sql = `SELECT * FROM ${this.instanceName} ${whereClause}`;
|
|
68
|
+
return this.databaseInstance.prepare(sql).all(...params);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Clears all entries from the map.
|
|
72
|
+
*/
|
|
73
|
+
clear() {
|
|
74
|
+
this.databaseInstance
|
|
75
|
+
.prepare(`DELETE FROM ${this.instanceName}`)
|
|
76
|
+
.run();
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Flush current buffer into DB synchronously.
|
|
80
|
+
*/
|
|
81
|
+
flush() {
|
|
82
|
+
// Skip if batch is empty
|
|
83
|
+
if (this.batch.length === 0)
|
|
84
|
+
return;
|
|
85
|
+
// Prepare the operation
|
|
86
|
+
const insertManyOperation = this.databaseInstance.transaction((rows) => {
|
|
87
|
+
rows.forEach((row) => {
|
|
88
|
+
// Populate the columns with the row values
|
|
89
|
+
// to ensure the order of placeholders is preserved
|
|
90
|
+
const rowValues = this.columns.map(col => row[col.name]);
|
|
91
|
+
this.insertStatement.run(rowValues);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
// Run the operation
|
|
95
|
+
insertManyOperation(this.batch);
|
|
96
|
+
// Empty batch
|
|
97
|
+
this.batch = [];
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Get a single row by column value.
|
|
101
|
+
* @param col The column to filter by.
|
|
102
|
+
* @param value The value to match.
|
|
103
|
+
* @returns The matching row, or undefined if not found.
|
|
104
|
+
*/
|
|
105
|
+
get(col, value) {
|
|
106
|
+
const sql = `SELECT * FROM ${this.instanceName} WHERE ${String(col)} = ? LIMIT 1`;
|
|
107
|
+
return this.databaseInstance.prepare(sql).get(value);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Check if a row exists by column value.
|
|
111
|
+
* @param col The column to filter by.
|
|
112
|
+
* @param value The value to match.
|
|
113
|
+
* @returns True if the row exists, false otherwise.
|
|
114
|
+
*/
|
|
115
|
+
has(col, value) {
|
|
116
|
+
const sql = `SELECT 1 FROM ${this.instanceName} WHERE ${String(col)} = ? LIMIT 1`;
|
|
117
|
+
return !!this.databaseInstance.prepare(sql).get(value);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Add one item to buffer, flush automatically when batchSize reached.
|
|
121
|
+
*/
|
|
122
|
+
write(item) {
|
|
123
|
+
this.batch.push(item);
|
|
124
|
+
if (this.batch.length >= this.batchSize)
|
|
125
|
+
this.flush();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -9,9 +9,13 @@ import { generateRandomNumber } from './generate-random-number.js';
|
|
|
9
9
|
export function generateRandomString({ length = 6, type = 'alphanumeric' } = {}) {
|
|
10
10
|
//
|
|
11
11
|
const numericSet = '0123456789';
|
|
12
|
+
const alphabeticSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
12
13
|
const alphanumericSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
13
14
|
let allowedCharacters;
|
|
14
15
|
switch (type) {
|
|
16
|
+
case 'alphabetic':
|
|
17
|
+
allowedCharacters = alphabeticSet;
|
|
18
|
+
break;
|
|
15
19
|
case 'numeric':
|
|
16
20
|
allowedCharacters = numericSet;
|
|
17
21
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tmlmobilidade/utils",
|
|
3
|
-
"version": "20250827.
|
|
3
|
+
"version": "20250827.1349.36",
|
|
4
4
|
"author": "João de Vasconcelos & Jusi Monteiro",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"homepage": "https://github.com/tmlmobilidade/services#readme",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"@carrismetropolitana/eslint": "20250622.1204.50",
|
|
55
55
|
"@tmlmobilidade/tsconfig": "*",
|
|
56
56
|
"@tmlmobilidade/types": "*",
|
|
57
|
+
"@types/better-sqlite3": "7.6.13",
|
|
57
58
|
"@types/luxon": "3.7.1",
|
|
58
59
|
"@types/node": "24.3.0",
|
|
59
60
|
"@types/papaparse": "5.3.16",
|