Is there a programmatic way to get UUIDs of subgroups

I have a Keyboard Maestro Macro that gets the UUIDs of all top-level groups in all open databases, which I then massage to create a dictionary in KBM that is called by various Stream Deck triggers. The relevant part of the KM macro is the following AppleScript that was cribbed from this forum (I will amend this post when I figure out who to thank/credit):

set masterList to {}
tell application id "DNtp"
	set allDatabases to every database
	repeat with thisDatabase in allDatabases
		set dbName to name of thisDatabase
		set allGroups to (every record of thisDatabase whose type is group)
		set groupList to {}
		repeat with thisGroup in allGroups
			set groupName to name of thisGroup
			set groupUuid to uuid of thisGroup as string
			if groupName is not in {"Trash", "Tags"} then
				set tempList to "{ \"group\" : \"" & groupName & "\" , \"uuid\" : \"" & groupUuid & "\" }"
				if groupList = {} then
					set groupList to tempList
				else
					set groupList to groupList & "," & tempList
				end if
			end if
		end repeat
		if groupList ≠ {} then
			if masterList = {} then
				set masterList to groupList
			else
				set masterList to masterList & "," & groupList
			end if
		end if
	end repeat
	set masterList to "[" & masterList & "]"
end tell
set saveTID to AppleScript's text item delimiters
set AppleScript's text item delimiters to {","}
set masterList to masterList as string
set AppleScript's text item delimiters to saveTID
return masterList

I would love to be able to extend this to obtain the UUIDs of subgroups, even if meant doing so on a database by database basis. That is, I only have fewer than a dozen databases that I need to put in the KM dictionary and my group names are fairly static, so if I run the above script to get top level groups and need to run another script or scripts to go database by database (or worst case, group by group) that would be fine for my purposes.

Groups are parents in a database.

Are you trying to output JSON data?

I’m not doing what you’re doing re: Keyboard Maestro but here’s a simple example…

tell application id "DNtp"
	set groupList to {}
	repeat with thisRecord in (parents of (current database))
		set recLoc to (location of thisRecord)
		if (recLoc does not start with "/Tags") or (recLoc does not start with "/Trash") then
			copy ("{" & linefeed & "\"name\": \"" & (name of thisRecord) & "\", " & linefeed & "\"uuid\": \"" & (reference URL of thisRecord) & "\"" & linefeed & "}" & linefeed) to end of groupList
		end if
	end repeat
end tell
return groupList as string

I dusted off some parenthesis and came up with this implementation in JXA – seems a bit more natural to use JavaScript if the desired output is JSON :wink:
Disclaimer: This code is not tested at all.

const app = Application("DEVONthink 3");
const groupList = [];
app.currentDatabase().parents().forEach(r => {
  if (! /^\(Tags|Trash)/.test(r.location())) {
     groupList.push({name: r.name(), uuid: r.referenceURL()});
  }
})
JSON.stringify(groupList);
1 Like

Thank you both @Bluefrog and @chrillek - your suggestions each do exactly what I needed to be done. @chrillek - for the benefit of anyone looking at this in the future, I kept getting an “unmatched parentheses” error in the Javascript and, after counting the parentheses about 20 times and seeing the same number of opening parens and closing ones, I realized the closing parenthesis after “Trash” needed to be escaped. Did that and then all worked a treat.
Thanks again to both of you!!

