Skip to content
This repository was archived by the owner on May 22, 2025. It is now read-only.

Commit 9cf88e7

Browse files
committed
Add initial sqlite implementation
1 parent e059f1f commit 9cf88e7

File tree

5 files changed

+536
-16
lines changed

5 files changed

+536
-16
lines changed

documentation.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,9 +272,10 @@ gtt report --output=markdown
272272
gtt report --output=csv
273273
gtt report --output=pdf --file=filename.pdf
274274
gtt report --output=xlsx --file=filename.xlsx
275+
gtt report --output=sqlite --file=filename.sqlite
275276
```
276277

277-
Defaults to `table`. `csv` and `markdown` can be printed to stdout, `pdf` and `xlsx` need the file parameter.
278+
Defaults to `table`. `csv` and `markdown` can be printed to stdout, `pdf`, `xlsx` and `sqlite` need the file parameter.
278279

279280
#### Print the output to a file
280281

@@ -589,7 +590,7 @@ timeFormat:
589590
timezone: "Europe/Berlin"
590591

591592
# Output type
592-
# Available: csv, table, markdown, pdf, xlsx
593+
# Available: csv, table, markdown, pdf, xlsx, sqlite
593594
# defaults to table
594595
output: markdown
595596

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"request": "^2.87.0",
5757
"request-promise-native": "^1.0.4",
5858
"shelljs": "^0.8.3",
59+
"sqlite3": "^4.0.6",
5960
"tempfile": "^2.0.0",
6061
"underscore": "^1.9.1",
6162
"xdg-basedir": "^3.0.0",

src/gtt-report.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ const Output = {
1515
pdf: require('./output/pdf'),
1616
markdown: require('./output/markdown'),
1717
dump: require('./output/dump'),
18-
xlsx: require('./output/xlsx')
18+
xlsx: require('./output/xlsx'),
19+
sqlite: require('./output/sqlite')
1920
};
2021

2122
// this collects options
@@ -181,6 +182,10 @@ if (config.get('output') === 'xlsx' && !config.get('file')) {
181182
Cli.error(`Cannot output an xlsx to stdout. You probably forgot to use the --file parameter`);
182183
}
183184

185+
if (config.get('output') === 'sqlite' && !config.get('file')) {
186+
Cli.error(`Cannot output an sqlite to stdout. You probably forgot to use the --file parameter`);
187+
}
188+
184189
// file prompt
185190
new Promise(resolve => {
186191
if (config.get('file') && fs.existsSync(config.get('file'))) {

src/output/sqlite.js

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
const _ = require('underscore');
2+
const fs = require('fs');
3+
const path = require('path');
4+
const sqlite3 = require('sqlite3').verbose();
5+
6+
const Base = require('./base');
7+
const Cli = require('./../include/cli');
8+
9+
10+
/*
11+
* Constants
12+
*/
13+
const TABLE_ISSUES = 'issues';
14+
const TABLE_MERGE_REQUESTS = 'merge_requests';
15+
const TABLE_TIMES = 'times';
16+
17+
const COLUMNS = {
18+
[TABLE_ISSUES]: 'issueColumns',
19+
[TABLE_MERGE_REQUESTS]: 'mergeRequestColumns',
20+
[TABLE_TIMES]: 'recordColumns'
21+
};
22+
23+
24+
/**
25+
* Returns the SQLite type for a specific column name.
26+
* Note that this is a general mapper, i.e. there is no distinction between table differences.
27+
*
28+
* @param columnName
29+
* @returns {string}
30+
*/
31+
function getGeneralColumnType(columnName) {
32+
if(['iid'].includes(columnName)) {
33+
return 'INTEGER PRIMARY KEY';
34+
}
35+
36+
if(['id', 'project_id'].includes(columnName)) {
37+
return 'INTEGER';
38+
}
39+
40+
if(['spent', 'total_spent', 'total_estimate', 'time'].includes(columnName)) {
41+
return 'REAL';
42+
}
43+
44+
if(['date', 'updated_at', 'created_at', 'due_date'].includes(columnName)) {
45+
return 'DATE';
46+
}
47+
48+
// default:
49+
return 'TEXT';
50+
}
51+
52+
53+
/**
54+
* Data required to create a SQLite table and fill with records.
55+
*/
56+
class Table {
57+
58+
/**
59+
*
60+
* @param name of the Table
61+
* @param columns Array of column names
62+
* @param records Array of records to insert
63+
*/
64+
constructor(name, columns, records) {
65+
this.name = name;
66+
this.columns = columns;
67+
this.records = records;
68+
}
69+
}
70+
71+
72+
class SqliteDatabaseAbstraction {
73+
74+
constructor(file) {
75+
this.file = file;
76+
}
77+
78+
/**
79+
* Opens the SQLite database in write mode
80+
*
81+
* @returns {Promise<any>}
82+
*/
83+
open() {
84+
return new Promise((resolve, reject) => {
85+
this.database = new sqlite3.Database(this.file, err => err? reject(err) : resolve());
86+
});
87+
}
88+
89+
/**
90+
* Closes the SQLite database before shutdown
91+
*
92+
* @returns {Promise<any>}
93+
*/
94+
close() {
95+
return new Promise((resolve, reject) => {
96+
this.database.close(err => err? reject(err) : resolve());
97+
});
98+
}
99+
100+
/**
101+
* Creates a table and inserts the records.
102+
*
103+
* @param table
104+
* @returns {Promise<void>}
105+
*/
106+
async buildTable(table) {
107+
const cols = table.columns.map(name => [name, getGeneralColumnType(name)]);
108+
await this.createTable(table.name, cols);
109+
110+
await this.insertRecords(table);
111+
}
112+
113+
/**
114+
* Inserts multiple records according to a column list into a table.
115+
*
116+
* @param table
117+
* @returns {Promise<any>}
118+
*/
119+
insertRecords(table) {
120+
return new Promise((resolve, reject) => {
121+
const stmt = this.database.prepare(`INSERT INTO ${table.name} VALUES (${table.columns.map(() => '?').join(', ')})`);
122+
for (const record of table.records) {
123+
stmt.run(...record)
124+
}
125+
126+
stmt.finalize((err) => {
127+
if(err) {
128+
reject(err);
129+
} else {
130+
resolve();
131+
}
132+
});
133+
});
134+
}
135+
136+
/**
137+
* Creates a new table in the SQLite Database
138+
*
139+
* @param name
140+
* @param columnNameTypes
141+
* @returns {Promise<any>}
142+
*/
143+
async createTable(name, columnNameTypes) {
144+
await new Promise((resolve, reject) => {
145+
this.database.run(`DROP TABLE IF EXISTS ${name}`, (err) => err? reject(err): resolve());
146+
});
147+
148+
await new Promise((resolve, reject) => {
149+
const columns = columnNameTypes.map(col => `${col[0]} ${col[1]}`).join(', ');
150+
this.database.run(`CREATE TABLE ${name} (${columns})`, (err) => err? reject(err): resolve());
151+
});
152+
}
153+
154+
/**
155+
* Creates a new view in the SQLite Database
156+
* @returns {Promise<void>}
157+
*/
158+
async createView(view) {
159+
await new Promise((resolve, reject) => {
160+
this.database.run(`DROP VIEW IF EXISTS ${view[0]}`, (err) => err? reject(err): resolve());
161+
});
162+
163+
await new Promise((resolve, reject) => {
164+
this.database.run(`CREATE VIEW ${view[0]} AS ${view[1]}`, (err) => err? reject(err): resolve());
165+
});
166+
}
167+
}
168+
169+
/**
170+
* sqlite output
171+
*/
172+
class Sqlite extends Base {
173+
174+
constructor(config, report) {
175+
super(config, report);
176+
this.tables = new Map();
177+
this.stats = new Map();
178+
}
179+
180+
makeStats() {
181+
this.stats.set('view_time_per_user', 'SELECT user, SUM(time) FROM times GROUP BY user');
182+
183+
// General Time Tracking summaries for some columns over multiple tables
184+
const queries = [];
185+
const fieldTables = [TABLE_ISSUES, TABLE_TIMES];
186+
const columns = ['total_estimate', 'total_spent', 'spent'];
187+
188+
for(const column of columns) {
189+
const subTableQueries = fieldTables
190+
.filter(table => this.config.get(COLUMNS[table]).includes(column))
191+
.map(table => `SELECT ${column} FROM ${table}`)
192+
.join(' UNION ALL ');
193+
194+
queries.push(`SELECT '${column}', SUM(${column}) FROM (${subTableQueries})`)
195+
}
196+
this.stats.set('view_time_stats', queries.join(' UNION ALL '))
197+
}
198+
199+
makeIssues() {
200+
this.makeTable(TABLE_ISSUES, this.report.issues);
201+
}
202+
203+
makeMergeRequests() {
204+
this.makeTable(TABLE_MERGE_REQUESTS, this.report.mergeRequests);
205+
}
206+
207+
makeRecords() {
208+
this.makeTable(TABLE_TIMES, this.times);
209+
}
210+
211+
makeTable(name, data) {
212+
const columns = this.config.get(COLUMNS[name]);
213+
const preparedData = data.map(record => this.prepare(record, columns));
214+
215+
const table = new Table(name, columns, preparedData);
216+
this.tables.set(name,table);
217+
}
218+
219+
toFile(file, resolve) {
220+
this.db = new SqliteDatabaseAbstraction(file);
221+
this.provisionDatabase()
222+
.then(() => resolve())
223+
.catch((err) => Cli.error("SQLITE: Error while building the database", err));
224+
}
225+
226+
async provisionDatabase() {
227+
await this.db.open();
228+
229+
for(const table of this.tables) {
230+
await this.db.buildTable(table[1]);
231+
}
232+
233+
if (this.config.get('report').includes('stats')) {
234+
for(const view of this.stats) {
235+
await this.db.createView(view);
236+
}
237+
}
238+
239+
await this.db.close();
240+
}
241+
242+
toStdOut() {
243+
Cli.error(`Can't output sqlite to std out`);
244+
}
245+
246+
247+
248+
/**
249+
* prepare the given object by converting numeric
250+
* columns/properties as numbers instead of strings
251+
* on top of what the parent method already does
252+
*
253+
* suboptimally done here to avoid impacts on other outputs
254+
*
255+
* @param obj
256+
* @param columns
257+
* @returns {Array}
258+
*/
259+
prepare(obj = {}, columns = []) {
260+
let formattedObj = super.prepare(obj, columns);
261+
return formattedObj.map(field => isNaN(field) ? field : Number(field));
262+
}
263+
}
264+
265+
module.exports = Sqlite;

0 commit comments

Comments
 (0)