diff --git a/grafast/website/grafast/step-library/dataplan-pg/registry/codecs.md b/grafast/website/grafast/step-library/dataplan-pg/registry/codecs.md index b8572a5339..8f08d14c02 100644 --- a/grafast/website/grafast/step-library/dataplan-pg/registry/codecs.md +++ b/grafast/website/grafast/step-library/dataplan-pg/registry/codecs.md @@ -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 diff --git a/grafast/website/grafast/step-library/standard-steps/loadMany.md b/grafast/website/grafast/step-library/standard-steps/loadMany.md index 79f49ff095..a5f606eaa8 100644 --- a/grafast/website/grafast/step-library/standard-steps/loadMany.md +++ b/grafast/website/grafast/step-library/standard-steps/loadMany.md @@ -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 @@ -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: @@ -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} @@ -154,14 +154,14 @@ function callback( attributes: ReadonlyArray; params: Record; }, -): PromiseOrDirect>; +): PromiseOrDirect>>; ``` :::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. ::: @@ -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 @@ -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 @@ -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 diff --git a/grafast/website/grafast/step-library/standard-steps/loadOne.md b/grafast/website/grafast/step-library/standard-steps/loadOne.md index 706f2034ca..5be859a736 100644 --- a/grafast/website/grafast/step-library/standard-steps/loadOne.md +++ b/grafast/website/grafast/step-library/standard-steps/loadOne.md @@ -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: @@ -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, + options: { + unary: unknown; + attributes: ReadonlyArray; + params: Record; + }, +): PromiseOrDirect>; +``` :::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('')` + - `params`: the params set via `$record.setParam('', )` -```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: @@ -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"); @@ -212,11 +238,7 @@ 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. @@ -224,8 +246,6 @@ 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 @@ -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: @@ -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