Custom MetaData "Can't Convert Types" Using JXA

I have a custom metadata field with identifier attorneyemail1 and type single-field text

When I select records and execute this script I get an error as below. How do I correct the type error?

(()=> {
  const app=Application("DEVONthink 3");
  const md = app.selectedRecords.customMetaData.attorneyemail1();
  return md.join("\n");
})()

What is returned by your chained method call? Turning on the script editor’s message pane should provide some ideas.

Edit: try using the get custom metadata method instead of accessing the data directly.

The result is blank.

This occurs even if I select only one record and I know that custom metadata field is not blank.

The events and replies tab are more useful here.

Also, what is customMetadata() of a single record returning?

Events:

Replies:

image

But the field is not blank:

And the identifier is correct:

See my last posting. I can’t check the scripting dictionary right now, but I think that customMetadata() might be an object. In that case, customMetadata().attorney… might work.

The dictionary says it is a property of a record

If I change the script the error changes

That is not the advocated form to retrieve specific custom metadata. That returns a record of any applied metadata.

You need to use getCustomMetaData with for: and from:.

But I’m also not the JXA guru so I can’t give you the implementation. However…

This is the expected return, just as you’d see similar via AppleScript.

Well that inspired me to try this - which does not yield an error but does yield null instead of the correct response. More interestingly I get the same null response (but no error) even if I include the name of a non-existent custom-metadata field.

customMetadata is an AppleScript record, which translates into a JavaScript Object. To access its properties, you have to deference it first and then address the property. So
customMetadata() yields the object, and customMetadara().attorney… should get you the metadata field.

Mind you: that’s how it would work for a single record. But you’re trying to get the metadata of all selected records, which would be an array (list in AppleScript). Chaining
selectedRecords.customMetadata will return an array of objects (or rather an object specifier of an array).
selectedRecords.customMetadata() will give you the array of objects. But you can’t access a property attorney… of this array since it’s a property of each individual array element.
So
selectedRecords.customMetadata()[0].attorney… might work.
But you can’t forego a loop over the array elements in this case.

OK Thank you for that explaination - if I can get this working for array element [0] as the first of the selected records then I can sort out how to loop over the rest of the selected records… but it still gives me a null for even array element [0]

Then that might be one of the cases where JXA is just not cutting it and you’d have to use the getCustomMetadata method on the records.

OK I will work on that - thank you for the efforts

Update… This pemutation gives me the output of [object Object] once for each selected record

How do I separate the object into the individual custom metadata items?

First off, I’m trying that with my very limited custom meta data, which is only one (“Betrag”, the amount in € for an invoice). Now, I select two records in DT and run a minimal script:

Stupidly simple (and as I supposed before):
app.selectedRecords.customMetadata() returns a JavaScript Array whose elements are simple objects of the form
{ key1: value1, key2: value2, ...}
What you apparently want is to return a string containing the values for one custom meta data key separated by newlines.

(() => {
  const app = Application("DEVONthink 3");
  return app.selectedRecords.customMetaData().map(cmd => cmd[key]).join('\n');
})()

Looks ok to me. As you can see, you have to prepend md to the name of the custom meta data field and use only lower case letters. I seem to remember that that’s mentioned somewhere in the documentation. And put the key you’re using in quotes (single/double/backquotes, whatever you fancy).

TL;DR
I’ll try to explain what this horrible one-liner does.
app.selectedRecords returns an object specifier to the currently selected records. Think of it as “something like an array of records, but not quite”. Not quite an array, because you can append customMetaData to it to get another object specifier. This time, think of it as “something like an array of custom meta data, but not quite”. The parenthesis in
app.selectedRecords.customMetaData() give you a real array of customMetadata.

Now, looking that up in the scripting dictionary, you see

customMetaData (*Record*) : User-defined metadata of a record as a dictionary containing key-value pairs.

which is unfortunately quite confusing, since this “Record” is not at all the same thing as a DT record. It’s simply a Record in AppleScript lingo, which translates into an object in JavaScript. The record’s keys are the object’s attributes/properties, and the values are the same for both languages.

So now we’re dealing with an array of objects, and we want the values for a particular attribute/property of these objects. That’s what the map method above is doing: for every element of the array, it returns the value of it’s “key” property. Since map builds a new array from these values, you can append join("\n") to it which finally gives you what you want.

What you did before was something like
app.selectedRecords.customMetadata().join('\n')
effectively joining the string representation of the custom meta data objects with newlines. Since JavaScript can’t translate an arbitrary object into a string, you get “[object Object]\n[object Object]”. If you’d turned on the “Replies” tab, you’d probably have seen what customMetaData() really is.

1 Like

@chrillek You retain your well-deserved title as Emperor of Javascript! Many thanks - that works perfectly - and your explanation is quite helpful.

One question - how did you know to reference the array elements in your Map method using cmd ? Where would that be documented for me to look it up?

You are correct that the Replies tab gives more information and would have clarified the above somewhat.

I did make one tweak to the script because as written your function failed if there were any null elements i.e. if any selected record had no entry for the specified custom metadata field. So I added a filter to the function and it works great.

Final output is as below. Again huge help - not only for this particular example but as a teaching example for how to generalize in using JXA.

(() => {
  const app = Application("DEVONthink 3");
  app.includeStandardAdditions = true;
  return app.selectedRecords.customMetaData().filter(obj=>obj).map(cmd => cmd['mdattorneyemail1']).join('\n');
})()

And for those who use TextExpander, it works well as a Snippet which inserts the desired custom metadata content for the selected records:

That’s completely arbitrary. I chose cmd for “custom metadata”, but you could use x, thisOtherName or something else entirely. It’s simply a placeholder for the current element of the array.

BTW: it’s not necessary to include the standard additions in this script. I had it in a former version and forgot to remove it before I took the screenshot.

2 Likes