gitsheets 0.20.0 → 0.21.3

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/bin/cli.js CHANGED
@@ -2,18 +2,22 @@
2
2
 
3
3
 
4
4
  // setup logger
5
- const logger = require('winston')
6
- module.exports = { logger }
5
+ const logger = require('winston');
6
+ const loggerConsole = new logger.transports.Console({
7
+ level: process.env.DEBUG ? 'debug' : 'info',
8
+ format: logger.format.combine(
9
+ logger.format.colorize(),
10
+ logger.format.prettyPrint(),
11
+ logger.format.splat(),
12
+ logger.format.simple(),
13
+ ),
7
14
 
8
- if (process.env.DEBUG) {
9
- logger.level = 'debug'
10
- }
15
+ // all logger output to STDERR
16
+ stderrLevels: Object.keys(require('winston/lib/winston/config').cli.levels),
17
+ });
18
+ logger.add(loggerConsole);
11
19
 
12
-
13
- // all logger output to STDERR
14
- for (const level in logger.levels) {
15
- logger.default.transports.console.stderrLevels[level] = true;
16
- }
20
+ module.exports = { logger };
17
21
 
18
22
 
19
23
  // route command line
@@ -25,15 +29,33 @@ require('yargs')
25
29
  default: false,
26
30
  global: true,
27
31
  })
32
+ .option('q', {
33
+ alias: 'quiet',
34
+ type: 'boolean',
35
+ default: false,
36
+ global: true,
37
+ })
28
38
  .check(function (argv) {
29
39
  if (argv.debug) {
30
- logger.level = 'debug'
40
+ loggerConsole.level = 'debug';
31
41
  } else if (argv.quiet) {
32
- logger.level = 'error'
42
+ loggerConsole.level = 'error';
33
43
  }
34
44
 
35
45
  return true;
36
46
  })
37
- .commandDir('../commands')
47
+ .commandDir('../commands', { exclude: /\.test\.js$/ })
38
48
  .demandCommand()
