Skip to content

fix(lookupRecord): search every open database when databaseName is omitted#34

Open
ebowman wants to merge 1 commit into
dvcrn:mainfrom
ebowman:fix/lookuprecord-default-scope
Open

fix(lookupRecord): search every open database when databaseName is omitted#34
ebowman wants to merge 1 commit into
dvcrn:mainfrom
ebowman:fix/lookuprecord-default-scope

Conversation

@ebowman

@ebowman ebowman commented May 25, 2026

Copy link
Copy Markdown
Contributor

Problem

lookup_record silently scopes to theApp.currentDatabase() when databaseName is omitted, masking the fact that other open databases were never searched. Callers see success: true with 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 currentDatabase is database A):

// Record with tag "x" exists in database B
{ "lookupType": "tags", "tags": ["x"] }
// → { success: true, results: [], totalCount: 0 }  ❌

The same call succeeds the moment you pass databaseName: "B". The bug was discovered during regression testing against DEVONthink 4 — six different lookupType calls all returned 0 results despite the records being present and get_record_properties finding them by UUID in the same session.

Fix

When databaseName is 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(), the lookupRecordsWithFile/Path/URL/Comment/ContentHash/Tags APIs don't natively span all open databases when in: 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 existing limit parameter on the result side.

The x-devonthink-item:// URL shortcut continues to resolve globally via getRecordWithUuid and short-circuits the loop after the first iteration.

Schema description for databaseName and the tool's description string have been updated so callers know the default scope is "every open database".

Test plan

  • npm test — 27/27 pass, including 11 new tests in tests/tools/lookupRecord.test.ts covering:
    • Default-scope behavior (no databaseName → iterates theApp.databases(), doesn't fall back to currentDatabase())
    • Scoped behavior (databaseName: "MyDB" → resolves to [targetDb])
    • All six lookupType branches dispatch to the correct AppleScript API
    • x-devonthink-item:// shortcut short-circuits the loop
    • matchAnyTag flag is honored
    • UUID dedup across databases
  • npm run build clean
  • npm run format:check clean
  • End-to-end verification against a live DEVONthink session with 8 open databases:
    • tags: ["regression"] (no databaseName) → finds the record in GTD Reference (previously returned 0)
    • filename: "X.md" (no databaseName) → finds the record
    • tags: ["regression"], databaseName: "King" → 0 (King doesn't have this tag — explicit scope honored)
    • tags: ["regression"], databaseName: "GTD Reference" → 1

Notes

  • Behavior change: callers that relied on the implicit current-database scoping will now see results from other open databases. In practice this is strictly more useful — the prior behavior was silent narrowing — but worth flagging for the release notes.
  • A follow-up PR will harden the JXA string escaping in 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.

…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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/tools/lookupRecord.ts
Comment on lines 67 to +69
if ("${databaseName || ""}") {
const databases = theApp.databases();
searchDatabase = databases.find(db => db.name() === "${databaseName}");
if (!searchDatabase) {
const targetDb = databases.find(db => db.name() === "${databaseName}");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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
  1. All user inputs must be properly escaped to prevent script injection and handle special characters correctly. (link)

Comment thread src/tools/lookupRecord.ts
Comment on lines +131 to +135
const tagOptions = { in: searchDatabase };
if (${matchAnyTag}) {
tagOptions.any = true;
}
dbResults = theApp.lookupRecordsWithTags(tagArray, tagOptions);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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).

Suggested change
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
  1. Use bracket notation for object property assignment in JXA to avoid ReferenceError issues with object literals in template literals. (link)

Comment thread src/tools/lookupRecord.ts
}

if (dbResults && dbResults.length > 0) {
for (let i = 0; i < dbResults.length; i++) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Comment thread src/tools/lookupRecord.ts
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) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
    }
  }

@dvcrn

dvcrn commented May 27, 2026

Copy link
Copy Markdown
Owner

Thanks for the PR! I left a small quality comment. Could you take a look?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants