Skip to content

Commit

Permalink
Add Documentation for ApplyTransforms, and Add Resources to Each and …
Browse files Browse the repository at this point in the history
…PgSelect Docs (#2163)
  • Loading branch information
benjie authored Sep 18, 2024
2 parents 47098fc + b7ae095 commit 3f9f015
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 3 deletions.
46 changes: 43 additions & 3 deletions grafast/website/grafast/step-library/dataplan-pg/pgSelect.md
Original file line number Diff line number Diff line change
Expand Up @@ -397,10 +397,50 @@ Equivalent to `pgSelectFromRecord(resource, $record).single()`.

`pgSelect` and `pgSelectSingle` are what we call "opaque steps" - that is you
are not intended to use their underlying data directly, instead you use their
methods to extract the data you need to use with other steps.
methods to extract the data you need to use with other steps - for example
`$user.get('username')` to extract the username, or `$user.record()` to turn a
pgSelectSingle step into a step representing a record object.

:::info Opaque step specifics

Currently a `pgSelectSingle` doesn't use the object representation you might
expect, instead it uses a tuple with entries for each of the selected
attributes. The makeup of this tuple will vary depending on which attributes
you requested, and in which order, so you must not rely on its structure. To
get an attribute you should use `$pgSelectSingle.get(attr)` or similar
you requested, and in which order, so you must not rely on its structure; this
approach makes pgSelectSingle very efficient, but it can cause confusion for
people trying to "print out" a user object and just seeing an array containing
some assorted values. Use `$pgSelectSingle.get(attrName)` or
`$pgSelectSingle.record()` to get a step representing a value more suitable for
logging/debugging.

:::

### Transforming the results from a PgSelect step

A PgSelect step represents a collection of opaque tuples; sometimes you may
want to transform these in some way, which you can do with list manipulation
steps such as:

- [`each`](../standard-steps/each.md) - maps over the list and builds a new
representation of each item (e.g. turning a list of users into a list of
usernames: `const $usernames = each($users, $user => $user.get('username'))`)
- [`filter`](../standard-steps/filter.md) - reduces the number of items in the
list by performing filtering in your JavaScript runtime; typically you'd want
to use `$pgSelect.where(...)` instead in order to filter on the database side
for efficiency, but `filter()` can be useful:
`const $admins = filter($users, $user => $user.get('is_admin'))`
- [`first`](../standard-steps/first.md) / [`last`](../standard-steps/last.md) -
get the first/last entry from the list:
`const $firstUser = $users.row(first($users));`
- [`groupBy`](../standard-steps/groupBy.md) - group the records into a map
containing sub-lists keyed by a shared value, for example "posts by author":
`const $postsByUser = groupBy($posts, $post => $post.get('author_id'))`

Note that if you perform this kind of transformation, it does not always take
place immediately - sometimes the transforms are only applied when the step is
paginated over. If you then use this step as a dependency of another step, you
may get the raw (untransformed) values, causing confusion and bugs. To solve
this, for now, you should use
[`applyTransforms`](../standard-steps/applyTransforms.md) to force the
transform to take place at the current level, such that depending on the
transformed values is safe.
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# applyTransforms

Takes a step as the first argument and returns a step that guarantees all
`listItem` transforms (especially `each()`) have been applied.

## When to use

It is rare that you need `applyTransforms()`; it is designed for use when
another step needs to depend on the full transformed value of the step.
Normally this kind of dependency wouldn't exist - you'd return your
(transformed) list step from your plan resolver, and Grafast would handle the
transforms when iterating over the resulting list.

An example of where you might need `applyTransforms()` is transforming a list
of users into usernames to send to a remote service:

```ts
// This step still represents a list of user objects until it is paginated by
// GraphQL; so if you pass it to another step as a dependency, that step will
// receive the untransformed user objects.
const $untransformed = each($users, ($user) => $user.get("username"));

// This step forces the `listItem` transforms to take place, so now it truly
// represents a list of usernames and is safe to pass as a dependency to other
// steps.
const $usernames = applyTransforms($untransformed);

return performRemoteRequestWithUsernames($usernames);
```

You should **not** use `applyTransforms()` when returning a list step from a
plan resolver for a list field. Grafast will automatically apply the transforms
when it iterates over the list, to `applyTransforms()` beforehand would force
this iteration to execute twice, which is inefficient.

```ts
const typeDefs = gql`
type Organization {
usernames: [String!]
}
`;
const plans = {
Organization: {
usernames($org) {
const $users = users.find({ organization_id: $org.get("id") });
// No need to transform here:
return each($users, ($user) => $user.get("username"));
},
},
};
```

## Type

```ts
function applyTransforms($step: ExecutableStep): ExecutableStep;
```

## Example

Imagine you want to generate a greeting string for all of the users
in a particular organization. Your first try might be something like:

```ts
// ❗ COUNTER-EXAMPLE!
const $users = usersResource.find();
$users.where(sql`${$users}.organization_name = 'Graphile'`);
const $usernames = each($users, ($user) => $user.get("username"));
return lambda(
// UNSAFE! $usernames has not been transformed yet, it still represents the
// same collection as $users.
$usernames,
(usernames) => `Hello ${usernames.join(", ")}!`,
true,
);
```

This will output confusing data, since `$usernames` was not actually transformed yet
(it would only be transformed if we walked over it via a GraphQL list field) - you
might end up with `Hello [object Object], [object Object]!` or similar.

Instead, we must use `applyTransforms()` to force the tranforms to be applied
before passing the step as a dependency to `lambda()`:

```ts
const $users = usersResource.find();
$users.where(sql`${$users}.organization_name = 'Graphile'`);
const $untransformed = each($users, ($user) => $user.get("username"));

// Force the `listItem` transforms to be applied, so `lambda` can depend on the
// transformed values.
const $usernames = applyTransforms($untransformed);

return lambda(
$usernames,
(usernames) => `Hello ${usernames.join(", ")}!`,
true,
);
```
28 changes: 28 additions & 0 deletions grafast/website/grafast/step-library/standard-steps/each.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,31 @@ Usage:
```ts
const $newList = each($oldList, ($listItem) => doSomethingWith($listItem));
```

## Example generating a list of objects

Sometimes you have a step representing a collection of resources, `$list`, and
you need to build a list of derivative objects from them. For example, the
items in your collection might have `x` and `y` properties, and you might want
to turn them into `lng` and `lat` attributes; which might look like this:

```ts
const $derivatives = each($list, ($item) =>
object({
name: $item.get("name"),
lng: $item.get("x"),
lat: $item.get("y"),
}),
);
return $derivatives;
```

:::warning Remember: `applyTransforms()` if passing to another step

If you aren't returning the result of `each()` from a plan resolver, but are
instead feeding it into another step, you will likely need to perform
`applyTransforms()` to force the list transforms to take place before the
dependent step receives the resulting values. Read more in
[`applyTransforms()`](./applyTransforms.md).

:::

0 comments on commit 3f9f015

Please sign in to comment.