39
- .argv
49
+ .showHelpOnFail(false, 'Specify --help for available options')
50
+ .fail((msg, err) => {
51
+ logger.error(msg || err.message);
52
+
53
+ if (err) {
54
+ logger.debug(err.stack);
55
+ }
56
+
57
+ process.exit(1);
58
+ })
59
+ .strict()
60
+ .help()
61
+ .argv;
package/commands/edit.js CHANGED
@@ -19,6 +19,8 @@ exports.builder = {
19
19
  exports.handler = async function edit({ recordPath, resumePath, encoding }) {
20
20
  const fs = require('fs');
21
21
  const { spawn } = require('child_process');
22
+ const path = require('path');
23
+ const tmp = require('tmp');
22
24
  const TOML = require('@iarna/toml');
23
25
  const Repository = require('../lib/Repository.js');
24
26
  const Sheet = require('../lib/Sheet.js')
@@ -26,17 +28,21 @@ exports.handler = async function edit({ recordPath, resumePath, encoding }) {
26
28
  const git = await repo.getGit();
27
29
 
28
30
  // open record
29
- const recordToml = fs.readFileSync(resumePath || recordPath, encoding);
31
+ let recordToml = fs.readFileSync(resumePath || recordPath, encoding);
30
32
 
31
- // get temp path
32
- const tempFilePath = await new Promise((resolve, reject) => {
33
- const mktemp = spawn('mktemp', ['-t', 'gitsheet.XXXXXX.toml']);
34
-
35
- let stdout = '', stderr = '';
36
- mktemp.stdout.on('data', chunk => stdout += chunk);
37
- mktemp.stderr.on('data', chunk => stderr += chunk);
33
+ // try to parse and format
34
+ try {
35
+ const record = TOML.parse(recordToml);
36
+ recordToml = Sheet.stringifyRecord(record);
37
+ } catch (err) {
38
+ console.warn(`Failed to parse opened record:\n${err}`);
39
+ }
38
40
 
39
- mktemp.on('close', code => code === 0 ? resolve(stdout.trim()) : reject(stderr.trim()));
41
+ // get temp path
42
+ const { name: tempFilePath } = tmp.fileSync({
43
+ prefix: path.basename(recordPath, '.toml'),
44
+ postfix: '.toml',
45
+ discardDescriptor: true,
40
46
  });
41
47
 
42
48
  // populate temp path
@@ -67,7 +73,7 @@ exports.handler = async function edit({ recordPath, resumePath, encoding }) {
67
73
  try {
68
74
  editedRecord = TOML.parse(editedToml);
69
75
  } catch (err) {
70
- console.error(`Failed to parse record:\n${err}`);
76
+ console.error(`Failed to parse edited record:\n${err}`);
71
77
  console.error(`To resume editing, run: git sheet edit ${recordPath} ${tempFilePath}`);
72
78
  process.exit(1);
73
79
  }
@@ -54,9 +54,23 @@ exports.handler = async function query({
54
54
  }
55
55
 
56
56
  // loop through all records and re-upsert
57
- for await (const record of sheet.query()) {
58
- logger.info(`rewriting ${sheetName}/${record[Symbol.for('gitsheets-path')]}`);
59
- await sheet.upsert(record);
57
+ try {
58
+ for await (const record of sheet.query()) {
59
+ const originalPath = record[Symbol.for('gitsheets-path')];
60
+ logger.info(`rewriting ${path.join(root, prefix, sheetName, originalPath)}.toml`);
61
+ const { path: normalizedPath } = await sheet.upsert(record);
62
+
63
+ if (normalizedPath !== originalPath) {
64
+ logger.warn(`^- moved to ${path.join(root, prefix, sheetName, normalizedPath)}.toml`);
65
+ }
66
+ }
67
+ } catch (err) {
68
+ if (err.constructor.name == 'TomlError') {
69
+ logger.error(`failed to parse ${path.join(root, prefix, err.file)}\n${err.message}`);
70
+ process.exit(1);
71
+ }
72
+
73
+ throw err;
60
74
  }
61
75
  }
62
76
 
package/commands/read.js CHANGED
@@ -3,7 +3,7 @@ exports.desc = 'Read a record, converting to desired format';
3
3
  exports.builder = {
4
4
  'record-path': {
5
5
  type: 'string',
6
- describe: 'The path to a record file to edit',
6
+ describe: 'The path to a record file to read',
7
7
  demandOption: true,
8
8
  },
9
9
  encoding: {
@@ -24,12 +24,7 @@ exports.builder = {
24
24
 
25
25
  exports.handler = async function edit({ recordPath, encoding, format, headers }) {
26
26
  const fs = require('fs');
27
- const { spawn } = require('child_process');
28
27
  const TOML = require('@iarna/toml');
29
- const Repository = require('../lib/Repository.js');
30
- const Sheet = require('../lib/Sheet.js')
31
- const repo = await Repository.getFromEnvironment({ working: true });
32
- const git = await repo.getGit();
33
28
 
34
29
  // open record
35
30
  const recordToml = fs.readFileSync(recordPath, encoding);
@@ -64,6 +59,6 @@ async function outputCsv(record, { headers = true, delimiter = ',' } = {}) {
64
59
  }
65
60
 
66
61
  async function outputToml(record) {
67
- const TOML = require('@iarna/toml');
68
- console.log(`${TOML.stringify(record)}`);
62
+ const Sheet = require('../lib/Sheet.js')
63
+ console.log(`${Sheet.stringifyRecord(record)}`);
69
64
  }
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const TOML = require('@iarna/toml');
3
3
  const { parse: csvParse } = require('fast-csv');
4
+ const deepmerge = require('deepmerge');
4
5
 
5
6
  const inputFormats = {
6
7
  json: readJsonFile,
@@ -46,6 +47,11 @@ exports.builder = {
46
47
  type: 'boolean',
47
48
  default: false,
48
49
  },
50
+ 'patch-existing': {
51
+ describe: 'For existing records, patch in provided values so that additional properties not included in the input are preserved',
52
+ type: 'boolean',
53
+ default: false,
54
+ },
49
55
  };
50
56
 
51
57
  exports.handler = async function upsert({
@@ -57,6 +63,7 @@ exports.handler = async function upsert({
57
63
  encoding,
58
64
  attachments = null,
59
65
  deleteMissing,
66
+ patchExisting,
60
67
  ...argv
61
68
  }) {
62
69
  const logger = require('../lib/logger.js');
@@ -96,7 +103,13 @@ exports.handler = async function upsert({
96
103
 
97
104
 
98
105
  // clear sheet
106
+ let inputSheet = sheet;
107
+
99
108
  if (deleteMissing) {
109
+ // re-open input sheet
110
+ inputSheet = await sheet.clone();
111
+
112
+ // clear target sheet
100
113
  await sheet.clear();
101
114
  }
102
115
 
@@ -113,8 +126,28 @@ exports.handler = async function upsert({
113
126
 
114
127
 
115
128
  // upsert record(s) into sheet
116
- for await (const inputRecord of inputRecords) {
117
- const { blob: outputBlob, path: outputPath } = await sheet.upsert(inputRecord);
129
+ for await (let inputRecord of inputRecords) {
130
+
131
+ if (patchExisting) {
132
+ // TODO: move more of this logic inside Sheet class
133
+
134
+ // fetch existing record from inputSheet
135
+ const inputRecordPath = await inputSheet.pathForRecord(await inputSheet.normalizeRecord(inputRecord));
136
+
137
+ if (inputRecordPath) {
138
+ const { root: inputSheetRoot } = await inputSheet.getCachedConfig();
139
+
140
+ // existing record find, merge
141
+ const existingBlob = await inputSheet.dataTree.getChild(`${path.join(inputSheetRoot, inputRecordPath)}.toml`);
142
+
143
+ if (existingBlob) {
144
+ const existingRecord = await inputSheet.readRecord(existingBlob);
145
+ inputRecord = deepmerge(inputRecord, existingRecord);
146
+ }
147
+ }
148
+ }
149
+
150
+ const { blob: outputBlob, path: outputPath } = await sheet.upsert(inputRecord, { patchExisting });
118
151
  console.log(`${outputBlob.hash}\t${outputPath}`);
119
152
 
120
153
  if (attachments) {
package/lib/Sheet.js CHANGED
@@ -118,9 +118,20 @@ class Sheet extends Configurable
118
118
  async readRecord (blob, path = null) {
119
119
  const cache = this.#recordCache.get(blob.hash);
120
120
 
121
- const record = cache
122
- ? v8.deserialize(cache)
123
- : await blob.read().then(TOML.parse);
121
+ let record;
122
+
123
+ if (cache) {
124
+ record = v8.deserialize(cache);
125
+ } else {
126
+ const toml = await blob.read();
127
+
128
+ try {
129
+ record = TOML.parse(toml);
130
+ } catch (err) {
131
+ err.file = path;
132
+ throw err;
133
+ }
134
+ }
124
135
 
125
136
  // annotate with gitsheets keys
126
137
  record[RECORD_SHEET_KEY] = this.name;
@@ -153,14 +164,14 @@ class Sheet extends Configurable
153
164
  const pathTemplate = PathTemplate.fromString(pathTemplateString);
154
165
  const sheetDataTree = await this.dataTree.getSubtree(sheetRoot);
155
166
 
156
- BLOBS: for await (const blob of pathTemplate.queryTree(sheetDataTree, query)) {
157
- const record = await this.readRecord(blob);
167
+ BLOBS: for await (const { blob, path: blobPath } of pathTemplate.queryTree(sheetDataTree, query)) {
168
+ const record = await this.readRecord(blob, blobPath);
158
169
 
159
170
  if (!queryMatches(query, record)) {
160
171
  continue BLOBS;
161
172
  }
162
173
 
163
- record[RECORD_PATH_KEY] = pathTemplate.render(record);
174
+ record[RECORD_PATH_KEY] = blobPath || pathTemplate.render(record);
164
175
 
165
176
  yield record;
166
177
  }
@@ -277,6 +288,15 @@ class Sheet extends Configurable
277
288
  return this.dataTree.writeChild(root, this.dataTree.repo.createTree());
278
289
  }
279
290
 
291
+ async clone () {
292
+ return new Sheet({
293
+ workspace: this.workspace,
294
+ name: this.name,
295
+ dataTree: await this.dataTree.clone(),
296
+ configPath: this.configPath,
297
+ });
298
+ }
299
+
280
300
  async upsert (record) {
281
301
  const {
282
302
  root: sheetRoot,
@@ -50,7 +50,7 @@ class Template
50
50
  return recordPath.join('/');
51
51
  }
52
52
 
53
- async* queryTree (tree, query, depth = 0) {
53
+ async* queryTree (tree, query, pathPrefix = '', depth = 0) {
54
54
  const numComponents = this.#components.length;
55
55
 
56
56
  if (!tree) {
@@ -68,7 +68,7 @@ class Template
68
68
  const child = await currentTree.getChild(`${nextName}.toml`);
69
69
 
70
70
  if (child) {
71
- yield child;
71
+ yield { path: path.join(pathPrefix, nextName), blob: child };
72
72
  }
73
73
 
74
74
  // absolute match on a leaf, we're done with this query
@@ -97,8 +97,9 @@ class Template
97
97
  continue;
98
98
  }
99
99
 
100
- attachmentsPrefix = `${childPath.substr(0, childPath.length - 5)}/`;
101
- yield child;
100
+ const childName = childPath.substr(0, childPath.length - 5);
101
+ attachmentsPrefix = `${childName}/`;
102
+ yield { path: path.join(pathPrefix, childName), blob: child };
102
103
  }
103
104
 
104
105
  return;
@@ -117,14 +118,14 @@ class Template
117
118
  } else {
118
119
  // each tree in current tree could contain matching records
119
120
  const children = await currentTree.getChildren();
120
- for (const childName in children) {
121
- const child = children[childName];
121
+ for (const childPath in children) {
122
+ const child = children[childPath];
122
123
 
123
124
  if (!child.isTree) {
124
125
  continue;
125
126
  }
126
127
 
127
- yield* this.queryTree(child, query, i+1);
128
+ yield* this.queryTree(child, query, path.join(pathPrefix, childPath), i+1);
128
129
  }
129
130
 
130
131
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitsheets",
3
- "version": "0.20.0",
3
+ "version": "0.21.3",
4
4
  "description": "A toolkit for using a git repository to store low-volume, high-touch, human-scale data",
5
5
  "main": "lib/GitSheets.js",
6
6
  "scripts": {
@@ -22,10 +22,11 @@
22
22
  "dependencies": {
23
23
  "@iarna/toml": "^2.2.5",
24
24
  "csv-parser": "^2.3.5",
25
- "fast-csv": "^3.7.0",
25
+ "deepmerge": "^4.2.2",
26
+ "fast-csv": "^4.3.6",
26
27
  "fast-json-patch": "^2.2.1",
27
28
  "get-stream": "^5.2.0",
28
- "hologit": "^0.34.3",
29
+ "hologit": "^0.40.5",
29
30
  "http-assert": "^1.4.1",
30
31
  "koa": "^2.13.1",
31
32
  "koa-bodyparser": "^4.3.0",
@@ -35,15 +36,16 @@
35
36
  "rfc6902": "^4.0.1",
36
37
  "sort-keys": "^4.2.0",
37
38
  "streaming-json-stringify": "^3.1.0",
39
+ "tmp": "^0.2.1",
38
40
  "to-readable-stream": "^2.1.0",
39
- "winston": "^2.4.5",
41
+ "winston": "^3.3.3",
40
42
  "yargs": "^13.3.2"
41
43
  },
42
44
  "devDependencies": {
43
45
  "common-tags": "^1.8.0",
44
46
  "del": "^5.1.0",
45
47
  "into-stream": "^5.1.1",
46
- "jest": "^24.9.0",
48
+ "jest": "^26.6.3",
47
49
  "make-dir": "^3.1.0",
48
50
  "supertest": "^4.0.2"
49
51
  }