# Sending a Localized Newsletter, User by User
This guide will make use of the PQL to satisfy an extensive functionality in a single query.
Let's create an automated email-sending service to distribute blog posts. All the data comes from 3 separate sources:
- A REST API endpoint returning the subscribers (pairs of
email
andlanguage
fields) to the newsletter, provided by Mailchimp. - A REST API endpoint with all your customer information, including their
email
andname
fields, provided by company's CRM. - The blog posts published in your website.
The email needs to be customized for each person:
- Greeting them personally by name
- Translating the content of the email to their own language. Using Google Translate is acceptable.
# Steps to follow
Our service will follow the following steps:
- Fetch the list of newsletter subscribers using the first API endpoint, getting their
email
andlanguage
fields - For every recipient in this list add the
name
field, fetching it from the second API endpoint by using the commonemail
field as ID.
By then, we will have the all the user data consolidated in a single list, containing fields name
, email
and language
. Let's continue.
- Retrieve the data for the specified blog post:
content
anddate
- Calculate all the different languages that the post needs be translated to
- Translate the post to each language
By then, in addition to the user data, we will also have the post data, translated to all required languages. Next, we need to craft the customized email content for each user.
- Iterate the list of users, and for each add a new field
emailContent
containing: a. The greeting message "Hi {name}, this is our our blog post from {date}", translated to the user's language b. The blog post content that had been translated to the user's language
By then, we have all the data: Rows of email
and emailContent
fields. We will then be able to send the email.
- Iterate the list, and send the email
# Final query
This query does the magic:
post(by:{id:$postId})@post.
content|
dateStr(d/m/Y)@date,
getJSON("https://newapi.getpop.org/wp-json/newsletter/v1/subscriptions")@userList|
arrayUnique(
extract(
getSelfProp(%{self}%, userList),
lang
)
)@userLangs|
extract(
getSelfProp(%{self}%, userList),
email
)@userEmails|
arrayFill(
getJSON(
sprintf(
"https://newapi.getpop.org/users/api/rest/?query=name|email%26emails[]=%s",
[arrayJoin(
getSelfProp(%{self}%, userEmails),
"%26emails[]="
)]
)
),
getSelfProp(%{self}%, userList),
email
)@userData;
post(by:{id:$postId})@post<
copyRelationalResults(
[content, date],
[postContent, postDate]
)
>;
getSelfProp(%{self}%, postContent)@postContent<
translateMultiple(
from: en,
to: arrayDiff([
getSelfProp(%{self}%, userLangs),
[en]
])
),
renameProperty(postContent-en)
>|
getSelfProp(%{self}%, userData)@userPostData<
forEach<
applyFunction(
function: arrayAddItem(
array: [],
value: ""
),
addArguments: [
key: postContent,
array: %{value}%,
value: getSelfProp(
%{self}%,
sprintf(
postContent-%s,
[extract(%{value}%, lang)]
)
)
]
),
applyFunction(
function: arrayAddItem(
array: [],
value: ""
),
addArguments: [
key: header,
array: %{value}%,
value: sprintf(
string: "<p>Hi %s, we published this post on %s, enjoy!</p>",
values: [
extract(%{value}%, name),
getSelfProp(%{self}%, postDate)
]
)
]
)
>
>;
getSelfProp(%{self}%, userPostData)@translatedUserPostProps<
forEach(
if: not(
equals(
extract(%{value}%, lang),
en
)
)
)<
advancePointerInArrayOrObject(
path: header,
appendExpressions: {
toLang: extract(%{value}%, lang)
}
)<
translateMultiple(
from: en,
to: %{toLang}%,
oneLanguagePerField: true,
override: true
)
>
>
>;
getSelfProp(%{self}%,translatedUserPostProps)@emails<
forEach<
applyFunction(
function: arrayAddItem(
array: [],
value: []
),
addArguments: [
key: content,
array: %{value}%,
value: concat([
extract(%{value}%, header),
extract(%{value}%, postContent)
])
]
),
applyFunction(
function: arrayAddItem(
array: [],
value: []
),
addArguments: [
key: to,
array: %{value}%,
value: extract(%{value}%, email)
]
),
applyFunction(
function: arrayAddItem(
array: [],
value: []
),
addArguments: [
key: subject,
array: %{value}%,
value: "PoP API example :)"
]
),
sendByEmail
>
>
Click here to view the query results.
In the rest of this guide, we will learn how this query came to be.
# Fetching the blog post
We can query field posts
to find the latest published blog post:
posts(
pagination: {
limit:1
}
sort: {
by: DATE
order: DESC
}
).
id|
title|
url
TIP
Use ,
to separate field arguments, each of them in key:value
format
Use .
to fetch nested properties from the object
Use |
to fetch several fields from an object
This query retrieves an array of posts. To operate with a single post, we can better use field post
, which receives the ID by argument:
post(
by: { id:1 }
).
id|
title|
url
Fields argument names are optional. The query above is similar to the one below, which skips fieldArg name "by"
:
post({id:1}).
id|
title|
url
We can pass the ID through a variable, which is resolved through a URL parameter under the variable name. For the query below, we add param postId=1
to the URL:
post(by:{id:$postId}).
id|
title|
url
TIP
Use $
to define a variable
Finally, we add an alias to make the response more compact:
post(by:{id:$postId})@post.
id|
title|
url
TIP
Use @
to define an alias
# Fetching the blog post (again)
The previous queries were demonstrating how to fetch data for the post. Now that we know, let's fetch the data needed for our use case: the content
and date
fields:
post(by:{id:$postId})@post.
content|
dateStr(d/m/Y)@date
TIP
Use [...]
to define an array and ,
to separate its items. The format for each item is either key:value
or value
(making the key numeric)
# Fetching the list of newsletter subscribers
To fetch the list of newsletter subscribers from a REST endpoint, we can use field getJSON
and specify the URL:
getJSON("https://newapi.getpop.org/wp-json/newsletter/v1/subscriptions")@userList
TIP
At any time, you can review the documentation for the fields/directives employed in this guide, by querying the fullSchema.
# Calculating the list of unique languages
The previous list contains pairs of email
and lang
fields. Next, we calculate the list of unique languages, as to translate the blog post to all those languages. This task will be composed of two steps.
First, we extract the field lang
from the array through field extract
(which takes an array and a path):
getJSON("https://newapi.getpop.org/wp-json/newsletter/v1/subscriptions")@userList|
extract(
getSelfProp(%{self}%, userList),
lang
)
TIP
Expression %{self}%
contains an object which has a pointer to all data retrieved for the current object. Accessed through function getSelfProp
, it enables to access this data, under the property name or alias under which it was stored.
Then, we apply operator arrayUnique
, and assign the results under alias userLangs
:
getJSON("https://newapi.getpop.org/wp-json/newsletter/v1/subscriptions")@userList|
arrayUnique(
extract(
getSelfProp(%{self}%, userList),
lang
)
)@userLangs
# Retrieving the rest of the user information
So far, we have a list of pairs of email
and lang
fields stored under property userList
. Next, using email
as the common identifier for the data, we query the REST endpoint from the CRM to fetch the remaining user information: the name
field. This task is composed of several steps.
First, we extract the list of all emails from userList
, and place them under userEmails
:
extract(
getSelfProp(%{self}%, userList),
email
)@userEmails
Our CRM exposes a REST endpoint which allows to filter users by email, like this:
/users/api/rest/?emails[]=email1&emails[]=email2&...
Then, we must generate the endpoint URL by converting the array of emails into a string with the right format, and then executing getJSON
on this URL. Let's do that.
To generate the URL, we use a combination of sprintf
and arrayJoin
:
sprintf(
"https://newapi.getpop.org/users/api/rest/?query=name|email%26emails[]=%s",
[arrayJoin(
getSelfProp(%{self}%, userEmails),
"%26emails[]="
)]
)
TIP
The string can't have character "&"
in it, or it will create trouble when appending it in the URL param. Instead, we must use its code "%26"
TIP
The PoP API (over which GraphQL by PoP is based) can also satisfy REST, through the REST API package. That's the case with the REST endpoint used in this guide.
Having generated the URL, we execute getJSON
on it:
getJSON(
sprintf(
"https://newapi.getpop.org/users/api/rest/?query=name|email%26emails[]=%s",
[arrayJoin(
getSelfProp(%{self}%,userEmails),
"%26emails[]="
)]
)
)
Finally, we must combine the 2 lists into one, generating a new list containing all user fields: name
, email
and lang
. To achieve this, we use function arrayFill
, which, given 2 arrays, returns an array containing the entries from each of them where the index (in this case, property email
) is the same, and we save the results under property userData
:
arrayFill(
getJSON(
sprintf(
"https://newapi.getpop.org/users/api/rest/?query=name|email%26emails[]=%s",
[arrayJoin(
getSelfProp(%{self}%, userEmails),
"%26emails[]="
)]
)
),
getSelfProp(%{self}%, userList),
email
)@userData
# Translating the post content to all different languages
By now we have collected the post data, saved under properties content
and date
, and all the user data, saved under property userData
. It is time to mix these pieces of data together, for which we need to have all data at the same level. However, if we pay attention to the latest query, we can notice that they are under 2 different paths:
userData
is under/
(root)content
anddate
are under/post/
Hence, we must either move userData
down to the post
level, or move content
and date
up to the root level. Due to GraphQL by PoP's dataloading engine architecture, only the latter option is feasible. Let's see why.
# Understanding how GraphQL by PoP loads data
When resolving the query to load data, the dataloader processes all elements from a same entity all at the same time, as to load all their data in a single query and completely avoiding the N+1 problem. Then, let's imagine that we have the following query:
posts.
title|
author.
name
Let's say this query returns 10 posts and, for each post, it retrieves its author, and some authors have 2 or more posts, so that the query retrieves 10 posts but only 4 unique authors. The dataloading mechanism will first process all 10 posts, fetching all their required data (properties title
and author
), and then it will fetch all data for all 4 authors (property name
).
If posts with IDs 1
and 2
both have author with ID 5
, and we copy a property downwards in the graph, post 1
will first copy its properties down to author 5
, then immediately post 2
will copy its own properties down to author 5
, overriding the properties set by post 1
. By the time the dataloading mechanism reaches the users level, author 5
will only have the data from posts 2
. This situation could be avoided by copying the properties the post ID in the user object, as to not override previous values. However, while the post entity knows that it is loading data for its author, the author entity doesn't know who loaded it (the graph direction is only top-to-bottom). Hence, the post entiy can fetch properties from the author entity and store them under theauthor ID (which the post knows about), but the other way around doesn't work.
That's why we can only copy properties upwards. In this case, the post's content
and date
properties must be copied upwards, to the root.
We can now go back to the query.
# Translating the post content to all different languages (again)
To copy the content
and date
properties upwards to the root level, we use directive <copyRelationalResults>
. This directive is applied on the root
entity, and it receives these inputs:
- The name of the relational property, in this case
post(by:{id:$postId})@post
- The name of the properties to copy, in this case
content
anddate
- (Optional) The name under which to copy the properties, in this case
postContent
andpostDate
self.
post(by:{id:$postId})@post<
copyRelationalResults(
[content, date],
[postContent, postDate]
)
>
[View query results: GraphQL output, PoP native output]
That this works is not evident at all. Moreover, you need to click on link PoP native output to see the results, and appreciate that the data was indeed copied one level up. The other link, GraphQL output, would seem to not work... it also does, but the results are not being output!
I'll explain why this is so: how data is loaded (once again) and how directives work.
# What are directives and how do they work
Directives are sheer power: They can affect execution of the query in any desired way. They are as close to the bare metal of the dataloading engine as possible. They have access to all previously loaded data and can modify it, remove it, etc.
Directives help regulate the lifecycle of loading data in the API, by validating and resolving the fields on the objects and adding these results on a directory with all results from all objects, from which the graph is drawn.
The dataloading engine relies on the following special directives to implement core functionality:
<setSelfAsExpression>
, which defines the "expression"%{self}%
which allows to retrieve previously loaded data<validate>
, which validates that the provided data matches against its definition on the schema and, if it doesn't, removes it and shows a error message<resolveValueAndMerge>
: it resolves all the fields in the query and merges their response into the final database object
These 3 directives are executed at the beginning of their own slots:
- Front:
<setSelfAsExpression>
- Middle:
<validate>
- Back:
<resolveValueAndMerge>
Every directive we create must indicate in which from these 3 slots it must be placed and executed. For instance, directives <skip>
and <include>
(mandatory ones in GraphQL) must be placed in the "Middle"
slot, that is after fields are validated but before resolved; directive <copyRelationalProperties>
must be placed in the "Back"
slot, since it requires the data to be resolved before it can copy it somewhere else.
# Revisiting how GraphQL by PoP loads data
Let's examine the query above together with the previous query bit that loads properties content
and date
:
post(by:{id:$postId})@post.
content|
dateStr(d/m/Y)@date,
self.
post(by:{id:$postId})@post<
copyRelationalResults(
[content, date],
[postContent, postDate]
)
>
[View query results: PoP native output]
We can see that these 2 queries are separated using ,
instead of |
, and that there is an entity self
after which we repeat the same field post(by:{id:$postId})@post
, and only then we apply directive <copyRelationalResults>
. Why is this so?
Field self
is an identity field: It returns the same object currently being operated on. In this case, it returns once again the entity root
. (Doing self
returns the root
object, doing post.self
returns the same post
object, doing post.author.self
returns the user
object, and so on.)
As mentioned before, the dataloader loads data in stages, in which all data for a same type of entity (all posts, all users, etc) is fetched all together. Using ,
to separate a query makes it start iterating from the root all over again. Then, when processing this query...
post,
self.post
...the entites being handled by the dataloader are these ones, in this exact order: root
(the first one, always), posts
(loaded by query posts
, before the ,
), root
(the first one again, after ,
), root
again (loaded by doing self
on the root
object) and then posts
again (by doing self.post
).
As can be seen, the self
field then enables to go back to an already loaded object, and keep loading properties on it. As such, it allows to delay loading certain data until a later iteration of the dataloader, to make sure a certain condition is satisfied.
That is exactly why we need it: Directive <copyRelationalResults>
copies a property one level up, but it is applied on the root
object and, by the time it is executed, the properties to copy must exist on the post
object. Hence the iteration: root
loads the post
, the post
loads its properties, then back to root
copies the properties from the post
to itself.
# Concerning the native data structure used in PoP (hint: it's not a graph!)
We saw in the query above...
self.
post(by:{id:$postId})@post<
copyRelationalResults(
[content, date],
[postContent, postDate]
)
>
... that we need to view the query results in PoP native output to see that the directive <copyRelationalResults>
worked, and that the GraphQL output doesn't mirror the changes. What is going on?
First of all: the PoP API does NOT use a graph to represent the data model. Instead, it uses components.
However (and this is the fact that makes the magic happen) a graph does naturally arise from the relationships among the database entities defined through components. Hence, the graph can be easily generated from the component-based architecture of the API, and the GraphQL implementation is simply an application among many. For instance, if replacing the /graphql
bit in the URL with /rest
, we obtain the equivalent REST endpoint (as demonstrated for the REST API endpoint to fetch the user data); if we replace it with /xml
, we access the data in XML format (example).
The real, underlying data structure in PoP is simply a set of relationships across database objects, which matches directly with how an SQL database works: Tables containing rows of data entries, and relationships among entities defined through IDs. That is exactly what you see when you remove the /graphql
bit from the URL, from any URL (example). That's the PoP native format. Looking at is like looking at the code in the matrix.
Finally, we can provide an explanation of why the query results in PoP native output for directive <copyRelationalResults>
are shown, but not in the GraphQL output: The PoP native format displays all the data it has accumulated, thereby there it is. The GraphQL format, though, doesn't show it because the properties under which the data are copied to, postContent
and postDate
, are not being queried. If we do (adding 2 levels of self
to make sure we query the data after it has been copied), the data then does appear in the response:
self.
post(by:{id:$postId})@post<
copyRelationalResults(
[content, date],
[postContent, postDate]
)
>,
self.
self.
getSelfProp(%{self}%, postContent)|
getSelfProp(%{self}%, postDate)
TIP
In the response for the GraphQL query above, properties appear under path /self.self
, and not directly under /
. However, they are the same entity root
(self
returns itself, on whichever object it is applied to). This is, once again, easier to visualize in the PoP native format, removing the /graphql
bit from the URL
# Translating the post content to all different languages (again x2)
We have by now properties postContent
, postDate
and userData
all loaded at the root level, which is all the information we need to work with. From now on, being at the root level we can execute all the operators and directives necessary to accomplish our goals.
Next, we apply directive <translate>
on postContent
, which will call the Google Translate API to translate the text. Why is it a directive, instead of an operator?
# Differences between directives and operators
When coding a query, it may be sometimes unclear what is better, if to use an operator or use a directive. After all, the two of them can both execute functionality (such as sending an email, or translating a piece of text). For instance, we could do either posts.translate(title)
(operator) or posts.title<translate>
(directive). So, when to use one or the other?
When executing functionality, the main difference between these 2 is the following:
An operator is a field. A field computes a value from a single object; every field is executed independently of each other field, and it is executed once per object. For instance, for the following query...
post.
title
... the field title
is executed once on each post object. If there are 10 posts, then title
is executed 10 times, once in each. And they see no history: given a set of inputs, they just return their output. They don't have really a lot of logic, or complexity.
Since operators are fields, we have the same situation: For the following query...
post.
sprintf(
"Post title is",
[title()]
)
... the sprintf
operator is executed once in each title
property, which is executed 10 times, once per post, all independently from each other, and oblivious of each other.
Directives work in a different way: They are executed just once on the set of affected objects, and on the set of affected properties for each object, and they can modify the value of these properties for each of the objects. For instance, for the following query:
posts.
title<
applyFunction(...)
>|
content<
applyFunction(...)
>
... the directive <applyFunction>
will be executed only once (even if it appears twice in the query, once for each field), receiving a set of posts and properties title
and content
for each post.
Hence, we must use directives when:
- It is more efficient to batch execute operations. For instance, a
<sendByEmail>
directive sending 10 emails at once is more effective than asendByEmail()
operator sending 10 emails independently, and making 10 SMTP connections; a<translate>
directive can make a single call to the translation API to translate all strings at once, which is more efficient than callingtranslate()
on 10 strings which will make 10 calls to the translation API. - We need low-level functionality, such as: modifying or deleting previous data, copying data to another object, iterating through a series of properties to apply a function to each of them, etc.
# Translating the post content to all different languages (again x3)
Now we know why we are doing content<translate>
instead of translate(content)
. Let's continue.
The following query takes care of translating the post content to all the different unique languages gathered earlier on from the user data:
self.
self.
getSelfProp(%{self}%, postContent)@postContent<
translateMultiple(
from:en,
to:arrayDiff([
getSelfProp(%{self}%, userLangs),
[en]
])
)
>
[View query results: GraphQL output (changes not yet visible), PoP native output (changes already there)]
We can see that the <translate>
directive takes 2 inputs through directive arguments: the from
language (English) and the to
language or array of languages. Since we want to translate to many languages, we provide this list, but first removing English from the list (through operator arrayDiff
). Otherwise, the Google Translate API throws an error when attempting to translate from English to English.
The <translate>
directive did not override the original property on the object, but instead created additional ones which append the language code. Hence, by now, we have the following entries with the post content: postContent
(original in English), postContent-es
(Spanish), postContent-fr
(French) and postContent-de
(German). To homogenize it, we rename property postContent
to postContent-en
through directive <renameProperty>
:
self.
self.
getSelfProp(%{self}%,postContent)@postContent<
translateMultiple(
from:en,
to:arrayDiff([
getSelfProp(%{self}%, userLangs),
[en]
])
),
renameProperty(postContent-en)
>
[View query results: GraphQL output (changes not yet visible), PoP native output (changes already there)]
TIP
When applying more than 1 directive to the same affected objects and fields, we can simply concatenate them with ,
in the order in which they will be executed, as in <translateMultiple(...), renameProperty(...)>
.
However, because a directive is executed on its selected slot from among "Front"
, "Middle"
and "Back"
, only the order within the slot will always be respected. It may be that defining <directive1, directive2>
will have <directive2>
execute before than <directive1>
if its slot is executed earlier.
# Selecting the corresponding translation for each user
By now, we have translated the post content to all different unique languages. Next, let's add the corresponding translation for each user, creating a new property userPostData
.
To achieve this, we will make use of directive <forEach>
which iterates over an array, and passes each array item to its composed directive <applyFunction>
through expression %{value}%
. This directive then executes function arrayAddItem
on each item, which adds an element (the translated post content) to an array (the user data). In order to deduce the selected language, it uses functions extract
to get the lang
property from the user data array, then injects it into sprintf
to generate the corresponding postContent-languagecode
property, which is then retrieved from the current object (the root) and placed under property postContent
on the array. All field arguments needed by function arrayAddItem
are injected by the directive <applyFunction>
on runtime through the array defined in argument addArguments
.
self.
self.
getSelfProp(%{self}%, userData)@userPostData<
forEach<
applyFunction(
function: arrayAddItem(
array: [],
value: ""
),
addArguments: [
key: postContent,
array: %{value}%,
value: getSelfProp(
%{self}%,
sprintf(
postContent-%s,
[extract(%{value}%,lang)]
)
)
]
)
>
TIP
Function arrayAddItem
must initially define field arguments array
and value
, even if initialized with empty values. This must be done because these arguments are set as mandatory in the schema definition, so if they are not present, it is considered a schema validation error and this section of the query is ignored.
# Adding a greeting message, and translating it to the user's language
Let's next deal with the greeting message, which must be translated to the user's language. Initially the message is a placeholder, and we customize it through the user name
field and the post date
field. Only then we can do the translation, as to help Google Translate do a better job at it (translating "Hi Leo!" should produce better results than translating "Hi %s!")
We first add the message into the array containing all other user data under property header
, and already customizing it with the user data. The logic is similar as in the previous query, for which we also use directive <applyFunction>
, which can be executed within the same iteration of the previous <forEach>
directive:
self.
self.
getSelfProp(%{self}%, userData)@userPostData<
forEach<
applyFunction(
function: arrayAddItem(
array: [],
value: ""
),
addArguments: [
key: header,
array: %{value}%,
value: sprintf(
string: "<p>Hi %s, we published this post on %s, enjoy!</p>",
values: [
extract(%{value}%, name),
getSelfProp(%{self}%, postDate)
]
)
]
)
>
>
Finally, we translate the message to the user's language. To do this, we use directive <forEach>
to iterate on all array items whose lang
field is "en"
(for English), since we don't want to translate those. This is accomplished through the filter condition passed through argument if
. Then, each array item is passed to the composed directive <advancePointerInArrayOrObject>
, which can navigate the inner structure of the array and position itself on the property that needs be translated: header
.
Finally the element is passed to the next composed directive, <translate>
, which receives a string of arrays to translate as its affected fields, and an array of languages to translate to passed through expression toLang
(which we create on-the-fly just for this purpose of communicating data across directives), and by setting argument oneLanguagePerField
to true
and override
to true
the directive knows to match each element on these 2 arrays to do the translation and place the result back on the original property.
self.
self.
self.
getSelfProp(%{self}%, userPostData)@translatedUserPostProps<
forEach(
if:not(equals(extract(%{value}%,lang),en))
)<
advancePointerInArrayOrObject(
path: header,
appendExpressions: {
toLang:extract(%{value}%,lang)
}
)<
translateMultiple(
from: en,
to: %{toLang}%,
oneLanguagePerField: true,
override: true
)
>
>
>
# Generating and sending the email
We are almost there! All that there is left to do is to generate the content for all the emails to send: arrays containing properties content
, to
and subject
, and then this array is passed to directive <sendByEmail>
which, voilà, does what it must do! (Or actually not: Since we don't want spam, the email sending is actually disabled... We just print the email data instead for this guide)
self.
self.
self.
self.
getSelfProp(%{self}%,translatedUserPostProps)@emails<
forEach<
applyFunction(
function: arrayAddItem(
array: [],
value: []
),
addArguments: [
key: content,
array: %{value}%,
value: concat([
extract(%{value}%,header),
extract(%{value}%,postContent)
])
]
),
applyFunction(
function: arrayAddItem(
array: [],
value: []
),
addArguments: [
key: to,
array: %{value}%,
value: extract(%{value}%,email)
]
),
applyFunction(
function: arrayAddItem(
array: [],
value: []
),
addArguments: [
key: subject,
array: %{value}%,
value: "PoP API example :)"
]
),
sendByEmail
>
>
# The final query!
We have everything we need! Let's get it all together into the one, final, complete query:
post(by:{id:$postId})@post.
content|
dateStr(d/m/Y)@date,
getJSON("https://newapi.getpop.org/wp-json/newsletter/v1/subscriptions")@userList|
arrayUnique(
extract(
getSelfProp(%{self}%, userList),
lang
)
)@userLangs|
extract(
getSelfProp(%{self}%, userList),
email
)@userEmails|
arrayFill(
getJSON(
sprintf(
"https://newapi.getpop.org/users/api/rest/?query=name|email%26emails[]=%s",
[arrayJoin(
getSelfProp(%{self}%, userEmails),
"%26emails[]="
)]
)
),
getSelfProp(%{self}%, userList),
email
)@userData,
self.
post(by:{id:$postId})@post<
copyRelationalResults(
[content, date],
[postContent, postDate]
)
>|
self.
getSelfProp(%{self}%, postContent)@postContent<
translateMultiple(
from: en,
to: arrayDiff([
getSelfProp(%{self}%, userLangs),
[en]
])
),
renameProperty(postContent-en)
>|
getSelfProp(%{self}%, userData)@userPostData<
forEach<
applyFunction(
function: arrayAddItem(
array: [],
value: ""
),
addArguments: [
key: postContent,
array: %{value}%,
value: getSelfProp(
%{self}%,
sprintf(
postContent-%s,
[extract(%{value}%, lang)]
)
)
]
),
applyFunction(
function: arrayAddItem(
array: [],
value: ""
),
addArguments: [
key: header,
array: %{value}%,
value: sprintf(
string: "<p>Hi %s, we published this post on %s, enjoy!</p>",
values: [
extract(%{value}%, name),
getSelfProp(%{self}%, postDate)
]
)
]
)
>
>|
self.
getSelfProp(%{self}%, userPostData)@translatedUserPostProps<
forEach(
if: not(
equals(
extract(%{value}%, lang),
en
)
)
)<
advancePointerInArrayOrObject(
path: header,
appendExpressions: {
toLang: extract(%{value}%, lang)
}
)<
translateMultiple(
from: en,
to: %{toLang}%,
oneLanguagePerField: true,
override: true
)
>
>
>|
self.
getSelfProp(%{self}%,translatedUserPostProps)@emails<
forEach<
applyFunction(
function: arrayAddItem(
array: [],
value: []
),
addArguments: [
key: content,
array: %{value}%,
value: concat([
extract(%{value}%, header),
extract(%{value}%, postContent)
])
]
),
applyFunction(
function: arrayAddItem(
array: [],
value: []
),
addArguments: [
key: to,
array: %{value}%,
value: extract(%{value}%, email)
]
),
applyFunction(
function: arrayAddItem(
array: [],
value: []
),
addArguments: [
key: subject,
array: %{value}%,
value: "PoP API example :)"
]
),
sendByEmail
>
>
We are done now! Use case accomplished!
# Simplified query
If using ;
to split the fields instead of ,
, the server already takes care of placing all the self
fields on the executable query.
When doing field1,field2,field3
we do not know in which order these fields will be resolved. But doing field1;field2;field3
, the order is guaranteed to be field1 => field2 => field3
, so we don't need to manually add all the self
fields anymore.
The query above is equivalent to this simplified query:
post(by:{id:$postId})@post.
content|
dateStr(d/m/Y)@date,
getJSON("https://newapi.getpop.org/wp-json/newsletter/v1/subscriptions")@userList|
arrayUnique(
extract(
getSelfProp(%{self}%, userList),
lang
)
)@userLangs|
extract(
getSelfProp(%{self}%, userList),
email
)@userEmails|
arrayFill(
getJSON(
sprintf(
"https://newapi.getpop.org/users/api/rest/?query=name|email%26emails[]=%s",
[arrayJoin(
getSelfProp(%{self}%, userEmails),
"%26emails[]="
)]
)
),
getSelfProp(%{self}%, userList),
email
)@userData;
post(by:{id:$postId})@post<
copyRelationalResults(
[content, date],
[postContent, postDate]
)
>;
getSelfProp(%{self}%, postContent)@postContent<
translateMultiple(
from: en,
to: arrayDiff([
getSelfProp(%{self}%, userLangs),
[en]
])
),
renameProperty(postContent-en)
>|
getSelfProp(%{self}%, userData)@userPostData<
forEach<
applyFunction(
function: arrayAddItem(
array: [],
value: ""
),
addArguments: [
key: postContent,
array: %{value}%,
value: getSelfProp(
%{self}%,
sprintf(
postContent-%s,
[extract(%{value}%, lang)]
)
)
]
),
applyFunction(
function: arrayAddItem(
array: [],
value: ""
),
addArguments: [
key: header,
array: %{value}%,
value: sprintf(
string: "<p>Hi %s, we published this post on %s, enjoy!</p>",
values: [
extract(%{value}%, name),
getSelfProp(%{self}%, postDate)
]
)
]
)
>
>;
getSelfProp(%{self}%, userPostData)@translatedUserPostProps<
forEach(
if: not(
equals(
extract(%{value}%, lang),
en
)
)
)<
advancePointerInArrayOrObject(
path: header,
appendExpressions: {
toLang: extract(%{value}%, lang)
}
)<
translateMultiple(
from: en,
to: %{toLang}%,
oneLanguagePerField: true,
override: true
)
>
>
>;
getSelfProp(%{self}%,translatedUserPostProps)@emails<
forEach<
applyFunction(
function: arrayAddItem(
array: [],
value: []
),
addArguments: [
key: content,
array: %{value}%,
value: concat([
extract(%{value}%, header),
extract(%{value}%, postContent)
])
]
),
applyFunction(
function: arrayAddItem(
array: [],
value: []
),
addArguments: [
key: to,
array: %{value}%,
value: extract(%{value}%, email)
]
),
applyFunction(
function: arrayAddItem(
array: [],
value: []
),
addArguments: [
key: subject,
array: %{value}%,
value: "PoP API example :)"
]
),
sendByEmail
>
>