actual-mcp-server 0.16.11 → 0.16.12
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
CHANGED
|
@@ -775,7 +775,7 @@ The software is provided **as-is**, without warranty of any kind. The author acc
|
|
|
775
775
|
|
|
776
776
|
---
|
|
777
777
|
|
|
778
|
-
**Version:** 0.16.
|
|
778
|
+
**Version:** 0.16.12 | **Tool Count:** 74 (verified LibreChat-compatible)
|
|
779
779
|
|
|
780
780
|
## Sponsor
|
|
781
781
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// calls and string parsing, with no module state or side effects. parseWhereClause
|
|
4
4
|
// is re-exported from actual-adapter.ts and is unit-tested directly. The #178
|
|
5
5
|
// operator support (LIKE / NOT LIKE / IS NULL, throw-on-unsupported) lives here.
|
|
6
|
+
import { getFieldType } from '../actual-schema.js';
|
|
6
7
|
// Strip a single pair of surrounding quotes from a SQL value literal.
|
|
7
8
|
function _stripWhereQuotes(s) {
|
|
8
9
|
return s.trim().replace(/^['"]|['"]$/g, '');
|
|
@@ -17,7 +18,38 @@ function _coerceWhereValue(s) {
|
|
|
17
18
|
const n = Number(v);
|
|
18
19
|
return isNaN(n) ? v : n;
|
|
19
20
|
}
|
|
20
|
-
|
|
21
|
+
// #420: coerce a SQL literal to a real JavaScript boolean, for a column the schema types as
|
|
22
|
+
// boolean. ActualQL requires a genuine JS boolean here: its compiler only tags a value as
|
|
23
|
+
// `boolean` from `typeof value === 'boolean'`, and `castInput` has no string-to-boolean or
|
|
24
|
+
// integer-to-boolean branch, so `"true"` and `1` are both rejected with the exact errors #420
|
|
25
|
+
// reports.
|
|
26
|
+
//
|
|
27
|
+
// Accepts `true`/`false` case-insensitively AND `1`/`0`. The integer form is accepted on purpose:
|
|
28
|
+
// the tool advertises SQL, and every mainstream engine (SQLite, which is Actual's own store, plus
|
|
29
|
+
// MySQL/MariaDB/SQL Server/Postgres) treats `1`/`0` as booleans. Refusing it would make this tool
|
|
30
|
+
// stricter than the database it models. A value that is neither throws, naming the column and the
|
|
31
|
+
// value, because a bad boolean literal is a caller mistake we can describe precisely.
|
|
32
|
+
function _coerceBoolean(field, s) {
|
|
33
|
+
const v = _stripWhereQuotes(s).toLowerCase();
|
|
34
|
+
if (v === 'true' || v === '1')
|
|
35
|
+
return true;
|
|
36
|
+
if (v === 'false' || v === '0')
|
|
37
|
+
return false;
|
|
38
|
+
throw new Error(`Invalid boolean value for column "${field}": ${JSON.stringify(_stripWhereQuotes(s))}. ` +
|
|
39
|
+
`Use true or false (1 or 0 are also accepted).`);
|
|
40
|
+
}
|
|
41
|
+
// Whether the schema types `field` (a plain or joined column) as boolean, given the base table.
|
|
42
|
+
// An unknown field is NOT boolean, so its value is left untouched: this is what keeps a string
|
|
43
|
+
// comparison like `notes = 'true'` a string, and the query validator handles the unknown-field
|
|
44
|
+
// case separately.
|
|
45
|
+
function _isBooleanField(tableName, field) {
|
|
46
|
+
return tableName !== undefined && getFieldType(tableName, field) === 'boolean';
|
|
47
|
+
}
|
|
48
|
+
// `tableName` is the FROM table, used to look up column types so boolean literals can be coerced
|
|
49
|
+
// (#420). It is optional only so the many existing unit tests that assert non-boolean behaviour
|
|
50
|
+
// need not all pass it; the production call site in actual-adapter.ts always does. When it is
|
|
51
|
+
// absent, no field resolves as boolean and the pre-#420 behaviour is exactly preserved.
|
|
52
|
+
export function parseWhereClause(query, whereClause, tableName) {
|
|
21
53
|
// OR is not supported. Detect it up front and fail loudly. Without this guard
|
|
22
54
|
// a clause like `amount = 100 OR amount < 0` is left as a single fragment by
|
|
23
55
|
// the AND-splitter, and the comparison regex's greedy value capture swallows
|
|
@@ -68,8 +100,20 @@ export function parseWhereClause(query, whereClause) {
|
|
|
68
100
|
const inMatch = trimmedCondition.match(/^([\w.]+)\s+IN\s+\((.+)\)$/i);
|
|
69
101
|
if (inMatch) {
|
|
70
102
|
const [, field, valuesStr] = inMatch;
|
|
71
|
-
const
|
|
72
|
-
|
|
103
|
+
const rawValues = valuesStr.split(',');
|
|
104
|
+
if (_isBooleanField(tableName, field)) {
|
|
105
|
+
// #420: `$oneof` STRINGIFIES every element (upstream `compiler.ts` emits
|
|
106
|
+
// `'${String(id)}'`), so `is_parent IN (true, false)` would compile to
|
|
107
|
+
// `IN ('true','false')` against a 0/1 column and silently match nothing, which is worse
|
|
108
|
+
// than the loud error we produce today. Expand to `$or` of equalities instead: each inner
|
|
109
|
+
// equality then routes through `castInput(..., 'boolean')` and works. `$or` is a real
|
|
110
|
+
// ActualQL condition key (compiler.ts handles `field === '$or'` via `compileOr`).
|
|
111
|
+
const orConditions = rawValues.map((v) => ({ [field]: _coerceBoolean(field, v) }));
|
|
112
|
+
query = query.filter({ $or: orConditions });
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
query = query.filter({ [field]: { $oneof: rawValues.map(_coerceWhereValue) } });
|
|
116
|
+
}
|
|
73
117
|
continue;
|
|
74
118
|
}
|
|
75
119
|
// Comparison operators: field >= value, field = value, etc.
|
|
@@ -86,7 +130,12 @@ export function parseWhereClause(query, whereClause) {
|
|
|
86
130
|
'!=': '$ne',
|
|
87
131
|
};
|
|
88
132
|
const actualOp = operatorMap[operator];
|
|
89
|
-
|
|
133
|
+
// #420: on a boolean column the literal must reach ActualQL as a real JS boolean, not the
|
|
134
|
+
// string "true" or the number 1. Only = and != are meaningful for a boolean; the ordering
|
|
135
|
+
// operators are left to their normal path, where ActualQL will reject them for a boolean as
|
|
136
|
+
// it does today, rather than us inventing an ordering the column does not have.
|
|
137
|
+
const isBool = _isBooleanField(tableName, field) && (actualOp === '$eq' || actualOp === '$ne');
|
|
138
|
+
const finalValue = isBool ? _coerceBoolean(field, valueStr) : _coerceWhereValue(valueStr);
|
|
90
139
|
if (actualOp === '$eq') {
|
|
91
140
|
// Simple equality can use the direct field: value shorthand.
|
|
92
141
|
query = query.filter({ [field]: finalValue });
|
|
@@ -2659,9 +2659,10 @@ export async function runQuery(queryString) {
|
|
|
2659
2659
|
});
|
|
2660
2660
|
query = query.select(fieldList);
|
|
2661
2661
|
}
|
|
2662
|
-
// Apply WHERE conditions
|
|
2662
|
+
// Apply WHERE conditions. tableName is passed so parseWhereClause can resolve column
|
|
2663
|
+
// types and coerce boolean literals to real booleans (#420).
|
|
2663
2664
|
if (whereClause) {
|
|
2664
|
-
query = parseWhereClause(query, whereClause);
|
|
2665
|
+
query = parseWhereClause(query, whereClause, tableName);
|
|
2665
2666
|
}
|
|
2666
2667
|
// Apply ORDER BY
|
|
2667
2668
|
if (orderField) {
|
|
@@ -211,3 +211,25 @@ export function isValidField(tableName, fieldName) {
|
|
|
211
211
|
export function isValidJoinPath(path) {
|
|
212
212
|
return path in JOIN_PATHS;
|
|
213
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Resolve a WHERE field to its declared type, for the given base table.
|
|
216
|
+
*
|
|
217
|
+
* Handles both a plain column (`is_parent`, resolved against `tableName`) and a joined field
|
|
218
|
+
* (`category.hidden`, resolved through JOIN_PATHS to the target table's column). Returns the
|
|
219
|
+
* `type` string from the schema, or `undefined` when the field is not known.
|
|
220
|
+
*
|
|
221
|
+
* #420: used to decide whether a WHERE literal must be coerced to a real boolean. It is
|
|
222
|
+
* deliberately conservative: an unknown field returns `undefined` and the caller then leaves the
|
|
223
|
+
* value untouched, so this can never turn a valid string comparison into a boolean by accident.
|
|
224
|
+
* The query validator (`query-validator.ts`) is what rejects an unknown field; this only reports a
|
|
225
|
+
* type when it is certain of one.
|
|
226
|
+
*/
|
|
227
|
+
export function getFieldType(tableName, field) {
|
|
228
|
+
if (field.includes('.')) {
|
|
229
|
+
const join = JOIN_PATHS[field];
|
|
230
|
+
if (!join)
|
|
231
|
+
return undefined;
|
|
232
|
+
return ACTUAL_SCHEMA[join.table]?.[join.field]?.type;
|
|
233
|
+
}
|
|
234
|
+
return ACTUAL_SCHEMA[tableName]?.[field]?.type;
|
|
235
|
+
}
|
|
@@ -24,6 +24,11 @@ Supported WHERE operators:
|
|
|
24
24
|
• LIKE / NOT LIKE for pattern search (case-insensitive, accent-insensitive; use % as wildcard)
|
|
25
25
|
e.g. "WHERE imported_payee LIKE '%amazon%'" to find raw bank-sync payee strings
|
|
26
26
|
• IS NULL / IS NOT NULL e.g. "WHERE imported_payee IS NULL" to find unmerged rows
|
|
27
|
+
• Boolean columns take true / false (case-insensitive; 1 / 0 also accepted)
|
|
28
|
+
e.g. "WHERE cleared = false" for uncleared, "WHERE reconciled = true",
|
|
29
|
+
"WHERE category.hidden = true", "WHERE account.closed = true"
|
|
30
|
+
Note: transactions are returned in split-INLINE mode, so "WHERE is_parent = true" returns
|
|
31
|
+
nothing (parent rows are hidden); use "is_parent = false" to exclude split children.
|
|
27
32
|
• Combine conditions with AND. OR, REGEXP, NOT IN, and parenthesised groups are not yet
|
|
28
33
|
supported and will return an error (the query is never silently run unfiltered).
|
|
29
34
|
|