Skip to content

Commit

Permalink
Merge branch 'main' into behavior-overhaul
Browse files Browse the repository at this point in the history
  • Loading branch information
benjie authored Sep 26, 2024
2 parents 822085b + 6c08a20 commit 34fcfbe
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,30 @@ const forumCodec = recordCodec({

- `identifier` - the database name for this type

### Example

For example, in this hypothetical E-commerce scenario, `listOfCodec` is used
in combination with the `$pgSelect.placeholder()` method to return a SQL
expression that allows the transformed list of `$orderIds` to be referenced
inside the step for selecting the associated order items.

```ts
const $orders = orders.find({
customer_id: context().get("customerId"),
});

const $orderIds = applyTransforms(each($orders, ($order) => $order.get("id")));

const $orderItems = registry.pgResources.order_items.find();

$orderItems.where(
sql`${$orderItems}.order_id = ANY (${$orderItems.placeholder(
$orderIds,
listOfCodec(TYPES.uuid),
)})`,
);
```

## rangeOfCodec(innerCodec, name, identifier)

`rangeOfCodec` returns a new codec that represents a range of the given
Expand Down
28 changes: 16 additions & 12 deletions grafast/website/grafast/step-library/standard-steps/loadMany.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ read many results from your business logic layer. To load just one, see

## Enhancements over DataLoader

Thanks to the planning system in Gra*fast*, `loadOne` can expose features that
Thanks to the planning system in Gra*fast*, `loadMany` can expose features that
are not possible in DataLoader.

### Attribute and parameter tracking
Expand Down Expand Up @@ -66,7 +66,7 @@ const plans = {
};
```

In it's current state the system doesn't know that the
In its current state the system doesn't know that the
`$user.get("organization_id")` is equivalent to the `id` argument to our
`usersByOrganizationId` field, so this would result in a chained fetch:

Expand All @@ -80,7 +80,7 @@ stateDiagram
`} />

However, we can indicate that the output of the `loadMany` step's records'
`organization_id` property (`$user.get("organization_id")`) is equivalent to it's input
`organization_id` property (`$user.get("organization_id")`) is equivalent to its input
(`$id`):

```diff {4-5}
Expand Down Expand Up @@ -154,14 +154,14 @@ function callback(
attributes: ReadonlyArray<string>;
params: Record<string, unknown>;
},
): PromiseOrDirect<ReadonlyArray<unknown>>;
): PromiseOrDirect<ReadonlyArray<ReadonlyArray<unknown>>>;
```

:::tip

For optimal results, we strongly recommend that the callback function is defined
in a common location so that it can be reused over and over again, rather than
defined inline. This will allow LoadManyStep to optimise calls to this function.
defined inline. This will allow the underlying steps to optimize calls to this function.

:::

Expand All @@ -176,7 +176,7 @@ Within this definition of `callback`:

`specs` is deduplicated using strict equality; so it is best to keep `$spec`
simple - typically it should only represent a single scalar value - which is
why `$unaryStep` exists...
why `$unaryStep` exists.

`options.unary` is very useful to keep specs simple (so that fetch
deduplication can work optimally) whilst passing in global values that you may
Expand All @@ -203,17 +203,19 @@ An example of the callback function might be:
```ts
const friendshipsByUserIdCallback = (ids, { attributes }) => {
// Your business logic would be called here; e.g. this might be the same
// function that your DataLoaders would call, except we can pass additional
// information to it:
// function that your DataLoaders would call, except additional information
// can be passed to it:
return getFriendshipsByUserIds(ids, { attributes });
};
```

[dataloader]: https://github.com/graphql/dataloader

### Unary step usage

(a step that only ever represents one value, e.g. simple derivatives of `context()`, `fieldArgs`, or `constant()`)
:::info

A unary step is a step that only ever represents one value, e.g. simple derivatives of `context()`, `fieldArgs`, or `constant()`.

:::

In addition to the forms seen in "Basic usage" above, you can pass a second
step to `loadMany`. This second step must be a [**unary
Expand Down Expand Up @@ -302,8 +304,10 @@ values from these plans: `ReadonlyArray<[a: AValue, b: BValue, c: CValue]>`.
:::tip Performance impact from using list/object

Using `list()` / `object()` like this will likely reduce the effectiveness of
loadMany's built in deduplication; to address this a stable object/list is
`loadMany`'s built in deduplication; to address this a stable object/list is
required - please track this issue:
https://github.com/graphile/crystal/issues/2170

:::

[dataloader]: https://github.com/graphql/dataloader
192 changes: 133 additions & 59 deletions grafast/website/grafast/step-library/standard-steps/loadOne.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const plans = {
};
```

In it's current state the system doesn't know that the `$user.get("id")` is
In its current state the system doesn't know that the `$user.get("id")` is
equivalent to the `context().get("userId")`, so this would result in a chained
fetch:

Expand Down Expand Up @@ -114,75 +114,89 @@ stateDiagram

## Usage

### Basic usage

```ts
const $userId = $post.get("author_id");
const $user = loadOne($userId, batchGetUserById);
// OR: const $user = loadOne($userId, 'id', batchGetUserById);
```
loadOne($spec, [$unaryStep,] [ioEquivalence,] callback)
```

`loadOne` accepts two to four arguments. The first is the step that specifies
which records to load (the _specifier step_), the last is the callback function called with these
specs responsible for loading them.

The callback function is called with two arguments, the first is a list of the
values from the _specifier step_ and the second is options that may affect the
fetching of the records.
```ts
// Basic usage:
const $record = loadOne($spec, callback);

// Advanced usage:
const $record = loadOne($spec, $unaryStep, ioEquivalence, callback);
const $record = loadOne($spec, $unaryStep, callback);
const $record = loadOne($spec, ioEquivalence, callback);
```

Where:

- `$spec` is any step
- `$unaryStep` is any _unary_ step - see [Unary step usage](#unary-step-usage) below
- `ioEquivalence` is either `null`, a string, an array of strings, or a string-string object map - see [ioEquivalence usage](#ioequivalence-usage) below
- and `callback` is a callback function responsible for fetching the data.

### Callback

The `callback` function is called with two arguments, the first is
a list of the values from the _specifier step_ `$spec` and the second is options that
may affect the fetching of the records.

```ts
function callback(
specs: ReadonlyArray<unknown>,
options: {
unary: unknown;
attributes: ReadonlyArray<string>;
params: Record<string, unknown>;
},
): PromiseOrDirect<ReadonlyArray<unknown>>;
```

:::tip

For optimal results, we strongly recommend that the callback function is defined
in a common location so that it can be reused over and over again, rather than
defined inline. This will allow LoadOneStep to optimise calls to this function.
defined inline. This will allow the underlying steps to optimize calls to this function.

:::

Optionally a penultimate argument (2nd of 3 arguments, or 3rd of 4 arguments)
can indicate the input/output equivalence - this can be:
Within this definition of `callback`:

- `null` to indicate no input/output equivalence
- a string to indicate that the same named property on the output is equivalent
to the entire input plan
- if the step is a `list()` (or similar) plan, an array containing a list of
keys (or null for no relation) on the output that are equivalent to the same
entry in the input
- if the step is a `object()` (or similar) plan, an object that maps between
the attributes of the object and the key(s) in the output that are equivalent
to the given entry on the input
- `specs` is the runtime values of each value that `$spec` represented
- `options` is an object containing:
- `unary`: the runtime value that `$unaryStep` (if any) represented
- `attributes`: the list of keys that have been accessed via
`$record.get('<key>')`
- `params`: the params set via `$record.setParam('<key>', <value>)`

```ts title="Example for a list step"
const $member = loadOne(
list([$organizationId, $userId]),
["organization_id", "user_id"],
batchGetMemberByOrganizationIdAndUserId,
);
`specs` is deduplicated using strict equality; so it is best to keep `$spec`
simple - typically it should only represent a single scalar value - which is
why `$unaryStep` exists.

// - batchGetMemberByOrganizationIdAndUserId will be called with a list of
// 2-tuples, the first value in each tuple being the organizationId and the
// second the userId.
// - Due to the io equivalence (2nd argument):
// - `$member.get("organization_id")` will return `$organizationId` directly
// - `$member.get("user_id")` will return `$userId` directly
```
`options.unary` is very useful to keep specs simple (so that fetch
deduplication can work optimally) whilst passing in global values that you may
need such as a database or API client.

```ts title="Example for an object step"
const $member = loadOne(
object({ oid: $organizationId, uid: $userId }),
{ oid: "organization_id", uid: "user_id" },
batchGetMemberByOrganizationIdAndUserId,
);
`options.attributes` is useful for optimizing your fetch - e.g. if the user
only ever requested `$record.get('id')` and `$record.get('avatarUrl')` then
there's no need to fetch all the other attributes from your datasource.

// - batchGetMemberByOrganizationIdAndUserId will be called with a list of
// objects; each object will have the key `oid` set to an organization id,
// and the key `uid` set to the user ID.
// - Due to the io equivalence (2nd argument):
// - `$member.get("organization_id")` will return the step used for `oid`
// (i.e. `$organizationId`) directly
// - Similarly `$member.get("user_id")` will return `$userId` directly
```
`options.params` can be used to pass additional context to your callback
function, perhaps options like "should we include archived records" or "should
we expand 'customer' into a full object rather than just returning the
identifier".

#### Example callback
### Basic usage

```ts
const $userId = $post.get("author_id");
const $user = loadOne($userId, batchGetUserById);
// OR: const $user = loadOne($userId, 'id', batchGetUserById);
```

An example of the callback function might be:

Expand All @@ -203,7 +217,19 @@ async function batchGetUserById(ids, { attributes }) {
}
```

### Advanced usage
### Unary step usage

:::info

A unary step is a step that only ever represents one value, e.g. simple derivatives of `context()`, `fieldArgs`, or `constant()`.

:::

In addition to the forms seen in "Basic usage" above, you can pass a second
step to `loadOne`. This second step must be a [**unary
step**](../../step-classes.md#addUnaryDependency), meaning that it must represent
exactly one value across the entire request (not a batch of values like most
steps).

```ts
const $userId = $post.get("author_id");
Expand All @@ -212,20 +238,14 @@ const $user = loadOne($userId, $dbClient, "id", batchGetUserFromDbById);
// OR: const $user = loadOne($userId, $dbClient, batchGetUserFromDbById);
```

In addition to the forms seen in "Basic usage" above, you can pass a second
step to `loadOne`. This second step must be a [**unary
step**](../../step-classes.md#addUnaryDependency), meaning that it must represent
exactly one value across the entire request (not a batch of values like most
steps). Since we know it will have exactly one value, we can pass it into the
Since we know it will have exactly one value, we can pass it into the
callback as a single value and our callback will be able to use it directly
without having to perform any manual grouping.

This unary dependency is useful for fixed values (for example, those from
GraphQL field arguments) and values on the GraphQL context such as clients to
various APIs and other data sources.

#### Example callback (advanced)

An example of the callback function might be:

```ts
Expand All @@ -241,7 +261,52 @@ async function batchGetUserFromDbById(ids, { attributes, unary }) {
}
```

## Multiple steps
### ioEquivalence usage

The `ioEquivalence` optional parameter can accept the following values:

- `null` to indicate no input/output equivalence
- a string to indicate that the same named property on the output is equivalent
to the entire input plan
- if the step is a `list()` (or similar) plan, an array containing a list of
keys (or null for no relation) on the output that are equivalent to the same
entry in the input
- if the step is a `object()` (or similar) plan, an object that maps between
the attributes of the object and the key(s) in the output that are equivalent
to the given entry on the input

```ts title="Example for a list step"
const $member = loadOne(
list([$organizationId, $userId]),
["organization_id", "user_id"],
batchGetMemberByOrganizationIdAndUserId,
);

// - batchGetMemberByOrganizationIdAndUserId will be called with a list of
// 2-tuples, the first value in each tuple being the organizationId and the
// second the userId.
// - Due to the io equivalence (2nd argument):
// - `$member.get("organization_id")` will return `$organizationId` directly
// - `$member.get("user_id")` will return `$userId` directly
```

```ts title="Example for an object step"
const $member = loadOne(
object({ oid: $organizationId, uid: $userId }),
{ oid: "organization_id", uid: "user_id" },
batchGetMemberByOrganizationIdAndUserId,
);

// - batchGetMemberByOrganizationIdAndUserId will be called with a list of
// objects; each object will have the key `oid` set to an organization id,
// and the key `uid` set to the user ID.
// - Due to the io equivalence (2nd argument):
// - `$member.get("organization_id")` will return the step used for `oid`
// (i.e. `$organizationId`) directly
// - Similarly `$member.get("user_id")` will return `$userId` directly
```

### Passing multiple steps

The [`list()`](./list) or [`object()`](./object) step can be used if you need
to pass the value of more than one step into your callback:
Expand Down Expand Up @@ -275,4 +340,13 @@ async function getLast4FromStripeIfAdmin(tuples) {

This technique can also be used with the unary step in advanced usage.

:::tip Performance impact from using list/object

Using `list()` / `object()` like this will likely reduce the effectiveness of
`loadOne`'s built in deduplication; to address this a stable object/list is
required - please track this issue:
https://github.com/graphile/crystal/issues/2170

:::

[dataloader]: https://github.com/graphql/dataloader

0 comments on commit 34fcfbe

Please sign in to comment.