Jim-
In case you or anyone else is using Keyboard Maestro and a StreamDeck and wants to know what I’m doing with this JSON data, in a nutshell I:

  1. Massage the JSON in BBEdit to get it in a form where the group name is the key and the UUID is the value and then import it into a KBM dictionary
  2. I have a KBM action (Execute an AppleScript) that, given a DEVONthink UUID, takes a file(s) selected in PathFinder and imports them into the DEVONthink group denoted by the UUID.
  3. On the StreamDeck, I have buttons that call a main KBM macro (in which the above-mentioned AppleScript action is run) via the KMLink plugin, which allows a parameter. For the parameter, I put the name of the DEVOnthink group I want to send the file to.
  4. The main KBM macro takes the parameter (which is a “TriggerValue” in KBM terms"),
    looks it (the “key”) up in the KBM dictionary created in step 1 and returns the UUID (the “value”) to a variable. That variable is placed in the AppleScript referred to Step 2, and voila!

The benefit of this is that:

  1. In KBM, I only need one KBM macro instead of a macro for each destination UUID
  2. On the StreamDeck, I can create one button using the KMLink plugin and duplicate it dozens of times, and only have to change the parameter on each new button to the name of the desired DT group. If I had to use multiple KBM macros, I’d have to create a button for each from scratch instead of being able to simply duplicate buttons.

This is all less complicated than it might sound and every step stands on the shoulders of scripts I got either in this forum or the KBM forum, which are hands down the two most helpful and substantive forums ever.

1 Like

Actually, the one before does not handed too be escaped. So it’s /^(Trash/Tags)/
Sorry for the confusion.

Why not simply generate the JSON exactly like that in the first place, like
groupList.push({`"${r.name()}"`:r.referenceURL()});

1 Like

Interesting for sure and if it scratches your itch, by all means, proceed!

What massaging needed to be done - removing the backslashes?

Why not? Because I am not as smart as you!! :grinning:
As I mentioned, all of my stuff is built off of scripts I’ve cribbed from those who really know how to script, and wherever I got the one I’m using left me with the “cruft” I need to massage out.
And to further demonstrate my ignorance, when I replace the

     groupList.push({name: r.name(), uuid: r.referenceURL()});

in the original script you provided with your latest suggestion, I get an error

Error on line 6: SyntaxError: Unexpected token '`'. Expected a property name

and that stops me in my tracks, because I have no idea what to do next.

Yes, the backslashes, and the “x-devonthink-itme://” come out. Also, and this probably just represents my ignorance about JSON and dictionaries, I need it so that the keys aren’t “name” and “uuid” , each of which has a value, but so that the group name is itself the key, and the UUID is the value. So my JSON looks like this:

{“LaunchBar”: “F70343FC-A59D-4F6E-95D7-8FF6BB718CD9”,
“Mail Apps”: “43F5FFD5-A286-4D92-8DEC-D094C338A691”,
“Markdown”: “C565B3D7-49BE-4022-9756-CF2E38E27290”,
“Mindmapping”: “54E68EA2-C570-4A3E-AC8C-403E0BE60CA8”,
etc.}

Thanks, but the more I write here, the less I’m conviced of my smartness.

const propName = r.name();
groupList[propName] = r.referenceURL().replace(/x-devonthink-item:\/\//,"");

might work better. As perhaps might

const propName = r.name();
groupList.push{[propName]: r.referenceURL()r.eplace(/x-devonthink-item:\/\//,""));

But the first variant should really be ok, I think. Both actually remove the DT protocol from the URL, as that seems to be what you want.

In any case, there’s really no point in creating an arbitrary JavaScript array in JSON notation only to use an editor (of all tools) to mogrify this array into the form you want.

I hate to be a pain, but I don’t know enough to know where to insert your new suggestions in the original script. Could you show the new versions in their entirety? Here’s what I tried, but it produces a whole lot of “Nulls”:

const app = Application("DEVONthink 3");
const groupList = [];
app.currentDatabase().parents().forEach(r => {
  if (! /^\(Tags|Trash\)/.test(r.location())) {
		const propName = r.name();
		groupList[propName] = r.referenceURL().replace(/x-devonthink-item:\/\//,"");	
	}
})
JSON.stringify(groupList);

I am motivated to learn Javascript and have a couple of books to begin learning, but what you are doing is beyond where I have gotten to.

Amen to that. I have actually graduated from using BBEdit workflows to using an app called Easy Data Transform, which performs all the necessary steps at the click of a button. But I am always looking to simplify, and so would love to be able to use your approach.

Since I do not like to dish out recipes, explanation first, starting at the back.

You want a JSON representation of an array. The elements of the array are simple objects with the group name as their sole property and its UUID as the value. Like

[ { "group 1": "uuid1"}, {"group 2": "uuid"}...]

Once you have this array, you can simply convert it to a string using JSON.stringify.

Now the code (which is completely untested, as I said before!) for each record

  • gets its name as r.name()
  • and stores it in propName
  • Then it creates a new object with the property propNameand the value of the URL.

Apparently, this does not really work yet. Also, apparently, because I just scribbled something down without really thinking about it. So here’s something that I did test:

const app = Application("DEVONthink 3");
const groupList = [];
app.currentDatabase().parents().forEach(r => {
   if (! /^\(Tags|Trash\)/.test(r.location())) {
		const propName = r.name();
		const URL = r.referenceURL();
		groupList.push({[propName]: URL.replace(/x-devonthink-item:\/\//,"")});
	}
})
JSON.stringify(groupList);

The relevant part is {[propName]: URL.replace(/x-devonthink-item:\/\//,"")}. It builds an object ({}) with a property using the current value of propName. The square brackets around it are needed so that the value of propName is used – otherwise, there’d be only a single property named ‘propName’ . The value of this property is, as before, the refereceURL with the protocol removed, so just the UUID.

Now that we have this object, we simply push it at the end of the array groupList. After we’ve looped over all the parents in currentDatabase, this array contains one object for each group, consisting of the group’s name as its key and the UUID as its value.

Beware, however, that as it stands, the code will also find all smart groups. If you do not want that, you’ll have to weed them out in the if statement by adding a check for r.type().

2 Likes

Thanks on two fronts. One, for the script, which works great. Two, and more importantly, for taking the time to go beyond dishing out recipes and giving an explanation. I find I learn more about scripting this way (i.e. in real use cases) than reading books, and I will now be able to study what you’ve done and learn from it.
Thanks again.

1 Like

I’m wondering two things about this:

  • is it possible that “/Trash” is a localized string that for example in a German environment might be called “Papierkorb”?
  • why do I not see that in any of my databases?

I.e. if I run

app.currentDatabase.parents().forEach(r => {console.log(r.location())});

I never see “/Trash” nor “/Papierkorb” in the output (though I do see “/Tags”). Also, I get some locations several times, possibly once for every subgroup they contain?

I’m probably blabbering here, but what exactly does currentDatabase.parents() contain? In my case, it seems to be “/Tags” for every tag in this database and all the groups (including the database root) that contain one or more subgroups. But I never see the subgroups themselves. Is that expected behavior?

The scripting dictionary says that parent is “A parent group of a record”. How does that translate to currentDatabase.parents()?