Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions .api-reports/api-report-utilities.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,26 +87,22 @@ type DeepPartialReadonlySet<T> = {} & ReadonlySet<DeepPartial<T>>;
// @public (undocumented)
type DeepPartialSet<T> = {} & Set<DeepPartial<T>>;

// @public (undocumented)
// @public
export class DocumentTransform {
// Warning: (ae-forgotten-export) The symbol "TransformFn" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "DocumentTransformOptions" needs to be exported by the entry point index.d.ts
constructor(transform: TransformFn, options?: DocumentTransformOptions);
// (undocumented)
concat(otherTransform: DocumentTransform): DocumentTransform;
// (undocumented)
static identity(): DocumentTransform;
// @internal @deprecated
readonly left?: DocumentTransform;
resetCache(): void;
// @internal @deprecated
readonly right?: DocumentTransform;
// (undocumented)
static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform & {
left: DocumentTransform;
right: DocumentTransform;
};
// (undocumented)
transformDocument(document: DocumentNode): DocumentNode;
}

Expand Down Expand Up @@ -135,7 +131,7 @@ export interface HKT {
return: unknown;
}

// @public (undocumented)
// @public
export function isFormattedExecutionResult(result?: object): result is FormattedExecutionResult;

// @public
Expand Down
2 changes: 1 addition & 1 deletion .api-reports/api-report-utilities_internal.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ export const getInMemoryCacheMemoryInternals: (() => {
};
}) | undefined;

// @internal @deprecated
// @public
export function getMainDefinition(queryDoc: DocumentNode): OperationDefinitionNode | FragmentDefinitionNode;

// @internal @deprecated (undocumented)
Expand Down
6 changes: 1 addition & 5 deletions .api-reports/api-report.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -948,26 +948,22 @@ type DistributedRequiredExclude<T, U> = T extends any ? Required<T> extends Requ

export { DocumentNode }

// @public (undocumented)
// @public
export class DocumentTransform {
// Warning: (ae-forgotten-export) The symbol "TransformFn" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "DocumentTransformOptions" needs to be exported by the entry point index.d.ts
constructor(transform: TransformFn, options?: DocumentTransformOptions);
// (undocumented)
concat(otherTransform: DocumentTransform): DocumentTransform;
// (undocumented)
static identity(): DocumentTransform;
// @internal @deprecated
readonly left?: DocumentTransform;
resetCache(): void;
// @internal @deprecated
readonly right?: DocumentTransform;
// (undocumented)
static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform & {
left: DocumentTransform;
right: DocumentTransform;
};
// (undocumented)
transformDocument(document: DocumentNode): DocumentNode;
}

