JavaScript: using whose with "type =" does not what I expect

According to Apple, the whose method should allow to drill down into arrays (I think it exists for AppleScript as well). So this

(() => {
	const app = Application("DEVONthink 3");
	let names = app.databases.records.whose({kind: {"=" : "Gruppe"}}).name();
	console.log(names);
})()

effectively (and very nicely, too) gives me the names of all groups in all of my databases:

--> [["Papierkorb", "Tags"], ["Papierkorb", "Eingang", "Tags"], ["Papierkorb", "Eingang", "Tags", "Sync", "Ausgaben", "Angebote", "Kontoauszüge", "Dokumente", "Korrespondenz"], ...] 

However, the DT doc clearly states that we should use the type property because it does not depend on the locale settings. But

(() => {
	const app = Application("DEVONthink 3");
	app.includeStandardAdditions = true; 

	const db = app.databases["Bru6"];
	let names = app.databases.records.whose({type: {"=" : "group"}}).name();
	console.log(names);
})()

returns nothing. Or rather: an array consisting of nine empty arrays:

app = Application("DEVONthink 3")
	app.databases.records.whose({_match: [ObjectSpecifier().type, "group"]}).name()
		--> [[], [], [], [], [], [], [], [], []]

The same happens with the simplified whose({type: "group"}). Am I misunderstanding the DT documentation, the working of whose or committing some other blunder? Or is this a bug somewhere?

Note: When I have the first script (i.e. the one that works) print out the type properties of the groups, it happily tells me that it is “group” in every case.

And to clarify: The same problem arises with type: "sheet" (and probably others).

EDIT: The same query works in AppleScript. But there, I have to use a constant named group (or any or whatever) instead of a string. So maybe I’ve to use something else but “group”, “any” etc. in JavaScript?

Which documentation actually? The one shown in the Script Editor.app? That’s just an automatic conversion of the documentation of the AppleScript support.

I tried various approaches, some of them work, some don’t. The joys of JXA :slight_smile:

	var app = Application("DEVONthink 3");
	app.includeStandardAdditions = true; 
	var theName = "Test";
	var theType = "group";

	// Works
	var allNames = app.currentDatabase().records.name();
	var allTypes = app.currentDatabase().records.type();

	// Works
	var namedRecords1 = app.currentDatabase().records.whose({name: {"=": theName}})();
	var namedRecords2 = app.currentDatabase().records.whose({name: theName})();

	// Fails
	var typedRecords1 = app.currentDatabase().records.whose({type: {"=": theType}})();
	var typedRecords2 = app.currentDatabase().records.whose({type: theType})();

	// Works
	var allRecords = app.currentDatabase().records();
	allRecords.forEach (r => {
	    var name = r.name();
	    if (name==theName) {
	    }
	})

	allRecords.forEach (r => {
	    var type = r.type();
	    if (type==theType) {
	    }
	})

Right. I’m singing only happy tunes since I discovered this marvel of automation. :notes:

As to the documentation in Script Editor: Yes, I guessed as much. The thing is: In AppleScript, all the values for type are constants. They probably have a value (integer? character?), and it might work with those values in JS, too. The whose function simply applies an operator (‘=’) to two objects. The left hand one being a property name, the right hand one should be … well, whatever works in AppleScript aka the values of the constants. Hopefully.

Internally these are four-char AppleEvent codes like ‘helo’ but these are definitely internals and shouldn’t be necessary. Otherwise the loops in my example should fail too.

Good point: theType == r.type() works, because r.type() returns a string representation of the type. On the other hand, ...whose({type: theType}) does not work. Because type is not cast to a string in this context? I definitly do not know enough about the internals of JXA, but it would be cool if it just worked :wink: Altough I can of course just search for the records, but AFAIK whose should be faster.

The search is probably a lot faster.

That’s debatable. I run a primitive benchmark (100 iterations) on three different approaches:

(db.records()).filter(el => {el.type() === "group" });

