├── .github └── asciicast.gif ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── app ├── cmd-delete.js ├── cmd-edit.js ├── cmd-log.js ├── cmd-write.js ├── config.js ├── docopt.txt ├── index.js ├── util-decorate.js └── util-editor.js ├── example-didrc ├── package.json └── test ├── cmd-delete.js ├── cmd-edit.js ├── cmd-log.js ├── cmd-write.js ├── mock ├── config.js ├── db.js └── editor.js ├── util-decorate.js └── util-editor.js /.github/asciicast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisallenlane/node-did/996d76eeb08017e09484526337cf8f5803d1b1f6/.github/asciicast.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "0.10" 5 | - "0.11" 6 | - "0.12" 7 | - "4.0" 8 | - "4.1" 9 | - "4.2" 10 | - "4.3" 11 | - "4.4" 12 | - "5.0" 13 | - "5.1" 14 | - "5.10" 15 | - "5.11" 16 | - "5.2" 17 | - "5.3" 18 | - "5.4" 19 | - "5.5" 20 | - "5.6" 21 | - "5.7" 22 | - "5.8" 23 | - "5.9" 24 | - "6.0" 25 | - "6.1" 26 | 27 | env: 28 | - CXX=g++-4.8 29 | 30 | addons: 31 | apt: 32 | sources: 33 | - ubuntu-toolchain-r-test 34 | packages: 35 | - g++-4.8 36 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | Please "fork and pull": 4 | 5 | https://help.github.com/articles/creating-a-pull-request-from-a-fork/ 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2017 Christopher Allen Lane 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/chrisallenlane/node-did.svg)](https://travis-ci.org/chrisallenlane/node-did) 2 | [![npm](https://img.shields.io/npm/v/node-did.svg)]() 3 | [![npm](https://img.shields.io/npm/dt/node-did.svg)]() 4 | [![Known Vulnerabilities](https://snyk.io/test/npm/node-did/badge.svg)](https://snyk.io/test/npm/node-did) 5 | 6 | did 7 | === 8 | A dead-simple, cli-based task journaler. It helps you remember what you _did_! 9 | 10 | ![asciicast](./.github/asciicast.gif) 11 | 12 | Installation 13 | ------------ 14 | `did` can be installed directly from `npm`: 15 | 16 | ```sh 17 | [sudo] npm install -g node-did 18 | ``` 19 | 20 | Usage 21 | ----- 22 | To record an entry: 23 | 24 | ```sh 25 | did Installed Gentoo. 26 | ``` 27 | 28 | To record an entry using `EDITOR` (will open in the terminal in focus): 29 | 30 | ```sh 31 | did 32 | ``` 33 | 34 | To view recent entries: 35 | 36 | ```sh 37 | did log 38 | ``` 39 | 40 | To view recent entries (last 3 only): 41 | 42 | ```sh 43 | did log -n3 44 | ``` 45 | 46 | To search for entries about Gentoo: 47 | 48 | ```sh 49 | did log -s gentoo 50 | ``` 51 | 52 | To search for entries about Gentoo between Monday and today: 53 | 54 | ```sh 55 | did log -s gentoo -f 'last monday' -u 'today' 56 | ``` 57 | 58 | To view the above in ascending order (ie, oldest first): 59 | 60 | ```sh 61 | did log -s gentoo -f 'last monday' -u 'today' -a 62 | ``` 63 | 64 | To edit entry `2`: 65 | 66 | ```sh 67 | did edit 2 Hacked the Gibson. 68 | ``` 69 | 70 | To edit entry `2` within `EDITOR`: 71 | 72 | ```sh 73 | did edit 2 74 | ``` 75 | 76 | To delete entry `2`: 77 | 78 | ```sh 79 | did delete 2 80 | ``` 81 | 82 | To delete entries `2` and `3`: 83 | 84 | ```sh 85 | did delete 2 3 86 | ``` 87 | 88 | Tagging 89 | ------- 90 | `did` provides full-text searching on log entries, and thus implicitly supports 91 | "tagging": 92 | 93 | ```sh 94 | $ did Installed Gentoo. +work 95 | $ did Freed Kevin. +personal 96 | $ did Hacked the planet. +work 97 | $ did log -s +personal 98 | 3 Freed Kevin. +personal 06 Jan | 05:29 PM 99 | ``` 100 | 101 | You may choose any tagging convention. Note, however, that `#` must be enclosed 102 | within quotations to prevent the shell from parsing it as a comment: 103 | 104 | ```sh 105 | $ did 'Hacked the planet. #work' 106 | ``` 107 | 108 | Additional Information 109 | ---------------------- 110 | Additional information can be found in the [wiki][]: 111 | 112 | - [Configuring][] 113 | - [Supported Platforms][] 114 | 115 | [Configuring]: https://github.com/chrisallenlane/node-did/wiki/Configuring 116 | [Supported Platforms]: https://github.com/chrisallenlane/node-did/wiki/Supported-Platforms 117 | [wiki]: https://github.com/chrisallenlane/node-did/wiki 118 | -------------------------------------------------------------------------------- /app/cmd-delete.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config, options, db, callback) { 2 | 3 | // buffer the id 4 | const id = options['']; 5 | 6 | // assemble prepared-statement placeholders for each id. (Placeholders may be 7 | // used for scalar values only. We may not use an array here.) 8 | const params = id.map(function() { return '?'; }).join(', '); 9 | 10 | // concatenate the database query 11 | const query = 'DELETE FROM Log WHERE id IN (' + params + ')'; 12 | 13 | // called back after query is run 14 | const cb = function(err) { 15 | 16 | // fail if an error occured 17 | if (err) { return callback(err); } 18 | 19 | // fail if no records were deleted 20 | if (this.changes === 0) { 21 | return callback(new Error('ID ' + id.join(', ') + ' is invalid.')); 22 | } 23 | 24 | callback(); 25 | }; 26 | 27 | // dynamically apply the arguments. (NB: we aren't using the spread [...] 28 | // operator here in order to maintain backwards-compatibility with node as 29 | // far back as we can.) 30 | db.run.apply(db, [ query ].concat(id, cb)); 31 | }; 32 | -------------------------------------------------------------------------------- /app/cmd-edit.js: -------------------------------------------------------------------------------- 1 | const editor = require('./util-editor'); 2 | 3 | module.exports = function(config, options, db, callback) { 4 | 5 | const entry = options[''].join(' ') || ''; 6 | const id = options[''][0]; 7 | 8 | // If the entry was supplied as a command-line argument, insert it 9 | // immediately 10 | if (entry !== '') { 11 | update(db, id, entry, callback); 12 | } 13 | 14 | // otherwise, open $EDITOR and write/read to/from a temp file, which is 15 | // pre-populated with the old text 16 | else { 17 | 18 | const query = 'SELECT entry FROM Log WHERE id = ?'; 19 | db.get(query, id, function(err, row) { 20 | if (err) { return callback(err); } 21 | 22 | editor(config, row.entry, function(err, entry) { 23 | if (err) { return callback(err); } 24 | update(db, id, entry, callback); 25 | }); 26 | }); 27 | 28 | } 29 | }; 30 | 31 | // updates the database 32 | const update = function(db, id, entry, callback) { 33 | 34 | // trim the entry 35 | entry = entry.trim(); 36 | 37 | // fail if the entry is empty 38 | if (entry === '') { 39 | return callback(new Error('Entry is empty. Aborting.')); 40 | } 41 | 42 | // run the query 43 | const query = 'UPDATE Log SET entry = ? WHERE id = ?'; 44 | db.run(query, entry, id, function(err) { 45 | 46 | // call back with error on error 47 | if (err) { return callback(err); } 48 | 49 | // call back with an error if an invalid was provided 50 | if (this.changes === 0) { 51 | return callback(new Error(' is invalid. No log entries updated.')); 52 | } 53 | 54 | // otherwise, the update was successful 55 | callback(); 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /app/cmd-log.js: -------------------------------------------------------------------------------- 1 | const decorate = require('./util-decorate'); 2 | require('datejs'); 3 | 4 | module.exports = function(config, options, db, callback) { 5 | 6 | var from; 7 | var until; 8 | 9 | // build the 'from' and 'until' phrases 10 | try { 11 | from = (options['--from']) 12 | ? Date.parse(options['--from']) .getTime() 13 | : 0 ; 14 | } catch (e) { 15 | return callback(new Error( 16 | 'Could not parse "' + options['--from'] + '" as a date.' 17 | )); 18 | } 19 | try { 20 | until = (options['--until']) 21 | ? Date.parse(options['--until']) .getTime() 22 | : 9999999999999 ; 23 | } catch (e) { 24 | return callback(new Error( 25 | 'Could not parse "' + options['--until'] + '" as a date.' 26 | )); 27 | } 28 | 29 | // build the 'search' phrase 30 | const search = (options['--search']) 31 | ? '%' + options['--search'] + '%' 32 | : '%' ; 33 | 34 | // build the 'order' phrase 35 | const order = (options['--ascending']) 36 | ? 'ASC' 37 | : 'DESC' ; 38 | 39 | // parse the limit 40 | const limit = options['--number'] || 10; 41 | 42 | // construct the query 43 | const query = [ 44 | 'SELECT *', 45 | 'FROM Log', 46 | 'WHERE timestamp >= ?', 47 | 'AND timestamp <= ?', 48 | 'AND entry LIKE ?', 49 | 'ORDER BY id ' + order, 50 | 'LIMIT ?', 51 | ].join(' '); 52 | 53 | // run the query 54 | db.all(query, from, until, search, limit, function(err, rows) { 55 | if (err) { return callback(err); } 56 | callback(null, decorate(config, options, rows)); 57 | }); 58 | 59 | }; 60 | -------------------------------------------------------------------------------- /app/cmd-write.js: -------------------------------------------------------------------------------- 1 | const editor = require('./util-editor'); 2 | 3 | module.exports = function(config, options, db, callback) { 4 | 5 | const entry = options[''].join(' ') || ''; 6 | 7 | // If the entry was supplied as a command-line argument, insert it 8 | // immediately 9 | if (entry !== '') { 10 | insert(db, entry, callback); 11 | } 12 | 13 | // otherwise, open $EDITOR and write/read to/from a temp file 14 | else { 15 | editor(config, function(err, entry) { 16 | if (err) { return callback(err); } 17 | insert(db, entry, callback); 18 | }); 19 | } 20 | }; 21 | 22 | // inserts into the database 23 | const insert = function(db, entry, callback) { 24 | 25 | // trim the entry 26 | entry = entry.trim(); 27 | 28 | // fail if the entry is empty 29 | if (entry === '') { 30 | return callback(new Error('Entry is empty. Aborting.')); 31 | } 32 | 33 | // run the query 34 | const query = 'INSERT INTO Log(entry, timestamp) VALUES(?, ?)'; 35 | db.run(query, entry, new Date(), callback); 36 | }; 37 | -------------------------------------------------------------------------------- /app/config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('rc')('did', { 2 | 3 | // path to the database 4 | database: '~/.did.sqlite3', 5 | 6 | // colorized output 7 | color: { 8 | id : 'cyan', 9 | timestamp : 'green', 10 | entry : 'white', 11 | match : 'yellow', 12 | }, 13 | 14 | // timestamp display format 15 | dateFormat : 'dd MMM | hh:mm tt', 16 | 17 | // EDITOR 18 | editor : process.env.EDITOR, 19 | }); 20 | -------------------------------------------------------------------------------- /app/docopt.txt: -------------------------------------------------------------------------------- 1 | did 2 | 3 | A dead-simple, cli-based task journaler. 4 | 5 | Usage: 6 | did edit []... 7 | did delete ... 8 | did log [options] 9 | did []... 10 | 11 | Options: 12 | -h --help Show this screen. 13 | --version Show version. 14 | -s --search= Search for entries matching . 15 | -f --from= Return entries newer than . 16 | -u --until= Return entries older than . 17 | -n --number= Number of entries to return. 18 | -a --ascending Return entries in ascending order. 19 | 20 | Examples: 21 | 22 | To record an entry: 23 | did Put new cover sheets on TPS reports 24 | 25 | To record an entry using EDITOR: 26 | did 27 | 28 | To view recent entries: 29 | did log 30 | 31 | To view recent entries (last 3 only): 32 | did log -n3 33 | 34 | To search for entries about TPS reports: 35 | did log -s tps 36 | 37 | To search for entries about TPS reports between Monday and today: 38 | did log -s tps -f 'last monday' -u 'today' 39 | 40 | To view the above in ascending order (ie, oldest first): 41 | did log -s tps -f 'last monday' -u 'today' -a 42 | 43 | To edit entry 10: 44 | did edit 10 Located my stapler. 45 | 46 | To edit entry 10 within EDITOR: 47 | did edit 10 48 | 49 | To delete entry 10: 50 | did delete 10 51 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // make package.json accessible 4 | const pkg = require('../package.json'); 5 | 6 | // dependencies 7 | const config = require('./config'); 8 | const docopt = require('docopt').docopt; 9 | const expand = require('expand-tilde'); 10 | const fs = require('fs'); 11 | const path = require('path'); 12 | const sqlite3 = require('sqlite3').verbose(); 13 | 14 | // generate and parse the command-line options 15 | const doc = fs.readFileSync(path.join(__dirname, 'docopt.txt'), 'utf8'); 16 | const options = docopt(doc, { version: pkg.version }); 17 | 18 | // database 19 | config.database = expand(config.database); 20 | const existed = fs.existsSync(config.database); 21 | const db = new sqlite3.Database(config.database); 22 | 23 | // continue when the database has opened 24 | db.on('open', function() { 25 | 26 | // emit 'ready' if the database previously existed 27 | if (existed) { db.emit('ready'); } 28 | 29 | // otherwise, create the table 30 | else { 31 | const query = 'CREATE TABLE Log(id INTEGER PRIMARY KEY, entry TEXT, timestamp DATE)'; 32 | db.run(query, function(err) { 33 | if (err) { 34 | console.warn(err.message); 35 | process.exit(1); 36 | } 37 | 38 | db.emit('ready'); 39 | }); 40 | } 41 | }); 42 | 43 | // execute the subcommands when the database is ready 44 | db.on('ready', function() { 45 | 46 | // load the subcommands 47 | const cmd = { 48 | delete : require('./cmd-delete'), 49 | edit : require('./cmd-edit'), 50 | log : require('./cmd-log'), 51 | write : require('./cmd-write'), 52 | }; 53 | 54 | // execute the appropriate subcommand 55 | const fn = 56 | (options.delete) ? cmd.delete : 57 | (options.edit) ? cmd.edit : 58 | (options.log) ? cmd.log : cmd.write ; 59 | fn(config, options, db, function(err, output) { 60 | 61 | // handle errors 62 | if (err) { 63 | console.warn(err.message); 64 | process.exit(1); 65 | } 66 | 67 | // display output 68 | if (output) { 69 | console.log(output); 70 | } 71 | 72 | }); 73 | 74 | }); 75 | -------------------------------------------------------------------------------- /app/util-decorate.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const esc = require('escape-string-regexp'); 3 | const table = require('text-table'); 4 | 5 | module.exports = function(config, options, rows) { 6 | 7 | // transform each row into a formatted string 8 | const transformed = rows.map(function(row) { 9 | 10 | // colorize the id 11 | const id = chalk[config.color.id](row.id); 12 | 13 | // colorize and format the entry 14 | var entry = row.entry; 15 | 16 | // highlight matches if --search was provided 17 | if (options['--search']) { 18 | const global = new RegExp(esc(options['--search']), 'gi'); 19 | const local = new RegExp(esc(options['--search']), 'i'); 20 | entry.match(global).forEach(function(m) { 21 | entry = entry.replace(local, chalk[config.color.match](m)); 22 | }); 23 | } 24 | // also apply a configured color to the rest of the entry string 25 | entry = chalk[config.color.entry](entry); 26 | 27 | // colorize and format the timestamp 28 | const timestamp = chalk[config.color.timestamp]( 29 | new Date(row.timestamp).toString(config.dateFormat) 30 | ); 31 | 32 | // return the formatted row 33 | return [ id, entry, timestamp ]; 34 | }); 35 | 36 | // return the formatted strings laid out as a table 37 | return table(transformed); 38 | 39 | }; 40 | -------------------------------------------------------------------------------- /app/util-editor.js: -------------------------------------------------------------------------------- 1 | const child_process = require('child_process'); 2 | const fs = require('fs'); 3 | const tmp = require('tmp'); 4 | 5 | module.exports = function(config, entry, callback) { 6 | 7 | // make "entry" text optional 8 | if (! callback) { 9 | callback = entry; 10 | entry = null; 11 | } 12 | 13 | // Determine which EDITOR is set, and error out if unset. 14 | const editor = config.editor || false; 15 | if (! editor) { 16 | return callback(new Error('EDITOR is not set. Aborting.')); 17 | } 18 | 19 | // create a temporary file 20 | const file = tmp.fileSync().name; 21 | 22 | // if `entry` was provided, populate the temp file with it 23 | if (entry !== null) { 24 | fs.writeFileSync(file, entry); 25 | } 26 | 27 | // spawn the editor in the active shell 28 | const child = child_process.spawn(editor, [ file ], { stdio: 'inherit' }); 29 | 30 | // call back with the file's contents when the editor is closed 31 | child.on('exit', function (code) { 32 | fs.readFile(file, 'utf8', callback); 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /example-didrc: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "database": "~/.did.sqlite3", 4 | 5 | "color": { 6 | "id" : "cyan", 7 | "timestamp" : "green", 8 | "entry" : "white", 9 | "match" : "yellow" 10 | }, 11 | 12 | "dateFormat" : "dd MMM | hh:mm tt", 13 | 14 | "editor" : "vim" 15 | } 16 | 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-did", 3 | "version": "1.1.2", 4 | "description": "A dead-simple, cli-based task journaler.", 5 | "author": { 6 | "name": "Chris Allen Lane", 7 | "email": "chris@chris-allen-lane.com" 8 | }, 9 | "license": "MIT", 10 | "scripts": { 11 | "lint": "jshint app test", 12 | "snyk": "snyk test", 13 | "test": "tape 'test/*.js' | faucet" 14 | }, 15 | "jshintConfig": { 16 | "esnext": true, 17 | "expr": true, 18 | "laxbreak": true 19 | }, 20 | "dependencies": { 21 | "chalk": "^1.1.3", 22 | "datejs": "^1.0.0-rc3", 23 | "docopt": "0.6.2", 24 | "escape-string-regexp": "^1.0.5", 25 | "expand-tilde": "^2.0.2", 26 | "rc": "1.1.6", 27 | "sqlite3": "^3.1.8", 28 | "text-table": "^0.2.0", 29 | "tmp": "0.0.31" 30 | }, 31 | "devDependencies": { 32 | "faucet": "0.0.1", 33 | "jshint": "2.9.4", 34 | "snyk": "^1.23.3", 35 | "tape": "^4.6.0" 36 | }, 37 | "keywords": [ 38 | "task", 39 | "journal" 40 | ], 41 | "repository": { 42 | "type": "git", 43 | "url": "git@github.com:chrisallenlane/node-did.git" 44 | }, 45 | "bugs": { 46 | "url": "https://github.com/chrisallenlane/node-did/issues" 47 | }, 48 | "bin": { 49 | "did": "app/index.js" 50 | }, 51 | "main": "app/index.js" 52 | } 53 | -------------------------------------------------------------------------------- /test/cmd-delete.js: -------------------------------------------------------------------------------- 1 | const Db = require('./mock/db'); 2 | const config = require('./mock/config'); 3 | const del = require('../app/cmd-delete'); 4 | const test = require('tape'); 5 | 6 | 7 | test('cmd-delete: should delete a log entry', function(t) { 8 | t.plan(6); 9 | 10 | // mock clean database for each test 11 | Db(function(err, db) { 12 | t.notOk(err, 'could not mock database (1)'); 13 | 14 | const options = { 15 | '' : [ 3 ], 16 | }; 17 | 18 | del(config, options, db, function(err) { 19 | t.notOk(err); 20 | 21 | const query = 'SELECT * FROM Log'; 22 | db.all(query, function(err, rows) { 23 | t.notOk(err); 24 | t.equals(rows.length, 2); 25 | t.equals(rows[0].id, 1); 26 | t.equals(rows[1].id, 2); 27 | }); 28 | }); 29 | }); 30 | 31 | }); 32 | 33 | test('cmd-delete: should delete multiple log entries', function(t) { 34 | t.plan(5); 35 | 36 | // mock clean database for each test 37 | Db(function(err, db) { 38 | t.notOk(err, 'could not mock database (1)'); 39 | 40 | const options = { 41 | '' : [ 2, 3 ], 42 | }; 43 | 44 | del(config, options, db, function(err) { 45 | t.notOk(err); 46 | 47 | const query = 'SELECT * FROM Log'; 48 | db.all(query, function(err, rows) { 49 | t.notOk(err); 50 | t.equals(rows.length, 1); 51 | t.equals(rows[0].id, 1); 52 | }); 53 | }); 54 | }); 55 | 56 | }); 57 | 58 | test('cmd-delete: should error on invalid ', function(t) { 59 | t.plan(4); 60 | 61 | // mock clean database for each test 62 | Db(function(err, db) { 63 | t.notOk(err, 'could not mock database (1)'); 64 | 65 | const options = { 66 | '' : [ 'xxx' ], 67 | }; 68 | 69 | del(config, options, db, function(err) { 70 | t.ok(err); 71 | 72 | const query = 'SELECT * FROM Log'; 73 | db.all(query, function(err, rows) { 74 | t.notOk(err); 75 | t.equals(rows.length, 3); 76 | }); 77 | }); 78 | }); 79 | 80 | }); 81 | -------------------------------------------------------------------------------- /test/cmd-edit.js: -------------------------------------------------------------------------------- 1 | const Db = require('./mock/db'); 2 | const config = require('./mock/config'); 3 | const edit = require('../app/cmd-edit'); 4 | const test = require('tape'); 5 | 6 | test('cmd-edit: should edit a log entry (entry via cli)', function(t) { 7 | t.plan(10); 8 | 9 | // mock clean database for each test 10 | Db(function(err, db) { 11 | t.notOk(err, 'could not mock database'); 12 | 13 | const options = { 14 | '' : [ '3' ], 15 | '' : [ 'qux' ], 16 | }; 17 | 18 | edit(config, options, db, function(err) { 19 | t.notOk(err); 20 | 21 | const query = 'SELECT * FROM Log'; 22 | db.all(query, function(err, rows) { 23 | t.notOk(err); 24 | t.equals(rows.length, 3); 25 | t.equals(rows[0].id, 1); 26 | t.equals(rows[0].entry, 'foo +test'); 27 | t.equals(rows[1].id, 2); 28 | t.equals(rows[1].entry, 'bar'); 29 | t.equals(rows[2].id, 3); 30 | t.equals(rows[2].entry, 'qux'); 31 | }); 32 | }); 33 | }); 34 | 35 | }); 36 | 37 | test('cmd-edit: should edit a log entry (entry via editor)', function(t) { 38 | t.plan(10); 39 | 40 | // mock clean database for each test 41 | Db(function(err, db) { 42 | t.notOk(err, 'could not mock database'); 43 | 44 | const options = { 45 | '' : [ '3' ], 46 | '' : [], 47 | }; 48 | 49 | edit(config, options, db, function(err) { 50 | t.notOk(err); 51 | 52 | const query = 'SELECT * FROM Log'; 53 | db.all(query, function(err, rows) { 54 | t.notOk(err); 55 | t.equals(rows.length, 3); 56 | t.equals(rows[0].id, 1); 57 | t.equals(rows[0].entry, 'foo +test'); 58 | t.equals(rows[1].id, 2); 59 | t.equals(rows[1].entry, 'bar'); 60 | t.equals(rows[2].id, 3); 61 | t.equals(rows[2].entry, 'quux'); 62 | }); 63 | }); 64 | }); 65 | 66 | }); 67 | 68 | test('cmd-edit: should call back with error when is invalid', function(t) { 69 | t.plan(2); 70 | 71 | // mock clean database for each test 72 | Db(function(err, db) { 73 | t.notOk(err, 'could not mock database'); 74 | 75 | const options = { 76 | '' : [ 'foo' ], 77 | '' : [ 'qux' ], 78 | }; 79 | 80 | edit(config, options, db, function(err) { 81 | t.equals(err.message, ' is invalid. No log entries updated.'); 82 | }); 83 | }); 84 | 85 | }); 86 | -------------------------------------------------------------------------------- /test/cmd-log.js: -------------------------------------------------------------------------------- 1 | const Db = require('./mock/db'); 2 | const config = require('./mock/config'); 3 | const eol = require('os').EOL; 4 | const log = require('../app/cmd-log'); 5 | const test = require('tape'); 6 | 7 | // connect to a mock database for testing 8 | Db(function(err, db) { 9 | 10 | if (err) { 11 | console.warn('Can\'t initialize test database.'); 12 | console.warn(err.message); 13 | process.exit(1); 14 | } 15 | 16 | test('cmd-log: should log correct output', function(t) { 17 | t.plan(5); 18 | const options = {}; 19 | 20 | log(config, options, db, function(err, output) { 21 | const lines = output.split(eol); 22 | t.notOk(err); 23 | t.equals(lines.length, 3); 24 | t.notEquals(lines[2].indexOf('foo'), -1); 25 | t.notEquals(lines[1].indexOf('bar'), -1); 26 | t.notEquals(lines[0].indexOf('baz'), -1); 27 | }); 28 | }); 29 | 30 | test('cmd-log: --number flag', function(t) { 31 | t.plan(3); 32 | const options = { 33 | '--number' : 1, 34 | }; 35 | 36 | log(config, options, db, function(err, output) { 37 | const lines = output.split(eol); 38 | t.notOk(err); 39 | t.equals(lines.length, 1); 40 | t.notEquals(lines[0].indexOf('baz'), -1); 41 | }); 42 | }); 43 | 44 | test('cmd-log: --ascending flag', function(t) { 45 | t.plan(5); 46 | const options = { 47 | '--ascending' : true, 48 | }; 49 | 50 | log(config, options, db, function(err, output) { 51 | const lines = output.split(eol); 52 | t.notOk(err); 53 | t.equals(lines.length, 3); 54 | t.notEquals(lines[0].indexOf('foo'), -1); 55 | t.notEquals(lines[1].indexOf('bar'), -1); 56 | t.notEquals(lines[2].indexOf('baz'), -1); 57 | }); 58 | }); 59 | 60 | test('cmd-log: --from flag', function(t) { 61 | t.plan(4); 62 | const options = { 63 | '--from' : 'January 2 2017', 64 | }; 65 | 66 | log(config, options, db, function(err, output) { 67 | const lines = output.split(eol); 68 | t.notOk(err); 69 | t.equals(lines.length, 2); 70 | t.notEquals(lines[0].indexOf('baz'), -1); 71 | t.notEquals(lines[1].indexOf('bar'), -1); 72 | }); 73 | }); 74 | 75 | test('cmd-log: --from flag - should error on invalid date', function(t) { 76 | t.plan(1); 77 | const options = { 78 | '--from' : 'xxx', 79 | }; 80 | 81 | log(config, options, db, function(err, output) { 82 | t.equals(err.message, 'Could not parse "xxx" as a date.'); 83 | }); 84 | }); 85 | 86 | test('cmd-log: --until flag', function(t) { 87 | t.plan(4); 88 | const options = { 89 | '--until' : 'January 2 2017', 90 | }; 91 | 92 | log(config, options, db, function(err, output) { 93 | const lines = output.split(eol); 94 | t.notOk(err); 95 | t.equals(lines.length, 2); 96 | t.notEquals(lines[0].indexOf('bar'), -1); 97 | t.notEquals(lines[1].indexOf('foo'), -1); 98 | }); 99 | }); 100 | 101 | test('cmd-log: --until flag - should throw on invalid date', function(t) { 102 | t.plan(1); 103 | const options = { 104 | '--until' : 'xxx', 105 | }; 106 | 107 | log(config, options, db, function(err, output) { 108 | t.equals(err.message, 'Could not parse "xxx" as a date.'); 109 | }); 110 | }); 111 | 112 | test('cmd-log: --search flag', function(t) { 113 | t.plan(3); 114 | const options = { 115 | '--search' : 'foo', 116 | }; 117 | 118 | log(config, options, db, function(err, output) { 119 | const lines = output.split(eol); 120 | t.notOk(err); 121 | t.equals(lines.length, 1); 122 | t.notEquals(lines[0].indexOf('foo'), -1); 123 | }); 124 | }); 125 | 126 | test('cmd-log: --search flag', function(t) { 127 | t.plan(3); 128 | const options = { 129 | '--search' : '+test', 130 | }; 131 | 132 | log(config, options, db, function(err, output) { 133 | const lines = output.split(eol); 134 | t.notOk(err); 135 | t.equals(lines.length, 1); 136 | t.notEquals(lines[0].indexOf('foo'), -1); 137 | }); 138 | }); 139 | 140 | }); 141 | -------------------------------------------------------------------------------- /test/cmd-write.js: -------------------------------------------------------------------------------- 1 | const Db = require('./mock/db'); 2 | const config = require('./mock/config'); 3 | const test = require('tape'); 4 | const write = require('../app/cmd-write'); 5 | 6 | // connect to a mock database for testing 7 | Db(function(err, db) { 8 | 9 | if (err) { 10 | console.warn('Can\'t initialize test database.'); 11 | console.warn(err.message); 12 | process.exit(1); 13 | } 14 | 15 | test('cmd-write: should write a log entry', function(t) { 16 | t.plan(6); 17 | const options = { 18 | '' : [ 'qux' ], 19 | }; 20 | 21 | write(config, options, db, function(err) { 22 | t.notOk(err); 23 | 24 | const query = 'SELECT * FROM Log'; 25 | db.all(query, function(err, rows) { 26 | t.notOk(err); 27 | t.equals(rows.length, 4); 28 | t.equals(rows[3].id, 4); 29 | t.equals(rows[3].entry, 'qux'); 30 | t.notEquals(rows[3].timestamp, NaN); 31 | }); 32 | }); 33 | }); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /test/mock/config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | 5 | // strip colors to make testing simpler 6 | color: { 7 | id : 'stripColor', 8 | timestamp : 'stripColor', 9 | entry : 'stripColor', 10 | match : 'stripColor', 11 | }, 12 | 13 | // this doesn't matter 14 | dateFormat : 'dd MMM | hh:mm tt', 15 | 16 | // this will 'no-op' in the shell 17 | editor : path.join(__dirname, 'editor.js'), 18 | }; 19 | -------------------------------------------------------------------------------- /test/mock/db.js: -------------------------------------------------------------------------------- 1 | const sqlite3 = require('sqlite3').verbose(); 2 | require('datejs'); 3 | 4 | module.exports = function(callback) { 5 | 6 | // create an in-memory database for testing 7 | const db = new sqlite3.Database(':memory:'); 8 | 9 | // query to create the database table 10 | const create = [ 11 | 'CREATE TABLE Log(', 12 | 'id INTEGER PRIMARY KEY,', 13 | 'entry TEXT,', 14 | 'timestamp DATE', 15 | ')', 16 | ].join(' '); 17 | 18 | // query to populate the table with some records 19 | const insert = [ 20 | 'INSERT INTO Log (entry, timestamp) VALUES', 21 | '("foo +test", ?),', 22 | '("bar", ?),', 23 | '("baz", ?)', 24 | ].join(' '); 25 | const jan1 = Date.parse('January 1 2017'); 26 | const jan2 = Date.parse('January 2 2017'); 27 | const jan3 = Date.parse('January 3 2017'); 28 | 29 | // run the queries when the database is opened 30 | db.on('open', function() { 31 | db.run(create, function(err) { 32 | if (err) { return callback(err); } 33 | 34 | db.run(insert, jan1, jan2, jan3, function(err) { 35 | if (err) { return callback(err); } 36 | callback(null, db); 37 | }); 38 | }); 39 | }); 40 | }; 41 | -------------------------------------------------------------------------------- /test/mock/editor.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs'); 4 | 5 | // mock as if the user saved 'quux' to the open file 6 | fs.writeFileSync(process.argv[2], 'quux', 'utf8'); 7 | -------------------------------------------------------------------------------- /test/util-decorate.js: -------------------------------------------------------------------------------- 1 | const Db = require('./mock/db'); 2 | const config = require('./mock/config'); 3 | const decorate = require('../app/util-decorate'); 4 | const eol = require('os').EOL; 5 | const test = require('tape'); 6 | 7 | Db(function(err, db) { 8 | 9 | if (err) { 10 | console.warn('Can\'t initialize test database.'); 11 | console.warn(err.message); 12 | process.exit(1); 13 | } 14 | 15 | test('util-decorate: should return formatted rows', function(t) { 16 | t.plan(8); 17 | 18 | const query = 'SELECT * FROM Log'; 19 | 20 | // query some records 21 | db.all(query, function(err, rows) { 22 | t.notOk(err); 23 | 24 | // and decorate them 25 | const decorated = decorate(config, {}, rows).split(eol); 26 | t.equals(decorated.length, 3); 27 | t.equals(decorated[0].indexOf('1'), 0); 28 | t.notEquals(decorated[0].indexOf('foo'), -1); 29 | t.equals(decorated[1].indexOf('2'), 0); 30 | t.notEquals(decorated[1].indexOf('bar'), -1); 31 | t.equals(decorated[2].indexOf('3'), 0); 32 | t.notEquals(decorated[2].indexOf('baz'), -1); 33 | }); 34 | }); 35 | 36 | }); 37 | -------------------------------------------------------------------------------- /test/util-editor.js: -------------------------------------------------------------------------------- 1 | const config = require('./mock/config'); 2 | const editor = require('../app/util-editor'); 3 | const test = require('tape'); 4 | 5 | test('util-editor: should error if an editor is not specified', function(t) { 6 | t.plan(2); 7 | editor({}, 'quux', function(err, entry) { 8 | t.notOk(entry); 9 | t.equals(err.message, 'EDITOR is not set. Aborting.'); 10 | }); 11 | }); 12 | 13 | test('util-editor: should spawn an editor and return edited file content', function(t) { 14 | t.plan(2); 15 | editor(config, 'quux', function(err, entry) { 16 | t.notOk(err); 17 | t.equals(entry, 'quux'); 18 | }); 19 | }); 20 | 21 | test('util-editor: "entry" should be optional', function(t) { 22 | t.plan(2); 23 | editor(config, function(err, entry) { 24 | t.notOk(err); 25 | t.equals(entry, 'quux'); 26 | }); 27 | }); 28 | --------------------------------------------------------------------------------