json-fs-query 3.3.1 → 3.5.0
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/README.md +104 -2
- package/browser/jsonFsQuery.js +810 -3845
- package/browser/jsonFsQuery.min.js +1 -1
- package/dist/cli/chooseDatabase.d.ts +13 -0
- package/dist/cli/chooseDatabase.js +37 -0
- package/dist/cli/chooseDatabase.min.js +1 -0
- package/dist/cli/formatCsv.d.ts +9 -0
- package/dist/cli/formatCsv.js +53 -0
- package/dist/cli/formatCsv.min.js +1 -0
- package/dist/cli/formatResult.d.ts +30 -0
- package/dist/cli/formatResult.js +139 -0
- package/dist/cli/formatResult.min.js +1 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.js +52 -0
- package/dist/cli/index.min.js +2 -0
- package/dist/cli/interactive.d.ts +15 -0
- package/dist/cli/interactive.js +257 -0
- package/dist/cli/interactive.min.js +1 -0
- package/dist/cli/output.d.ts +31 -0
- package/dist/cli/output.js +64 -0
- package/dist/cli/output.min.js +1 -0
- package/dist/cli/runDirect.d.ts +11 -0
- package/dist/cli/runDirect.js +28 -0
- package/dist/cli/runDirect.min.js +1 -0
- package/dist/data/introspect/alter.js +2 -10
- package/dist/data/introspect/alter.min.js +1 -1
- package/dist/data/introspect/create.js +15 -4
- package/dist/data/introspect/create.min.js +1 -1
- package/dist/data/introspect/definitions/key.js +0 -2
- package/dist/data/introspect/definitions/key.min.js +1 -1
- package/dist/data/introspect/definitions/record.js +0 -2
- package/dist/data/introspect/definitions/record.min.js +1 -1
- package/dist/data/introspect/describe.js +2 -0
- package/dist/data/introspect/describe.min.js +1 -1
- package/dist/data/introspect/drop.js +3 -4
- package/dist/data/introspect/drop.min.js +1 -1
- package/dist/data/introspect/formatDatabase.js +0 -2
- package/dist/data/introspect/formatDatabase.min.js +1 -1
- package/dist/data/introspect/index.js +0 -4
- package/dist/data/introspect/index.min.js +1 -1
- package/dist/data/introspect/packDatabase.js +0 -19
- package/dist/data/introspect/packDatabase.min.js +1 -1
- package/dist/data/queries/createRecord.js +0 -2
- package/dist/data/queries/createRecord.min.js +1 -1
- package/dist/data/queries/databaseStatements.js +0 -3
- package/dist/data/queries/databaseStatements.min.js +1 -1
- package/dist/data/queries/deleteEntity.js +0 -20
- package/dist/data/queries/deleteEntity.min.js +1 -1
- package/dist/data/queries/dropRecord.js +0 -5
- package/dist/data/queries/dropRecord.min.js +1 -1
- package/dist/data/queries/groupBy.js +0 -16
- package/dist/data/queries/groupBy.min.js +1 -1
- package/dist/data/queries/insertEntity.js +0 -35
- package/dist/data/queries/insertEntity.min.js +1 -1
- package/dist/data/queries/joinEntity.js +0 -15
- package/dist/data/queries/joinEntity.min.js +1 -1
- package/dist/data/queries/parser.js +0 -2
- package/dist/data/queries/parser.min.js +1 -1
- package/dist/data/queries/selectFields.js +0 -3
- package/dist/data/queries/selectFields.min.js +1 -1
- package/dist/data/queries/showRecords.js +0 -2
- package/dist/data/queries/showRecords.min.js +1 -1
- package/dist/data/queries/updateEntity.js +0 -4
- package/dist/data/queries/updateEntity.min.js +1 -1
- package/dist/data/queries/where.js +0 -4
- package/dist/data/queries/where.min.js +1 -1
- package/dist/data/storage/databases.js +1 -2
- package/dist/data/storage/databases.min.js +1 -1
- package/dist/data/storage/listFiles.js +0 -2
- package/dist/data/storage/listFiles.min.js +1 -1
- package/dist/data/storage/readCache.js +0 -13
- package/dist/data/storage/readCache.min.js +1 -1
- package/dist/data/storage/readJson.js +2 -2
- package/dist/data/storage/readJson.min.js +1 -1
- package/dist/data/storage/storagePath.js +0 -1
- package/dist/data/storage/storagePath.min.js +1 -1
- package/dist/data/storage/writeSet.js +0 -32
- package/dist/data/storage/writeSet.min.js +1 -1
- package/dist/data/utilities/assertValidName.d.ts +14 -0
- package/dist/data/utilities/assertValidName.js +34 -0
- package/dist/data/utilities/assertValidName.min.js +1 -0
- package/dist/data/utilities/computeKeyValue.js +0 -2
- package/dist/data/utilities/computeKeyValue.min.js +1 -1
- package/dist/data/utilities/conditionEntities.js +0 -20
- package/dist/data/utilities/conditionEntities.min.js +1 -1
- package/dist/data/utilities/conditionTree.js +0 -3
- package/dist/data/utilities/conditionTree.min.js +1 -1
- package/dist/data/utilities/config.d.ts +11 -0
- package/dist/data/utilities/config.js +15 -1
- package/dist/data/utilities/config.min.js +1 -1
- package/dist/data/utilities/databaseLock.js +0 -14
- package/dist/data/utilities/databaseLock.min.js +1 -1
- package/dist/data/utilities/deregisterRecord.js +0 -2
- package/dist/data/utilities/deregisterRecord.min.js +1 -1
- package/dist/data/utilities/fieldType.js +0 -2
- package/dist/data/utilities/fieldType.min.js +1 -1
- package/dist/data/utilities/getConditionIndexedList.js +0 -20
- package/dist/data/utilities/getConditionIndexedList.min.js +1 -1
- package/dist/data/utilities/indexBuffer.js +0 -33
- package/dist/data/utilities/indexBuffer.min.js +1 -1
- package/dist/data/utilities/indexHas.js +0 -3
- package/dist/data/utilities/indexHas.min.js +1 -1
- package/dist/data/utilities/joinBases.js +0 -33
- package/dist/data/utilities/joinBases.min.js +1 -1
- package/dist/data/utilities/keyIndexFiles.js +0 -19
- package/dist/data/utilities/keyIndexFiles.min.js +1 -1
- package/dist/data/utilities/knownFields.js +0 -21
- package/dist/data/utilities/knownFields.min.js +1 -1
- package/dist/data/utilities/mergeJoin.js +0 -2
- package/dist/data/utilities/mergeJoin.min.js +1 -1
- package/dist/data/utilities/packedTable.js +0 -3
- package/dist/data/utilities/packedTable.min.js +1 -1
- package/dist/data/utilities/parsers/findConditions.js +0 -2
- package/dist/data/utilities/parsers/findConditions.min.js +1 -1
- package/dist/data/utilities/parsers/findGroupBy.js +0 -2
- package/dist/data/utilities/parsers/findGroupBy.min.js +1 -1
- package/dist/data/utilities/parsers/maskStrings.js +0 -3
- package/dist/data/utilities/parsers/maskStrings.min.js +1 -1
- package/dist/data/utilities/parsers/parseCreate.js +0 -21
- package/dist/data/utilities/parsers/parseCreate.min.js +1 -1
- package/dist/data/utilities/parsers/splitClauses.js +0 -16
- package/dist/data/utilities/parsers/splitClauses.min.js +1 -1
- package/dist/data/utilities/parsers/splitQueries.js +0 -3
- package/dist/data/utilities/parsers/splitQueries.min.js +1 -1
- package/dist/data/utilities/reconcileJoin.js +0 -4
- package/dist/data/utilities/reconcileJoin.min.js +1 -1
- package/dist/data/utilities/removeFromIndex.js +0 -3
- package/dist/data/utilities/removeFromIndex.min.js +1 -1
- package/dist/data/utilities/requireNode.js +0 -1
- package/dist/data/utilities/requireNode.min.js +1 -1
- package/dist/data/utilities/retrieveRecord.js +0 -1
- package/dist/data/utilities/retrieveRecord.min.js +1 -1
- package/dist/data/utilities/retrieveRecords.js +0 -16
- package/dist/data/utilities/retrieveRecords.min.js +1 -1
- package/dist/data/utilities/rowLookup.js +0 -15
- package/dist/data/utilities/rowLookup.min.js +1 -1
- package/dist/data/utilities/runQuery.js +0 -16
- package/dist/data/utilities/runQuery.min.js +1 -1
- package/dist/data/utilities/semiJoin.js +0 -19
- package/dist/data/utilities/semiJoin.min.js +1 -1
- package/dist/data/utilities/updateIndex.js +0 -2
- package/dist/data/utilities/updateIndex.min.js +1 -1
- package/dist/index.js +0 -4
- package/dist/index.min.js +1 -1
- package/package.json +9 -3
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type DatabaseOptions = {
|
|
2
|
+
database?: string;
|
|
3
|
+
databases?: string;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Point json-fs-query at the database the options name: `-d <path>` alone is one database folder (databasePath),
|
|
7
|
+
* `--databases <folder>` a folder of databases (databasesPath) with `-d <name>` picking one in it.
|
|
8
|
+
* @param options
|
|
9
|
+
* @returns {Promise<string>} A line saying which database is in use.
|
|
10
|
+
* @throws {Error} For an invalid database name, or a database that doesn't exist.
|
|
11
|
+
*/
|
|
12
|
+
export declare const chooseDatabase: (options: DatabaseOptions) => Promise<string>;
|
|
13
|
+
//# sourceMappingURL=chooseDatabase.d.ts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.chooseDatabase = void 0;
|
|
7
|
+
var _databaseStatements = require("../data/queries/databaseStatements");
|
|
8
|
+
var _config = require("../data/utilities/config");
|
|
9
|
+
/**
|
|
10
|
+
* Point json-fs-query at the database the options name: `-d <path>` alone is one database folder (databasePath),
|
|
11
|
+
* `--databases <folder>` a folder of databases (databasesPath) with `-d <name>` picking one in it.
|
|
12
|
+
* @param options
|
|
13
|
+
* @returns {Promise<string>} A line saying which database is in use.
|
|
14
|
+
* @throws {Error} For an invalid database name, or a database that doesn't exist.
|
|
15
|
+
*/
|
|
16
|
+
const chooseDatabase = async options => {
|
|
17
|
+
if (options.databases) {
|
|
18
|
+
(0, _config.configure)({
|
|
19
|
+
databasesPath: options.databases,
|
|
20
|
+
...(options.database ? {
|
|
21
|
+
database: options.database
|
|
22
|
+
} : {})
|
|
23
|
+
});
|
|
24
|
+
if (!options.database) {
|
|
25
|
+
return `Databases: ${options.databases} (none in use yet: use <name>;)`;
|
|
26
|
+
}
|
|
27
|
+
// Checks that it exists (configure only checks the name)
|
|
28
|
+
await (0, _databaseStatements.useDatabase)(options.database);
|
|
29
|
+
} else if (options.database) {
|
|
30
|
+
// databasePath is joined straight onto file names, so it needs its trailing slash (`-d data` is data/)
|
|
31
|
+
(0, _config.configure)({
|
|
32
|
+
databasePath: options.database.endsWith('/') ? options.database : `${options.database}/`
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return `Database: ${(0, _config.activeDatabasePath)()}`;
|
|
36
|
+
};
|
|
37
|
+
exports.chooseDatabase = chooseDatabase;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.chooseDatabase=void 0;var _databaseStatements=require("../data/queries/databaseStatements"),_config=require("../data/utilities/config");const chooseDatabase=async a=>{if(a.databases){if((0,_config.configure)({databasesPath:a.databases,...a.database?{database:a.database}:{}}),!a.database)return`Databases: ${a.databases} (none in use yet: use <name>;)`;await(0,_databaseStatements.useDatabase)(a.database)}else a.database&&(0,_config.configure)({databasePath:a.database.endsWith("/")?a.database:`${a.database}/`});return`Database: ${(0,_config.activeDatabasePath)()}`};exports.chooseDatabase=chooseDatabase;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format one query's result as CSV: a header line of field names (in the order they first appear), then a line per
|
|
3
|
+
* row. A grouped read is its rows, plain values (show records) one `value` column, an empty result nothing at all.
|
|
4
|
+
* Anything else (`describe`, a single value) isn't a table: an object prints as JSON, a value as it is.
|
|
5
|
+
* @param result One entry of what query() resolves to
|
|
6
|
+
* @returns {string}
|
|
7
|
+
*/
|
|
8
|
+
export declare const formatCsv: (result: unknown) => string;
|
|
9
|
+
//# sourceMappingURL=formatCsv.d.ts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.formatCsv = void 0;
|
|
7
|
+
require("core-js/modules/es.json.stringify.js");
|
|
8
|
+
var _formatResult = require("./formatResult");
|
|
9
|
+
const isPlainObject = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
10
|
+
/**
|
|
11
|
+
* One CSV field (RFC 4180): quoted when it holds a comma, a quote, a line break, or space at either end, with its
|
|
12
|
+
* quotes doubled. `null` and a missing field are empty and unquoted, the empty string is `""` (as psql's CSV output
|
|
13
|
+
* tells them apart). Objects and arrays are their JSON.
|
|
14
|
+
* @param value
|
|
15
|
+
* @param present False when the row doesn't have the field
|
|
16
|
+
*/
|
|
17
|
+
const csvField = (value, present = true) => {
|
|
18
|
+
if (!present || value === null || typeof value === 'undefined') {
|
|
19
|
+
return '';
|
|
20
|
+
}
|
|
21
|
+
const text = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
22
|
+
return text === '' || /[",\r\n]|^\s|\s$/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The rows of a grouped read, in group order (each row still holds the fields it was grouped by).
|
|
26
|
+
* @param groups
|
|
27
|
+
*/
|
|
28
|
+
const groupRows = groups => groups.flatMap(group => group.groups ? groupRows(group.groups) : group.rows);
|
|
29
|
+
/**
|
|
30
|
+
* Format one query's result as CSV: a header line of field names (in the order they first appear), then a line per
|
|
31
|
+
* row. A grouped read is its rows, plain values (show records) one `value` column, an empty result nothing at all.
|
|
32
|
+
* Anything else (`describe`, a single value) isn't a table: an object prints as JSON, a value as it is.
|
|
33
|
+
* @param result One entry of what query() resolves to
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
const formatCsv = result => {
|
|
37
|
+
if (!Array.isArray(result)) {
|
|
38
|
+
return isPlainObject(result) ? JSON.stringify(result, null, 2) : csvField(result);
|
|
39
|
+
}
|
|
40
|
+
const rows = result.length && result.every(_formatResult.isGroup) ? groupRows(result) : result;
|
|
41
|
+
if (!rows.length) {
|
|
42
|
+
return '';
|
|
43
|
+
}
|
|
44
|
+
// A boolean, not a type guard: a guard would narrow rows to never in the branch below
|
|
45
|
+
const objects = rows.every(isPlainObject);
|
|
46
|
+
if (!objects) {
|
|
47
|
+
return ['value', ...rows.map(row => csvField(row))].join('\n');
|
|
48
|
+
}
|
|
49
|
+
const columns = [...new Set(rows.flatMap(row => Object.keys(row)))];
|
|
50
|
+
const has = (row, column) => Object.prototype.hasOwnProperty.call(row, column);
|
|
51
|
+
return [columns.map(column => csvField(column)).join(','), ...rows.map(row => columns.map(column => csvField(row[column], has(row, column))).join(','))].join('\n');
|
|
52
|
+
};
|
|
53
|
+
exports.formatCsv = formatCsv;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.formatCsv=void 0,require("core-js/modules/es.json.stringify.js");var _formatResult=require("./formatResult");const isPlainObject=r=>null!==r&&"object"==typeof r&&!Array.isArray(r),csvField=(r,e=!0)=>{if(!e||null==r)return"";const t="object"==typeof r?JSON.stringify(r):String(r);return""===t||/[",\r\n]|^\s|\s$/.test(t)?`"${t.replace(/"/g,'""')}"`:t},groupRows=r=>r.flatMap((r=>r.groups?groupRows(r.groups):r.rows)),formatCsv=r=>{if(!Array.isArray(r))return isPlainObject(r)?JSON.stringify(r,null,2):csvField(r);const e=r.length&&r.every(_formatResult.isGroup)?groupRows(r):r;if(!e.length)return"";if(!e.every(isPlainObject))return["value",...e.map((r=>csvField(r)))].join("\n");const t=[...new Set(e.flatMap((r=>Object.keys(r))))];return[t.map((r=>csvField(r))).join(","),...e.map((r=>t.map((e=>csvField(r[e],((r,e)=>Object.prototype.hasOwnProperty.call(r,e))(r,e)))).join(",")))].join("\n")};exports.formatCsv=formatCsv;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Group } from '../data/queries/groupBy';
|
|
2
|
+
/**
|
|
3
|
+
* How a cell shows a value that isn't plain text. Each marker reads without colour (piped output, NO_COLOR, screen
|
|
4
|
+
* readers); dimming is only extra.
|
|
5
|
+
*/
|
|
6
|
+
export type Markers = {
|
|
7
|
+
missing: string;
|
|
8
|
+
null: string;
|
|
9
|
+
empty: string;
|
|
10
|
+
};
|
|
11
|
+
export type FormatOptions = {
|
|
12
|
+
color?: boolean;
|
|
13
|
+
markers?: Partial<Markers>;
|
|
14
|
+
legend?: boolean;
|
|
15
|
+
};
|
|
16
|
+
export declare const defaultMarkers: Markers;
|
|
17
|
+
/**
|
|
18
|
+
* Whether a value is one group of a grouped read: a field name, then either its rows or its groups, and nothing else.
|
|
19
|
+
* @param value
|
|
20
|
+
*/
|
|
21
|
+
export declare const isGroup: (value: unknown) => value is Group;
|
|
22
|
+
/**
|
|
23
|
+
* Format one query's result for the terminal. Rows become a table, a grouped read a table per group, an empty list
|
|
24
|
+
* `(0 rows)`; anything else (`describe`, a database name) prints as it is, or as JSON for an object.
|
|
25
|
+
* @param result One entry of what query() resolves to
|
|
26
|
+
* @param options
|
|
27
|
+
* @returns {string}
|
|
28
|
+
*/
|
|
29
|
+
export declare const formatResult: (result: unknown, options?: FormatOptions) => string;
|
|
30
|
+
//# sourceMappingURL=formatResult.d.ts.map
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.isGroup = exports.formatResult = exports.defaultMarkers = void 0;
|
|
7
|
+
require("core-js/modules/es.json.stringify.js");
|
|
8
|
+
var _util = require("util");
|
|
9
|
+
const defaultMarkers = exports.defaultMarkers = {
|
|
10
|
+
missing: '—',
|
|
11
|
+
null: 'NULL',
|
|
12
|
+
empty: '\'\''
|
|
13
|
+
};
|
|
14
|
+
const dim = (text, color) => color ? `\u001b[2m${text}\u001b[22m` : text;
|
|
15
|
+
// Width as it shows in the terminal: colour codes take no space
|
|
16
|
+
const visibleWidth = text => (0, _util.stripVTControlCharacters)(text).length;
|
|
17
|
+
const isPlainObject = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
18
|
+
const groupKeys = new Set(['field', 'value', 'missing', 'rows', 'groups']);
|
|
19
|
+
/**
|
|
20
|
+
* Whether a value is one group of a grouped read: a field name, then either its rows or its groups, and nothing else.
|
|
21
|
+
* @param value
|
|
22
|
+
*/
|
|
23
|
+
const isGroup = value => isPlainObject(value) && typeof value['field'] === 'string' && Array.isArray(value['rows']) !== Array.isArray(value['groups']) && Object.keys(value).every(key => groupKeys.has(key));
|
|
24
|
+
/**
|
|
25
|
+
* The cell for one value. Text is shown as it is (line breaks as \n so a row stays on one line), numbers and booleans
|
|
26
|
+
* as written, objects and arrays as compact JSON.
|
|
27
|
+
* @param value
|
|
28
|
+
* @param present False when the row doesn't have the field at all
|
|
29
|
+
*/
|
|
30
|
+
exports.isGroup = isGroup;
|
|
31
|
+
const toCell = (value, present = true) => {
|
|
32
|
+
if (!present) {
|
|
33
|
+
return {
|
|
34
|
+
text: '',
|
|
35
|
+
marker: 'missing'
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (value === null) {
|
|
39
|
+
return {
|
|
40
|
+
text: '',
|
|
41
|
+
marker: 'null'
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (value === '') {
|
|
45
|
+
return {
|
|
46
|
+
text: '',
|
|
47
|
+
marker: 'empty'
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (typeof value === 'string') {
|
|
51
|
+
return {
|
|
52
|
+
text: value.replace(/\r?\n/g, '\\n')
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (typeof value === 'object') {
|
|
56
|
+
return {
|
|
57
|
+
text: JSON.stringify(value)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
text: String(value)
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
const showCell = (cell, settings) => cell.marker ? dim(settings.markers[cell.marker], settings.color) : cell.text;
|
|
65
|
+
const rowCount = count => `(${count} ${count === 1 ? 'row' : 'rows'})`;
|
|
66
|
+
/**
|
|
67
|
+
* Draw rows as a table: columns in the order the fields first appear, a count underneath, and a legend when a marker
|
|
68
|
+
* that needs one is shown.
|
|
69
|
+
* @param rows Plain objects, or plain values (shown as one `value` column)
|
|
70
|
+
* @param settings
|
|
71
|
+
*/
|
|
72
|
+
const formatTable = (rows, settings) => {
|
|
73
|
+
if (!rows.length) {
|
|
74
|
+
return rowCount(0);
|
|
75
|
+
}
|
|
76
|
+
const objects = rows.every(isPlainObject);
|
|
77
|
+
const columns = objects ? [...new Set(rows.flatMap(row => Object.keys(row)))] : ['value'];
|
|
78
|
+
const has = (row, column) => Object.prototype.hasOwnProperty.call(row, column);
|
|
79
|
+
const cells = rows.map(row => objects ? columns.map(column => toCell(row[column], has(row, column))) : [toCell(row)]);
|
|
80
|
+
const body = cells.map(rowCells => rowCells.map(cell => showCell(cell, settings)));
|
|
81
|
+
const widths = columns.map((column, i) => Math.max(visibleWidth(column), ...body.map(texts => visibleWidth(texts[i]))));
|
|
82
|
+
const pad = (text, i) => text + ' '.repeat(widths[i] - visibleWidth(text));
|
|
83
|
+
const line = '+' + widths.map(width => '-'.repeat(width + 2)).join('+') + '+';
|
|
84
|
+
const tableRow = texts => '| ' + texts.map(pad).join(' | ') + ' |';
|
|
85
|
+
const lines = [line, tableRow(columns), line, ...body.map(tableRow), line, rowCount(rows.length)];
|
|
86
|
+
if (settings.legend) {
|
|
87
|
+
// Only the markers this table shows (an empty string explains itself)
|
|
88
|
+
const shown = new Set(cells.flat().map(cell => cell.marker));
|
|
89
|
+
const legend = ['missing', 'null'].filter(marker => shown.has(marker)).map(marker => `${dim(settings.markers[marker], settings.color)} ${marker}`);
|
|
90
|
+
if (legend.length) {
|
|
91
|
+
lines.push(legend.join(' · '));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return lines.join('\n');
|
|
95
|
+
};
|
|
96
|
+
const indent = (text, depth) => text.split('\n').map(line => ' '.repeat(depth) + line).join('\n');
|
|
97
|
+
/**
|
|
98
|
+
* Draw the groups of a grouped read: a heading per group (`date = 2024-06-05 (3 rows)`), then its table, or its own
|
|
99
|
+
* groups indented under it.
|
|
100
|
+
* @param groups
|
|
101
|
+
* @param settings
|
|
102
|
+
* @param depth How far nested groups are indented
|
|
103
|
+
*/
|
|
104
|
+
const formatGroups = (groups, settings, depth = 0) => groups.map(group => {
|
|
105
|
+
const heading = group.missing ? `${group.field} is missing` : `${group.field} = ${showCell(toCell(group.value), settings)}`;
|
|
106
|
+
const content = group.groups ? `${heading} (${group.groups.length} ${group.groups.length === 1 ? 'group' : 'groups'})\n${formatGroups(group.groups, settings, depth + 1)}` : `${heading} ${rowCount(group.rows.length)}\n${indent(formatTable(group.rows, settings), 1)}`;
|
|
107
|
+
return indent(content, depth);
|
|
108
|
+
}).join('\n\n');
|
|
109
|
+
/**
|
|
110
|
+
* Format one query's result for the terminal. Rows become a table, a grouped read a table per group, an empty list
|
|
111
|
+
* `(0 rows)`; anything else (`describe`, a database name) prints as it is, or as JSON for an object.
|
|
112
|
+
* @param result One entry of what query() resolves to
|
|
113
|
+
* @param options
|
|
114
|
+
* @returns {string}
|
|
115
|
+
*/
|
|
116
|
+
const formatResult = (result, options = {}) => {
|
|
117
|
+
const settings = {
|
|
118
|
+
color: options.color ?? false,
|
|
119
|
+
markers: {
|
|
120
|
+
...defaultMarkers,
|
|
121
|
+
...options.markers
|
|
122
|
+
},
|
|
123
|
+
legend: options.legend ?? true
|
|
124
|
+
};
|
|
125
|
+
if (Array.isArray(result)) {
|
|
126
|
+
if (result.length && result.every(isGroup)) {
|
|
127
|
+
return formatGroups(result, settings);
|
|
128
|
+
}
|
|
129
|
+
if (result.every(isPlainObject) || result.every(value => !isPlainObject(value) && !Array.isArray(value))) {
|
|
130
|
+
return formatTable(result, settings);
|
|
131
|
+
}
|
|
132
|
+
return JSON.stringify(result, null, 2);
|
|
133
|
+
}
|
|
134
|
+
if (isPlainObject(result)) {
|
|
135
|
+
return JSON.stringify(result, null, 2);
|
|
136
|
+
}
|
|
137
|
+
return showCell(toCell(result), settings);
|
|
138
|
+
};
|
|
139
|
+
exports.formatResult = formatResult;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.isGroup=exports.formatResult=exports.defaultMarkers=void 0,require("core-js/modules/es.json.stringify.js");var _util=require("util");const defaultMarkers=exports.defaultMarkers={missing:"—",null:"NULL",empty:"''"},dim=(e,r)=>r?`[2m${e}[22m`:e,visibleWidth=e=>(0,_util.stripVTControlCharacters)(e).length,isPlainObject=e=>null!==e&&"object"==typeof e&&!Array.isArray(e),groupKeys=new Set(["field","value","missing","rows","groups"]),isGroup=e=>isPlainObject(e)&&"string"==typeof e.field&&Array.isArray(e.rows)!==Array.isArray(e.groups)&&Object.keys(e).every((e=>groupKeys.has(e)));exports.isGroup=isGroup;const toCell=(e,r=!0)=>r?null===e?{text:"",marker:"null"}:""===e?{text:"",marker:"empty"}:"string"==typeof e?{text:e.replace(/\r?\n/g,"\\n")}:"object"==typeof e?{text:JSON.stringify(e)}:{text:String(e)}:{text:"",marker:"missing"},showCell=(e,r)=>e.marker?dim(r.markers[e.marker],r.color):e.text,rowCount=e=>`(${e} ${1===e?"row":"rows"})`,formatTable=(e,r)=>{if(!e.length)return rowCount(0);const t=e.every(isPlainObject),s=t?[...new Set(e.flatMap((e=>Object.keys(e))))]:["value"],o=e.map((e=>t?s.map((r=>toCell(e[r],((e,r)=>Object.prototype.hasOwnProperty.call(e,r))(e,r)))):[toCell(e)])),l=o.map((e=>e.map((e=>showCell(e,r))))),i=s.map(((e,r)=>Math.max(visibleWidth(e),...l.map((e=>visibleWidth(e[r])))))),a=(e,r)=>e+" ".repeat(i[r]-visibleWidth(e)),n="+"+i.map((e=>"-".repeat(e+2))).join("+")+"+",p=e=>"| "+e.map(a).join(" | ")+" |",u=[n,p(s),n,...l.map(p),n,rowCount(e.length)];if(r.legend){const e=new Set(o.flat().map((e=>e.marker))),t=["missing","null"].filter((r=>e.has(r))).map((e=>`${dim(r.markers[e],r.color)} ${e}`));t.length&&u.push(t.join(" · "))}return u.join("\n")},indent=(e,r)=>e.split("\n").map((e=>" ".repeat(r)+e)).join("\n"),formatGroups=(e,r,t=0)=>e.map((e=>{const s=e.missing?`${e.field} is missing`:`${e.field} = ${showCell(toCell(e.value),r)}`,o=e.groups?`${s} (${e.groups.length} ${1===e.groups.length?"group":"groups"})\n${formatGroups(e.groups,r,t+1)}`:`${s} ${rowCount(e.rows.length)}\n${indent(formatTable(e.rows,r),1)}`;return indent(o,t)})).join("\n\n"),formatResult=(e,r={})=>{const t={color:r.color??!1,markers:{...defaultMarkers,...r.markers},legend:r.legend??!0};return Array.isArray(e)?e.length&&e.every(isGroup)?formatGroups(e,t):e.every(isPlainObject)||e.every((e=>!isPlainObject(e)&&!Array.isArray(e)))?formatTable(e,t):JSON.stringify(e,null,2):isPlainObject(e)?JSON.stringify(e,null,2):showCell(toCell(e),t)};exports.formatResult=formatResult;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
Object.defineProperty(exports, "__esModule", {
|
|
5
|
+
value: true
|
|
6
|
+
});
|
|
7
|
+
exports.whenIdle = void 0;
|
|
8
|
+
var _commander = require("commander");
|
|
9
|
+
var _interactive = require("./interactive");
|
|
10
|
+
var _output = require("./output");
|
|
11
|
+
var _runDirect = require("./runDirect");
|
|
12
|
+
// The package's own version (package.json sits two folders up from both src/cli and dist/cli)
|
|
13
|
+
const {
|
|
14
|
+
version
|
|
15
|
+
} = require('../../package.json');
|
|
16
|
+
// Command-line options (commander exits with a message for an unknown option, and prints --help and --version)
|
|
17
|
+
const program = new _commander.Command().name('jfsq').description('Query a json-fs-query database from the command line: with -q, run the query and exit; without it, open the prompt.').version(version, '-v, --version', 'print the version').option('-q, --query <query>', 'run this query (or several, separated by ;), print the results and exit').option('-d, --database <path>', 'the database folder (default database/); with --databases, the name of a database in that folder').option('--databases <folder>', 'a folder holding several databases (create database, use, show databases)').addOption(new _commander.Option('-o, --output <format>', 'how results print (at the prompt: /output <format>)').choices(_output.outputFormats).default('table')).option('--null <text>', 'how a table shows null (default NULL; at the prompt: /null <text>)').option('--missing <text>', 'how a table shows a missing field (default —; at the prompt: /missing <text>)').helpOption('-h, --help', 'print this help').addHelpText('after', `
|
|
18
|
+
Examples:
|
|
19
|
+
jfsq open the prompt on database/
|
|
20
|
+
jfsq -d tasks-db -q "read tasks where done = false"
|
|
21
|
+
jfsq --databases dbs -d real -q "show records"
|
|
22
|
+
jfsq -o json -q "read tasks" | jq .
|
|
23
|
+
jfsq -o csv -q "read tasks" > tasks.csv`).parse(process.argv);
|
|
24
|
+
const options = program.opts();
|
|
25
|
+
// Shared with the prompt, whose /output, /null and /missing commands change it
|
|
26
|
+
const settings = {
|
|
27
|
+
format: options.output,
|
|
28
|
+
markers: {
|
|
29
|
+
...(typeof options.null === 'string' ? {
|
|
30
|
+
null: options.null
|
|
31
|
+
} : {}),
|
|
32
|
+
...(typeof options.missing === 'string' ? {
|
|
33
|
+
missing: options.missing
|
|
34
|
+
} : {})
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
// With -q the query runs straight away and the exit code says whether it worked; otherwise the prompt opens
|
|
38
|
+
const session = typeof options.query === 'string' ? (() => {
|
|
39
|
+
const done = (0, _runDirect.runDirect)(options.query, options, settings).then(code => {
|
|
40
|
+
// exitCode rather than exit(): process.exit can cut off output still being written to a pipe
|
|
41
|
+
process.exitCode = code;
|
|
42
|
+
});
|
|
43
|
+
return {
|
|
44
|
+
whenIdle: () => done
|
|
45
|
+
};
|
|
46
|
+
})() : (0, _interactive.startInteractive)(options, settings);
|
|
47
|
+
/**
|
|
48
|
+
* Resolves once the session is idle: every line handled (the prompt), or the query finished (-q).
|
|
49
|
+
* @returns {Promise<void>}
|
|
50
|
+
*/
|
|
51
|
+
const whenIdle = () => session.whenIdle();
|
|
52
|
+
exports.whenIdle = whenIdle;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.whenIdle=void 0;var _commander=require("commander"),_interactive=require("./interactive"),_output=require("./output"),_runDirect=require("./runDirect");const{version:version}=require("../../package.json"),program=(new _commander.Command).name("jfsq").description("Query a json-fs-query database from the command line: with -q, run the query and exit; without it, open the prompt.").version(version,"-v, --version","print the version").option("-q, --query <query>","run this query (or several, separated by ;), print the results and exit").option("-d, --database <path>","the database folder (default database/); with --databases, the name of a database in that folder").option("--databases <folder>","a folder holding several databases (create database, use, show databases)").addOption(new _commander.Option("-o, --output <format>","how results print (at the prompt: /output <format>)").choices(_output.outputFormats).default("table")).option("--null <text>","how a table shows null (default NULL; at the prompt: /null <text>)").option("--missing <text>","how a table shows a missing field (default —; at the prompt: /missing <text>)").helpOption("-h, --help","print this help").addHelpText("after",'\nExamples:\n jfsq open the prompt on database/\n jfsq -d tasks-db -q "read tasks where done = false"\n jfsq --databases dbs -d real -q "show records"\n jfsq -o json -q "read tasks" | jq .\n jfsq -o csv -q "read tasks" > tasks.csv').parse(process.argv),options=program.opts(),settings={format:options.output,markers:{..."string"==typeof options.null?{null:options.null}:{},..."string"==typeof options.missing?{missing:options.missing}:{}}},session="string"==typeof options.query?(()=>{const e=(0,_runDirect.runDirect)(options.query,options,settings).then((e=>{process.exitCode=e}));return{whenIdle:()=>e}})():(0,_interactive.startInteractive)(options,settings),whenIdle=()=>session.whenIdle();exports.whenIdle=whenIdle;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { DatabaseOptions } from './chooseDatabase';
|
|
2
|
+
import { OutputSettings } from './output';
|
|
3
|
+
export type Session = {
|
|
4
|
+
whenIdle: () => Promise<void>;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Open the prompt: read queries line by line, run each one once it is complete, until /exit, Ctrl+C or the end of
|
|
8
|
+
* piped input. Piped input (`jfsq < seed.txt`, stdin not a terminal) runs the same way, but quietly: no banner, no
|
|
9
|
+
* prompts and no goodbye, only the results and errors, and the exit code is 1 when any query failed.
|
|
10
|
+
* @param options The database options
|
|
11
|
+
* @param settings The output format and markers (the /output, /null and /missing commands change them)
|
|
12
|
+
* @returns {Session}
|
|
13
|
+
*/
|
|
14
|
+
export declare const startInteractive: (options: DatabaseOptions, settings: OutputSettings) => Session;
|
|
15
|
+
//# sourceMappingURL=interactive.d.ts.map
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.startInteractive = void 0;
|
|
7
|
+
require("core-js/modules/es.array.includes.js");
|
|
8
|
+
var readline = _interopRequireWildcard(require("readline"));
|
|
9
|
+
var _util = require("util");
|
|
10
|
+
var _queryStatus = require("../data/queries/queryStatus");
|
|
11
|
+
var _query = require("../data/queries/query");
|
|
12
|
+
var _config = require("../data/utilities/config");
|
|
13
|
+
var _chooseDatabase = require("./chooseDatabase");
|
|
14
|
+
var _formatResult = require("./formatResult");
|
|
15
|
+
var _output = require("./output");
|
|
16
|
+
var e;
|
|
17
|
+
function _interopRequireWildcard(t, r) { if (!r && t && t.__esModule) return t; var n, a, o = Object.defineProperty, f = { __proto__: null, default: t }; if (Object(t) !== t) return f; if (e || "function" != typeof WeakMap || (e = new WeakMap()), e) { if (e.has(t)) return e.get(t); e.set(t, f); } for (a in t) "default" !== a && {}.hasOwnProperty.call(t, a) && ((n = o && Object.getOwnPropertyDescriptor(t, a)) && (n.get || n.set) ? o(f, a, n) : f[a] = t[a]); return f; }
|
|
18
|
+
const defaultPrompt = 'JSON-Query> ';
|
|
19
|
+
// A CLI command: a slash and a word, then its value (a query never starts like this; a `/*` comment has no word)
|
|
20
|
+
const commandMatch = /^\/([a-z]+)(?:\s+(.*))?$/i;
|
|
21
|
+
/**
|
|
22
|
+
* Whether text holds anything to run: something other than whitespace and comments.
|
|
23
|
+
* @param text
|
|
24
|
+
* @returns {boolean}
|
|
25
|
+
*/
|
|
26
|
+
const hasQuery = text => text.replace(/\/\*[\s\S]*?\*\//g, '').replace(/--[^\n]*/g, '').trim() !== '';
|
|
27
|
+
/**
|
|
28
|
+
* The main prompt: the name of the database in use when there is one (it changes with `use`), otherwise JSON-Query.
|
|
29
|
+
* @returns {string}
|
|
30
|
+
*/
|
|
31
|
+
const mainPrompt = () => {
|
|
32
|
+
const database = (0, _config.getSetting)('database', undefined);
|
|
33
|
+
return database ? `${database}> ` : defaultPrompt;
|
|
34
|
+
};
|
|
35
|
+
// Continuation prompt, padded to line up with the first line and dimmed so it reads as secondary
|
|
36
|
+
const continuePrefix = waitingFor => (0, _util.styleText)('dim', `${waitingFor}> `.padStart(mainPrompt().length));
|
|
37
|
+
/**
|
|
38
|
+
* Open the prompt: read queries line by line, run each one once it is complete, until /exit, Ctrl+C or the end of
|
|
39
|
+
* piped input. Piped input (`jfsq < seed.txt`, stdin not a terminal) runs the same way, but quietly: no banner, no
|
|
40
|
+
* prompts and no goodbye, only the results and errors, and the exit code is 1 when any query failed.
|
|
41
|
+
* @param options The database options
|
|
42
|
+
* @param settings The output format and markers (the /output, /null and /missing commands change them)
|
|
43
|
+
* @returns {Session}
|
|
44
|
+
*/
|
|
45
|
+
const startInteractive = (options, settings) => {
|
|
46
|
+
// Reading a file or a pipe rather than a person typing
|
|
47
|
+
const piped = !process.stdin.isTTY;
|
|
48
|
+
// Create the interface for reading from standard input/output
|
|
49
|
+
const rl = readline.createInterface({
|
|
50
|
+
input: process.stdin,
|
|
51
|
+
output: process.stdout,
|
|
52
|
+
prompt: defaultPrompt
|
|
53
|
+
});
|
|
54
|
+
// 1 when the session can't start (a bad option), so a script can tell
|
|
55
|
+
let exitCode = 0;
|
|
56
|
+
// Set when a query fails: piped input then exits 1, so a seed script can tell (the prompt carries on regardless)
|
|
57
|
+
let failed = false;
|
|
58
|
+
// Buffer to store multi-line inputs
|
|
59
|
+
let inputBuffer = '';
|
|
60
|
+
// Set once readline closes. Piped input closes it as soon as stdin ends, while queued lines may still be running,
|
|
61
|
+
// and readline throws if prompted after that
|
|
62
|
+
let closed = false;
|
|
63
|
+
/**
|
|
64
|
+
* Show the prompt, unless the session has already closed.
|
|
65
|
+
*/
|
|
66
|
+
const showPrompt = () => {
|
|
67
|
+
if (!closed && !piped) {
|
|
68
|
+
rl.prompt();
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
// Lines are handled one at a time, in order: readline does not wait for an async 'line' handler, so without this
|
|
72
|
+
// queue a line typed (or piped) while a query runs would be appended to the buffer before that query finished.
|
|
73
|
+
let lineQueue = Promise.resolve();
|
|
74
|
+
const whenIdle = async () => {
|
|
75
|
+
// A handled line can queue more work (e.g. /exit queues the goodbye), so wait until the queue stops growing
|
|
76
|
+
let current;
|
|
77
|
+
do {
|
|
78
|
+
current = lineQueue;
|
|
79
|
+
await current;
|
|
80
|
+
} while (current !== lineQueue);
|
|
81
|
+
};
|
|
82
|
+
if (!piped) {
|
|
83
|
+
console.log('JSON Filesystem Query CLI');
|
|
84
|
+
console.log('Type your command. End it with a semicolon (;) and press Enter to execute.');
|
|
85
|
+
console.log('Type "/exit" or press Ctrl+C to quit.');
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Print a confirmation for a person typing; piped input stays quiet, so its output is only results (e.g. for jq).
|
|
89
|
+
* @param {string} message
|
|
90
|
+
*/
|
|
91
|
+
const say = message => {
|
|
92
|
+
if (!piped) {
|
|
93
|
+
console.log(message);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Run one CLI command. Without a value each one says what it is set to now.
|
|
98
|
+
* @param {string} line The trimmed line, e.g. `/output csv` or `/null (null)`
|
|
99
|
+
*/
|
|
100
|
+
const runCommand = line => {
|
|
101
|
+
const [, name, value = ''] = line.match(commandMatch);
|
|
102
|
+
switch (name.toLowerCase()) {
|
|
103
|
+
case 'output':
|
|
104
|
+
{
|
|
105
|
+
const outputFormat = value.toLowerCase();
|
|
106
|
+
if (value && !_output.outputFormats.includes(outputFormat)) {
|
|
107
|
+
failed = true;
|
|
108
|
+
(0, _output.printError)(`Unknown output "${value}": use ${_output.outputFormats.join(', ')}`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (value) {
|
|
112
|
+
settings.format = outputFormat;
|
|
113
|
+
}
|
|
114
|
+
say(`Output: ${settings.format}`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
case 'null':
|
|
118
|
+
case 'missing':
|
|
119
|
+
{
|
|
120
|
+
const marker = name.toLowerCase();
|
|
121
|
+
if (value) {
|
|
122
|
+
settings.markers = {
|
|
123
|
+
...settings.markers,
|
|
124
|
+
[marker]: value
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
say(`${marker === 'null' ? 'Null' : 'Missing'} shows as: ${settings.markers[marker] ?? _formatResult.defaultMarkers[marker]}`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
default:
|
|
131
|
+
failed = true;
|
|
132
|
+
(0, _output.printError)(`Unknown command /${name}: use /output <${_output.outputFormats.join('|')}>, /null <text>, /missing <text> or /exit`);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Run a completed buffer through the query parser. A parse or runtime error is printed and the session carries on.
|
|
137
|
+
* @param {string} input The completed query buffer (one or more queries)
|
|
138
|
+
* @returns {Promise<void>}
|
|
139
|
+
*/
|
|
140
|
+
const processInput = async input => {
|
|
141
|
+
try {
|
|
142
|
+
(0, _output.assertPrintable)(settings);
|
|
143
|
+
(0, _output.printResults)(await (0, _query.query)(input), settings);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
failed = true;
|
|
146
|
+
(0, _output.printError)(error);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Handle one line of input: exit, skip, add to the buffer, or run the buffer once the query is complete.
|
|
151
|
+
* @param {string} line The line as typed, without its line ending
|
|
152
|
+
* @returns {Promise<void>}
|
|
153
|
+
*/
|
|
154
|
+
const handleLine = async line => {
|
|
155
|
+
// The session didn't start (a bad option): lines piped after it must not run against the wrong database
|
|
156
|
+
if (exitCode) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const trimmedLine = line.trim();
|
|
160
|
+
// Check if the user wants to exit
|
|
161
|
+
if (!inputBuffer && trimmedLine.toLowerCase() === '/exit') {
|
|
162
|
+
rl.close();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
// A CLI command (/output, /null, /missing) at the start of a query: never sent to the query parser
|
|
166
|
+
if (!inputBuffer && commandMatch.test(trimmedLine)) {
|
|
167
|
+
runCommand(trimmedLine);
|
|
168
|
+
showPrompt();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
// Skip blank lines on their own, accept them mid-query
|
|
172
|
+
if (!inputBuffer && !trimmedLine) {
|
|
173
|
+
showPrompt();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
// Append current line to the buffer
|
|
177
|
+
inputBuffer += (inputBuffer ? '\n' : '') + line;
|
|
178
|
+
const status = (0, _queryStatus.queryStatus)(inputBuffer);
|
|
179
|
+
// Check if the line ends the query (closed query and semicolon present)
|
|
180
|
+
if (status.complete) {
|
|
181
|
+
// Take the buffer before running, so the next query starts empty even if this one fails
|
|
182
|
+
const input = inputBuffer;
|
|
183
|
+
inputBuffer = '';
|
|
184
|
+
await processInput(input);
|
|
185
|
+
// After the query, so a `use` in it shows in the prompt
|
|
186
|
+
rl.setPrompt(mainPrompt());
|
|
187
|
+
} else {
|
|
188
|
+
// Change prompt visually to show it is expecting continuation
|
|
189
|
+
rl.setPrompt(continuePrefix(status.waitingFor));
|
|
190
|
+
}
|
|
191
|
+
showPrompt();
|
|
192
|
+
};
|
|
193
|
+
// Handle line-by-line input, queued so each line waits for the one before it
|
|
194
|
+
rl.on('line', line => {
|
|
195
|
+
lineQueue = lineQueue.then(() => handleLine(line))
|
|
196
|
+
// Never let one failed line stop the queue (query errors are already handled in processInput)
|
|
197
|
+
.catch(error => {
|
|
198
|
+
(0, _output.printError)(error);
|
|
199
|
+
showPrompt();
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
rl.on('SIGINT', () => {
|
|
203
|
+
if (!inputBuffer && !rl.line) {
|
|
204
|
+
rl.close();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
rl.write(null, {
|
|
208
|
+
ctrl: true,
|
|
209
|
+
name: 'u'
|
|
210
|
+
});
|
|
211
|
+
inputBuffer = '';
|
|
212
|
+
rl.setPrompt(mainPrompt());
|
|
213
|
+
showPrompt();
|
|
214
|
+
});
|
|
215
|
+
// Handle graceful exit on Ctrl+C, /exit or the end of piped input, after any queued lines have run
|
|
216
|
+
rl.on('close', () => {
|
|
217
|
+
closed = true;
|
|
218
|
+
lineQueue = lineQueue.then(async () => {
|
|
219
|
+
// A file whose last query has no closing `;` still runs it (unless all that's left is comments or space)
|
|
220
|
+
if (piped && !exitCode && hasQuery(inputBuffer)) {
|
|
221
|
+
const input = inputBuffer;
|
|
222
|
+
inputBuffer = '';
|
|
223
|
+
await processInput(input);
|
|
224
|
+
}
|
|
225
|
+
if (!exitCode && !piped) {
|
|
226
|
+
console.log('\nGoodbye!');
|
|
227
|
+
}
|
|
228
|
+
process.exit(exitCode || (piped && failed ? 1 : 0));
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
/**
|
|
232
|
+
* Point the session at the database the options name, then open the prompt. A bad option (an invalid name, a
|
|
233
|
+
* database that doesn't exist) prints the error and ends the session with exit code 1.
|
|
234
|
+
* @returns {Promise<void>}
|
|
235
|
+
*/
|
|
236
|
+
const start = async () => {
|
|
237
|
+
try {
|
|
238
|
+
const database = await (0, _chooseDatabase.chooseDatabase)(options);
|
|
239
|
+
if (!piped) {
|
|
240
|
+
console.log(`${database}\n`);
|
|
241
|
+
}
|
|
242
|
+
} catch (error) {
|
|
243
|
+
(0, _output.printError)(error);
|
|
244
|
+
exitCode = 1;
|
|
245
|
+
rl.close();
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
rl.setPrompt(mainPrompt());
|
|
249
|
+
showPrompt();
|
|
250
|
+
};
|
|
251
|
+
// Lines typed before the session is ready wait for it
|
|
252
|
+
lineQueue = start();
|
|
253
|
+
return {
|
|
254
|
+
whenIdle
|
|
255
|
+
};
|
|
256
|
+
};
|
|
257
|
+
exports.startInteractive = startInteractive;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.startInteractive=void 0,require("core-js/modules/es.array.includes.js");var e,readline=_interopRequireWildcard(require("readline")),_util=require("util"),_queryStatus=require("../data/queries/queryStatus"),_query=require("../data/queries/query"),_config=require("../data/utilities/config"),_chooseDatabase=require("./chooseDatabase"),_formatResult=require("./formatResult"),_output=require("./output");function _interopRequireWildcard(t,r){if(!r&&t&&t.__esModule)return t;var o,s,a=Object.defineProperty,n={__proto__:null,default:t};if(Object(t)!==t)return n;if(e||"function"!=typeof WeakMap||(e=new WeakMap),e){if(e.has(t))return e.get(t);e.set(t,n)}for(s in t)"default"!==s&&{}.hasOwnProperty.call(t,s)&&((o=a&&Object.getOwnPropertyDescriptor(t,s))&&(o.get||o.set)?a(n,s,o):n[s]=t[s]);return n}const defaultPrompt="JSON-Query> ",commandMatch=/^\/([a-z]+)(?:\s+(.*))?$/i,hasQuery=t=>""!==t.replace(/\/\*[\s\S]*?\*\//g,"").replace(/--[^\n]*/g,"").trim(),mainPrompt=()=>{const t=(0,_config.getSetting)("database",void 0);return t?`${t}> `:defaultPrompt},continuePrefix=t=>(0,_util.styleText)("dim",`${t}> `.padStart(mainPrompt().length)),startInteractive=(t,e)=>{const r=!process.stdin.isTTY,o=readline.createInterface({input:process.stdin,output:process.stdout,prompt:defaultPrompt});let s=0,a=!1,n="",u=!1;const i=()=>{u||r||o.prompt()};let c=Promise.resolve();r||(console.log("JSON Filesystem Query CLI"),console.log("Type your command. End it with a semicolon (;) and press Enter to execute."),console.log('Type "/exit" or press Ctrl+C to quit.'));const l=t=>{r||console.log(t)},p=async t=>{try{(0,_output.assertPrintable)(e),(0,_output.printResults)(await(0,_query.query)(t),e)}catch(t){a=!0,(0,_output.printError)(t)}},m=async t=>{if(s)return;const r=t.trim();if(!n&&"/exit"===r.toLowerCase())return void o.close();if(!n&&commandMatch.test(r))return(t=>{const[,r,o=""]=t.match(commandMatch);switch(r.toLowerCase()){case"output":{const t=o.toLowerCase();return o&&!_output.outputFormats.includes(t)?(a=!0,void(0,_output.printError)(`Unknown output "${o}": use ${_output.outputFormats.join(", ")}`)):(o&&(e.format=t),void l(`Output: ${e.format}`))}case"null":case"missing":{const t=r.toLowerCase();return o&&(e.markers={...e.markers,[t]:o}),void l(`${"null"===t?"Null":"Missing"} shows as: ${e.markers[t]??_formatResult.defaultMarkers[t]}`)}default:a=!0,(0,_output.printError)(`Unknown command /${r}: use /output <${_output.outputFormats.join("|")}>, /null <text>, /missing <text> or /exit`)}})(r),void i();if(!n&&!r)return void i();n+=(n?"\n":"")+t;const u=(0,_queryStatus.queryStatus)(n);if(u.complete){const t=n;n="",await p(t),o.setPrompt(mainPrompt())}else o.setPrompt(continuePrefix(u.waitingFor));i()};o.on("line",(t=>{c=c.then((()=>m(t))).catch((t=>{(0,_output.printError)(t),i()}))})),o.on("SIGINT",(()=>{n||o.line?(o.write(null,{ctrl:!0,name:"u"}),n="",o.setPrompt(mainPrompt()),i()):o.close()})),o.on("close",(()=>{u=!0,c=c.then((async()=>{if(r&&!s&&hasQuery(n)){const t=n;n="",await p(t)}s||r||console.log("\nGoodbye!"),process.exit(s||(r&&a?1:0))}))}));return c=(async()=>{try{const e=await(0,_chooseDatabase.chooseDatabase)(t);r||console.log(`${e}\n`)}catch(t){return(0,_output.printError)(t),s=1,void o.close()}o.setPrompt(mainPrompt()),i()})(),{whenIdle:async()=>{let t;do{t=c,await t}while(t!==c)}}};exports.startInteractive=startInteractive;
|