fix(lookupRecord): search every open database when databaseName is omitted#34
fix(lookupRecord): search every open database when databaseName is omitted#34ebowman wants to merge 1 commit into
Conversation
…itted Previously, when `databaseName` was not supplied, `lookup_record` silently scoped to `theApp.currentDatabase()`. This caused lookups to miss records that exist in other open databases, with no signal to the caller that the scope had been narrowed — `success: true` with zero results. DEVONthink's `lookupRecordsWith*` AppleScript APIs (unlike `search`) don't natively span all open databases when `in:` is omitted, so the fix iterates `theApp.databases()` and unions the per-database results, deduplicating by UUID. The `x-devonthink-item://` URL shortcut still resolves globally via `getRecordWithUuid` and short-circuits the loop. Schema and tool descriptions updated to document the new default. Tests: 11 unit tests verifying script generation across all six `lookupType` branches, scoping behavior with and without `databaseName`, URL shortcut, matchAnyTag, and UUID dedup.
There was a problem hiding this comment.
Code Review
This pull request enhances the lookup_record tool by allowing it to search across all open databases by default, rather than just the current one. It also introduces a databaseName parameter to scope searches to a specific database and adds comprehensive unit tests for these new behaviors. Feedback focuses on critical security and correctness issues: user-provided strings are currently interpolated into JXA scripts without escaping, posing a script injection risk, and object literals are used in a way that can cause ReferenceError in the JXA environment. Additionally, implementing early exits in the search loops when a limit is reached would improve performance for multi-database queries.
| if ("${databaseName || ""}") { | ||
| const databases = theApp.databases(); | ||
| searchDatabase = databases.find(db => db.name() === "${databaseName}"); | ||
| if (!searchDatabase) { | ||
| const targetDb = databases.find(db => db.name() === "${databaseName}"); |
There was a problem hiding this comment.
User-provided strings like databaseName and value are interpolated directly into the JXA script without escaping. This violates the repository style guide regarding safe string escaping and poses a security risk (script injection) as well as a correctness issue if the values contain double quotes. Although the PR description mentions a follow-up, it is highly recommended to use the escapeString utility for all user inputs in JXA scripts to ensure robustness.
References
- All user inputs must be properly escaped to prevent script injection and handle special characters correctly. (link)
| const tagOptions = { in: searchDatabase }; | ||
| if (${matchAnyTag}) { | ||
| tagOptions.any = true; | ||
| } | ||
| dbResults = theApp.lookupRecordsWithTags(tagArray, tagOptions); |
There was a problem hiding this comment.
Following the repository style guide, object properties in JXA scripts should be assigned using bracket notation instead of object literals. This avoids potential ReferenceError issues during script execution, which is particularly important for reserved keywords like in. This pattern should also be applied to other lookup calls in this switch statement (e.g., lines 92, 95, 116, 121, 124).
| const tagOptions = { in: searchDatabase }; | |
| if (${matchAnyTag}) { | |
| tagOptions.any = true; | |
| } | |
| dbResults = theApp.lookupRecordsWithTags(tagArray, tagOptions); | |
| const tagOptions = {}; | |
| tagOptions["in"] = searchDatabase; | |
| if (${matchAnyTag}) { | |
| tagOptions["any"] = true; | |
| } | |
| dbResults = theApp.lookupRecordsWithTags(tagArray, tagOptions); |
References
- Use bracket notation for object property assignment in JXA to avoid ReferenceError issues with object literals in template literals. (link)
| } | ||
|
|
||
| if (dbResults && dbResults.length > 0) { | ||
| for (let i = 0; i < dbResults.length; i++) { |
There was a problem hiding this comment.
To improve performance when searching across multiple databases, consider checking the limit inside the loops and breaking early once the desired number of results is reached. Currently, the script processes all matches across all databases before applying the limit at the end, which can be resource-intensive for generic queries or when many databases are open.
| if (urlValue.startsWith(dtPrefix)) { | ||
| // x-devonthink-item:// resolves globally via UUID — do it once, | ||
| // not per-database, then short-circuit the loop. | ||
| if (dbIdx === 0) { |
There was a problem hiding this comment.
This reads a bit like a code smell: a for loop, then if url type, always on first iteration of the loop short-circuit. If theApp.getRecordWithUuid is global and not scoped to db, we should do this check first before iterating over all the databases IMO
There was a problem hiding this comment.
Example:
// Handle x-devonthink-item:// globally — it resolves by UUID, not per-database.
if ("${lookupType}" === "url") {
const urlValue = "${value}";
const dtPrefix = "x-devonthink-item://";
if (urlValue.startsWith(dtPrefix)) {
const identifier = decodeURIComponent(urlValue.substring(dtPrefix.length));
const record = theApp.getRecordWithUuid(identifier);
if (record && record.exists()) {
searchResults = [record];
}
// Skip the per-database loop entirely — we're done.
// (jump to the result-formatting logic below)
}
}
// Only enter the per-database loop if we haven't already resolved.
if (searchResults.length === 0) {
for (let dbIdx = 0; dbIdx < searchDatabases.length; dbIdx++) {
// ... url case now only handles non-DT URLs via lookupRecordsWithURL
}
}
|
Thanks for the PR! I left a small quality comment. Could you take a look? |
Problem
lookup_recordsilently scopes totheApp.currentDatabase()whendatabaseNameis omitted, masking the fact that other open databases were never searched. Callers seesuccess: truewith empty results and have no way to tell whether the record genuinely doesn't exist or just lives in a database other than the one the user happens to have focused.Concretely (on a session where
currentDatabaseis database A):The same call succeeds the moment you pass
databaseName: "B". The bug was discovered during regression testing against DEVONthink 4 — six differentlookupTypecalls all returned 0 results despite the records being present andget_record_propertiesfinding them by UUID in the same session.Fix
When
databaseNameis supplied: scope to that one database (unchanged from before).When it is omitted: iterate
theApp.databases()and union the per-database results, deduplicating by UUID.Unlike
theApp.search(), thelookupRecordsWithFile/Path/URL/Comment/ContentHash/TagsAPIs don't natively span all open databases whenin:is omitted — they appear to return nothing in that case — so the union has to be done in the script. The cost is one AppleScript call per open database, capped by the existinglimitparameter on the result side.The
x-devonthink-item://URL shortcut continues to resolve globally viagetRecordWithUuidand short-circuits the loop after the first iteration.Schema description for
databaseNameand the tool'sdescriptionstring have been updated so callers know the default scope is "every open database".Test plan
npm test— 27/27 pass, including 11 new tests intests/tools/lookupRecord.test.tscovering:databaseName→ iteratestheApp.databases(), doesn't fall back tocurrentDatabase())databaseName: "MyDB"→ resolves to[targetDb])lookupTypebranches dispatch to the correct AppleScript APIx-devonthink-item://shortcut short-circuits the loopmatchAnyTagflag is honorednpm run buildcleannpm run format:checkcleantags: ["regression"](nodatabaseName) → finds the record inGTD Reference(previously returned 0)filename: "X.md"(nodatabaseName) → finds the recordtags: ["regression"], databaseName: "King"→ 0 (King doesn't have this tag — explicit scope honored)tags: ["regression"], databaseName: "GTD Reference"→ 1Notes
lookupRecord.ts(raw${value}interpolation can still break the script on values containing double quotes / newlines). Kept separate to keep this PR focused on the scope fix.