needs 35 ms per iteration

recs = app.search("type: group", {in: db.root()});

needs 11 ms per iteration

recs = db.records.whose({kind: "Gruppe"});

needs 0.15 ms per iteration.

So the whose implementation is about two orders of magnitude faster (aka 100 times) than search which is about twice as fast as filter.

Which makes it all the more interesting to actually make whose work with the type property. I guess that there’s just a tiny bit missing in the DT code that converts the value of type to a string before comparing.

If I use record.properties()["type"], I do get a string without any further ado, even the typeof of it returns “string”. So apparently, sometimes it is a string, just not in the context of whose.

The records of a database (db.records()) are just the records of the database’s root. This dates back to DEVONthink Pro 1.0 and hasn’t been changed for compatibility. And that’s why it’s a lot faster.

Do you mean “there are also other records that are not returned by db.records()”? If not, isn’t it still correct that whose is a hundred times faster than search?

Exactly. Depending on the database this means that a tiny percentage of your items might be returned. To get results really comparable to a search, you would have to use both contents() and parents() instead of records().

Even in this example the search is slightly faster although the whose variant doesn’t scan the contents, only the parents:

tell application id "DNtp"
	set theDB to current database
	set theDate to current date
	repeat with i from 1 to 100
		set theGroups to search "type:group type:!tag" in (root of theDB)
		-- set theGroups to (parents of theDB whose kind is "Gruppe")
	end repeat
	set theTime to (current date) - theDate
	return theTime / 100
end tell

And in this case the search is about 10 x faster than a simple whose query, at least in case of my test database (20 groups, more than 6000 tags, about 26500 documents)

tell application id "DNtp"
	set theDB to current database
	set theDate to current date
	repeat with i from 1 to 100
		set theBookmarks to search "type:bookmark" in (root of theDB)
		-- set theBookmarks to (contents of theDB whose type is bookmark)
	end repeat
	set theTime to (current date) - theDate
	return theTime / 100
end tell

This would probably merit its own thread.

Until now, I though that a database consists of records of different types (documents, groups, smart groups, tags). So that database.records() (in whatever scripting language) would return all records in this database. Now I’m wondering: what is contents() in a database (given that it inherits from records?) and what is parents() in a database? If I could nest databases, I’d understand the parent concept, but I don’t think I can (nest them, that is).

In my smallish database, I can now see that I have about 500 content records (those seem to be the “real” documents), 76 “parents” (which seem to 2nd level groups, tags and smart groups in the database) and 11 “records” (which seem to be top level (smart) groups).

If that’s correct, I’d suggest to add one or two sentences to the documentation describing these concepts a bit clearer. If it says “records”, I’d expect to get “records”, not only “top level (smart) groups”.

contents: All documents inside a database
parents: All containers (e.g. groups, tags or feeds) in a database
records: Basically identical to children of root.

Thanks, cool. So all “records” in a database would be records + contents + parents or is there an overlap between the three somewhere?

I would have thought (contents + parents) would contain records

Unless you don’t want to explicitly retrieve the children of the root, there’s no need to use records at all. It’s just there for compatibility.

Nope. parents and contents are disjunct (i.e. have no common elements) as are records and contents.

contents is only the records proper, i.e. no groups
parents is all containers (as @cgrunenberg said: groups, tags, feeds)
records is a subset of parents, i.e. the direct children of the database root (aka top level containers)

It’s a bit confusing and not clear at all from the scripting directory. Which is, of course, also due to the limitations of this format.

1 Like

records contains any item at the top level including documents.

image

As Criss noted, records returns only top-level items.
As seen here, this includes: Inbox, Trash, Tags group, smart groups, groups, and and documents (here, the 2Export Numbers file)

You’ll notice the map.png file isn’t reported as it’s a child of the group (parent) Group 01.

Also note the Inbox is reported though it’s not shown when using Unify inboxes and the Trash is also reported as they’re both top level items.

1 Like