Expand Down
11 changes: 11 additions & 0 deletions src/utilities/common/stripTypename.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ import { omitDeep } from "@apollo/client/utilities/internal";
*
* @param value - The object or array that should have `__typename` removed.
* @returns The object with all `__typename` properties removed.
*
* @example
*
* ```ts
* stripTypename({
* __typename: "User",
* id: 1,
* profile: { __typename: "Profile", name: "John Doe" },
* });
* // => { id: 1, profile: { name: "John Doe"}}
* ```
*/
export function stripTypename<T>(value: T) {
return omitDeep(value, "__typename");
Expand Down
99 changes: 94 additions & 5 deletions src/utilities/graphql/DocumentTransform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,23 @@ type TransformFn = (document: DocumentNode) => DocumentNode;

interface DocumentTransformOptions {
/**
* Determines whether to cache the transformed GraphQL document. Caching can speed up repeated calls to the document transform for the same input document. Set to `false` to completely disable caching for the document transform. When disabled, this option takes precedence over the [`getCacheKey`](#getcachekey) option.
* Determines whether to cache the transformed GraphQL document. Caching can
* speed up repeated calls to the document transform for the same input
* document. Set to `false` to completely disable caching for the document
* transform. When disabled, this option takes precedence over the [`getCacheKey`](#getcachekey)
* option.
*
* The default value is `true`.
* @defaultValue `true`
*/
cache?: boolean;
/**
* Defines a custom cache key for a GraphQL document that will determine whether to re-run the document transform when given the same input GraphQL document. Returns an array that defines the cache key. Return `undefined` to disable caching for that GraphQL document.
*
* > **Note:** The items in the array may be any type, but also need to be referentially stable to guarantee a stable cache key.
* > [!NOTE]
* > The items in the array can be any type, but each item needs to be
* > referentially stable to guarantee a stable cache key.
*
* The default implementation of this function returns the `document` as the cache key.
* @defaultValue `(document) => [document]`
*/
getCacheKey?: (
document: DocumentNode
Expand All @@ -35,6 +41,25 @@ function identity(document: DocumentNode) {
return document;
}

/**
* A class for transforming GraphQL documents. See the [Document transforms
* documentation](https://www.apollographql.com/docs/react/data/document-transforms) for more details on using them.
*
* @example
*
* ```ts
* import { DocumentTransform } from "@apollo/client/utilities";
* import { visit } from "graphql";
*
* const documentTransform = new DocumentTransform((doc) => {
* return visit(doc, {
* // ...
* });
* });
*
* const transformedDoc = documentTransform.transformDocument(myDocument);
* ```
*/
export class DocumentTransform {
private readonly transform: TransformFn;
private cached: boolean;
Expand All @@ -52,13 +77,38 @@ export class DocumentTransform {
return [document];
}

/**
* Creates a DocumentTransform that returns the input document unchanged.
*
* @returns The input document
*/
static identity() {
// No need to cache this transform since it just returns the document
// unchanged. This should save a bit of memory that would otherwise be
// needed to populate the `documentCache` of this transform.
return new DocumentTransform(identity, { cache: false });
}

/**
* Creates a DocumentTransform that conditionally applies one of two transforms.
*
* @param predicate - Function that determines which transform to apply
* @param left - Transform to apply when `predicate` returns `true`
* @param right - Transform to apply when `predicate` returns `false`. If not provided, it defaults to `DocumentTransform.identity()`.
* @returns A DocumentTransform that conditionally applies a document transform based on the predicate
*
* @example
*
* ```ts
* import { isQueryOperation } from "@apollo/client/utilities";
*
* const conditionalTransform = DocumentTransform.split(
* (document) => isQueryOperation(document),
* queryTransform,
* mutationTransform
* );
* ```
*/
static split(
predicate: (document: DocumentNode) => boolean,
left: DocumentTransform,
Expand Down Expand Up @@ -91,7 +141,7 @@ export class DocumentTransform {
}

/**
* Resets the internal cache of this transform, if it has one.
* Resets the internal cache of this transform, if it is cached.
*/
resetCache() {
if (this.cached) {
Expand Down Expand Up @@ -121,6 +171,29 @@ export class DocumentTransform {
return this.transform(document);
}

/**
* Transforms a GraphQL document using the configured transform function.
*
* @remarks
*
* Note that `transformDocument` caches the transformed document. Calling
* `transformDocument` again with the already-transformed document will
* immediately return it.
*
* @param document - The GraphQL document to transform
* @returns The transformed document
*
* @example
*
* ```ts
* const document = gql`
* # ...
* `;
*
* const documentTransform = new DocumentTransform(transformFn);
* const transformedDocument = documentTransform.transformDocument(document);
* ```
*/
transformDocument(document: DocumentNode) {
// If a user passes an already transformed result back to this function,
// immediately return it.
Expand All @@ -135,6 +208,22 @@ export class DocumentTransform {
return transformedDocument;
}

/**
* Combines this document transform with another document transform. The
* returned document transform first applies the current document transform,
* then applies the other document transform.
*
* @param otherTransform - The transform to apply after this one
* @returns A new DocumentTransform that applies both transforms in sequence
*
* @example
*
* ```ts
* const combinedTransform = addTypenameTransform.concat(
* removeDirectivesTransform
* );
* ```
*/
concat(otherTransform: DocumentTransform): DocumentTransform {
return Object.assign(
new DocumentTransform(
Expand Down
27 changes: 27 additions & 0 deletions src/utilities/graphql/isFormattedExecutionResult.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
import type { FormattedExecutionResult } from "graphql";

/**
* Determines whether the given object is a valid GraphQL execution result
* according to the GraphQL specification.
*
* @remarks
*
* A valid execution result must be an object that contains only `data`,
* `errors`, and/or `extensions` properties. At least one of `data` or `errors`
* must be present.
*
* @param result - The object to test
* @returns `true` if the object conforms to the GraphQL execution result format
*
* @example
*
* ```ts
* import { isFormattedExecutionResult } from "@apollo/client/utilities";
*
* // Valid execution result
* const validResult = { data: { user: { name: "John" } } };
* console.log(isFormattedExecutionResult(validResult)); // true
*
* // Invalid - contains non-standard properties
* const invalidResult = { data: {}, customField: "value" };
* console.log(isFormattedExecutionResult(invalidResult)); // false
* ```
*/
export function isFormattedExecutionResult(
result?: object
): result is FormattedExecutionResult {
Expand Down
45 changes: 42 additions & 3 deletions src/utilities/graphql/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,21 @@ function isOperation(
* Determine if a document is a mutation document.
*
* @param document - The GraphQL document to check
* @returns A boolean indicating if the document is a mutation operation
*
* @since 3.8.0
* @example
*
* ```ts
* import { isMutationOperation } from "@apollo/client/utilities";
*
* const mutation = gql`
* mutation MyMutation {
* # ...
* }
* `;
*
* isMutationOperation(mutation); // true
* ```

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* ```
* ```
*
* @remark
* If you are authoring a link, you might not need to use this utility.
* Use the `operationType` passed into the link as a property on the `operation` argument instead.

Do you think it makes sense to add this remark to all three functions?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it. Added in 97af276

*/
export function isMutationOperation(document: DocumentNode) {
return isOperation(document, "mutation");
Expand All @@ -23,8 +36,21 @@ export function isMutationOperation(document: DocumentNode) {
* Determine if a document is a query document.
*
* @param document - The GraphQL document to check
* @returns A boolean indicating if the document is a query operation
*
* @example
*
* ```ts
* import { isQueryOperation } from "@apollo/client/utilities";
*
* @since 3.8.0
* const query = gql`
* query MyQuery {
* # ...
* }
* `;
*
* isQueryOperation(query); // true
* ```
*/
export function isQueryOperation(document: DocumentNode) {
return isOperation(document, "query");
Expand All @@ -34,8 +60,21 @@ export function isQueryOperation(document: DocumentNode) {
* Determine if a document is a subscription document.
*
* @param document - The GraphQL document to check
* @returns A boolean indicating if the document is a subscription operation
*
* @example
*
* ```ts
* import { isSubscriptionOperation } from "@apollo/client/utilities";
*
* const subscription = gql`
* subscription MySubscription {
* # ...
* }
* `;
*
* @since 3.8.0
* isSubscriptionOperation(subscription); // true
* ```
*/
export function isSubscriptionOperation(document: DocumentNode) {
return isOperation(document, "subscription");
Expand Down
9 changes: 9 additions & 0 deletions src/utilities/graphql/storeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ export interface Reference {
* Determines if a given object is a reference object.
*
* @param obj - The object to check if its a reference object
*
* @example
*
* ```ts
* import { isReference } from "@apollo/client/utilities";
*
* isReference({ __ref: "User:1" }); // true
* isReference({ __typename: "User", id: 1 }); // false
* ```
*/
export function isReference(obj: any): obj is Reference {
return Boolean(
Expand Down
13 changes: 12 additions & 1 deletion src/utilities/graphql/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,20 @@ const TYPENAME_FIELD: FieldNode = {
};

/**
* Adds `__typename` to all selection sets in the document.
* Adds `__typename` to all selection sets in the document. The operation

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Adds `__typename` to all selection sets in the document. The operation
* Adds `__typename` to all selection sets in the document. Beyond that, the operation

The original sentence confused me a bit - is this what you mean?

* definition's selection set remains unchanged.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* definition's selection set remains unchanged.
* Adds `__typename` to all selection sets in the document.
* Returns a modified copy - the original document is not mutated.

Or did you mean this?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite. I was trying to get at the fact we don't add __typename to the root selection set (perhaps I just use that exact phrase 🤣). Meaning:

const query = gql`
  query {
    __typename # this field is not added by `addTypenameToDocument`
  }
`;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went with this instead: 650a16e

*
* @param doc - The `ASTNode` to add `__typename` to
*
* @example
*
* ```ts
* const document = gql`
* # ...
* `;
*
* const withTypename = addTypenameToDocument(document);
* ```
*/
export const addTypenameToDocument = Object.assign(
function <TNode extends ASTNode>(doc: TNode): TNode {
Expand Down
Loading