diff --git a/website/src/docs/docs.json b/website/src/docs/docs.json index cf25162a607..c230605ad24 100644 --- a/website/src/docs/docs.json +++ b/website/src/docs/docs.json @@ -188,6 +188,10 @@ "path": "entities-and-lookups", "title": "Entities and Lookups" }, + { + "path": "data-requirements-and-mapping", + "title": "Data Requirements and Mapping" + }, { "path": "composition", "title": "Composition" diff --git a/website/src/docs/fusion/v16/attribute-and-directive-reference.md b/website/src/docs/fusion/v16/attribute-and-directive-reference.md index 2a830a79bff..d54eec8ef9d 100644 --- a/website/src/docs/fusion/v16/attribute-and-directive-reference.md +++ b/website/src/docs/fusion/v16/attribute-and-directive-reference.md @@ -6,28 +6,28 @@ Quick reference for all Fusion-related attributes and their GraphQL directive eq # Attribute and Directive Reference Table -| Attribute | Directive | Description | Guide Page | -| --------------------------- | --------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | -| `[ObjectType]` | — | Maps static class as extension to entity type T | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | -| `[QueryType]` | — | Marks class as contributing Query root fields | [Getting Started](/docs/fusion/v16/getting-started) | -| `[MutationType]` | — | Marks class as contributing Mutation root fields | [Getting Started](/docs/fusion/v16/getting-started) | -| `[SubscriptionType]` | — | Marks class as contributing Subscription root fields | [Getting Started](/docs/fusion/v16/getting-started) | -| `[Lookup]` | `@lookup` | Declares field as entity lookup resolver | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | -| `[NodeResolver]` | — | Marks as Relay node resolver | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | -| `[Internal]` | `@internal` | Hides lookup from composed schema | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | -| `[Shareable]` | `@shareable` | Allows multiple subgraphs to resolve field | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | -| `[Parent(requires: "...")]` | — | Declares field requirements from parent | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | -| `[Require("...")]` | `@require` | Declares complex field requirements | [Getting Started](/docs/fusion/v16/getting-started), [Adding a Subgraph](/docs/fusion/v16/adding-a-subgraph) | -| `[EntityKey("...")]` | `@key` | Declares entity key for resolution | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | -| `[BindMember(nameof(...))]` | — | Replaces raw FK with resolved entity | [Adding a Subgraph](/docs/fusion/v16/adding-a-subgraph) | -| `[Tag("...")]` | `@tag` | Applies tag for composition filtering | [Composition](/docs/fusion/v16/composition) | -| `[DataLoader]` | — | Source-generates DataLoader interface | [Getting Started](/docs/fusion/v16/getting-started), [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | -| `[UsePaging]` | — | Enables cursor-based pagination | [Getting Started](/docs/fusion/v16/getting-started) | -| `[ID]` | — | Declares field as Relay-style ID | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | -| `[Inaccessible]` | `@inaccessible` | Hides from composite schema | [Composition](/docs/fusion/v16/composition) | -| `[Override(from: "...")]` | `@override` | Migrates field ownership | [Deployment and CI/CD](/docs/fusion/v16/deployment-and-ci-cd) | -| `[Provides("...")]` | `@provides` | Declares locally-resolvable subfields | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | -| `[External]` | `@external` | Field defined by another subgraph | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | +| Attribute | Directive | Description | Guide Page | +| --------------------------- | --------------- | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `[ObjectType]` | — | Maps static class as extension to entity type T | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | +| `[QueryType]` | — | Marks class as contributing Query root fields | [Getting Started](/docs/fusion/v16/getting-started) | +| `[MutationType]` | — | Marks class as contributing Mutation root fields | [Getting Started](/docs/fusion/v16/getting-started) | +| `[SubscriptionType]` | — | Marks class as contributing Subscription root fields | [Getting Started](/docs/fusion/v16/getting-started) | +| `[Lookup]` | `@lookup` | Declares field as entity lookup resolver | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | +| `[NodeResolver]` | — | Marks as Relay node resolver | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | +| `[Internal]` | `@internal` | Hides lookup from composed schema | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | +| `[Shareable]` | `@shareable` | Allows multiple subgraphs to resolve field | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | +| `[Parent(requires: "...")]` | — | Declares field requirements from parent | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | +| `[Require("...")]` | `@require` | Declares complex field requirements | [Data Requirements and Mapping](/docs/fusion/v16/data-requirements-and-mapping), [Adding a Subgraph](/docs/fusion/v16/adding-a-subgraph) | +| `[EntityKey("...")]` | `@key` | Declares entity key for resolution | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | +| `[BindMember(nameof(...))]` | — | Replaces raw FK with resolved entity | [Adding a Subgraph](/docs/fusion/v16/adding-a-subgraph) | +| `[Tag("...")]` | `@tag` | Applies tag for composition filtering | [Composition](/docs/fusion/v16/composition) | +| `[DataLoader]` | — | Source-generates DataLoader interface | [Getting Started](/docs/fusion/v16/getting-started), [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | +| `[UsePaging]` | — | Enables cursor-based pagination | [Getting Started](/docs/fusion/v16/getting-started) | +| `[ID]` | — | Declares field as Relay-style ID | [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) | +| `[Inaccessible]` | `@inaccessible` | Hides from composite schema | [Composition](/docs/fusion/v16/composition) | +| `[Override(from: "...")]` | `@override` | Migrates field ownership | [Deployment and CI/CD](/docs/fusion/v16/deployment-and-ci-cd) | +| `[Provides("...")]` | `@provides` | Declares locally-resolvable subfields | [Data Requirements and Mapping](/docs/fusion/v16/data-requirements-and-mapping) | +| `[External]` | `@external` | Field defined by another subgraph | [Data Requirements and Mapping](/docs/fusion/v16/data-requirements-and-mapping) | # See Also diff --git a/website/src/docs/fusion/v16/composition.md b/website/src/docs/fusion/v16/composition.md index fd5c33cf067..e5abcaecf97 100644 --- a/website/src/docs/fusion/v16/composition.md +++ b/website/src/docs/fusion/v16/composition.md @@ -19,7 +19,7 @@ Composition is a three-phase process that transforms multiple source schemas int Each source schema is validated in isolation. The composition engine checks: - The schema is valid GraphQL according to the base specification. -- All Fusion directives and attributes are used correctly (e.g., `@external` fields are referenced by `@key` or `@provides`). +- All Fusion directives and attributes are used correctly (e.g., `@external` fields are referenced by `@provides`). - Key fields reference valid scalar or object types. - Lookup fields have the correct argument-to-entity-field mappings. diff --git a/website/src/docs/fusion/v16/data-requirements-and-mapping.md b/website/src/docs/fusion/v16/data-requirements-and-mapping.md new file mode 100644 index 00000000000..18d9d10d1c0 --- /dev/null +++ b/website/src/docs/fusion/v16/data-requirements-and-mapping.md @@ -0,0 +1,393 @@ +--- +title: "Data Requirements and Mapping" +--- + +In traditional distributed systems, dependencies between services hide beneath the surface. A service assumes another service provides certain fields, responds in a certain shape, or is available at a certain time. These assumptions are invisible: they live in code, not in contracts. You discover them when something breaks in production. A field gets renamed, a service changes its response format, or a new team removes data another team depended on without knowing. + +Fusion makes these dependencies **declarative and validated at build time**. When a resolver in one subgraph needs data from another subgraph, it declares that dependency explicitly in the schema using the `@require` directive. The composition step validates that every declared dependency is satisfiable before any code reaches production: the required fields must exist, be reachable, and have compatible types. If a dependency cannot be satisfied, composition fails and tells you exactly what is missing and where. + +This shifts cross-service data dependencies from hidden runtime failures to visible, validated build-time contracts. + +This chapter covers the directives and attributes that make this work: `@require` for declaring data requirements and `@provides` for declaring fields that a subgraph can resolve locally alongside an entity reference. You will learn the full range of patterns and the syntax behind each directive. + +## Declaring Data Dependencies + +Use `@require` on a resolver argument when that argument's value must come from fields owned by other subgraphs. Instead of calling another service yourself, you declare what data you need and the gateway fetches it for you. + +Here is the simplest case: a resolver in the Shipping subgraph needs the product's `weight` to calculate a shipping estimate. The Products subgraph owns `weight`, not the Shipping subgraph. With `@require`, the Shipping subgraph declares this dependency directly in its schema: + +**GraphQL schema** + +```graphql +# Shipping subgraph +type Product { + id: ID! + shippingEstimate(zip: String!, weight: Float! @require(field: "weight")): Int! +} + +type Query { + productById(id: ID!): Product @lookup @internal +} +``` + +The `@require(field: "weight")` directive on the `weight` argument tells the gateway: "Before calling this resolver, fetch `weight` from whichever subgraph can provide it and pass it in as the `weight` argument." + +**C# resolver** + +```csharp +[ObjectType] +public static partial class ProductNode +{ + public static int GetShippingEstimate( + [Parent] Product product, + string zip, + [Require] float weight) + => ShippingCalculator.Estimate(zip, weight); +} +``` + +The `[Require]` attribute maps to `@require(field: "weight")` in the exported schema. Because the C# argument name `weight` matches the entity field name, the field path is inferred automatically. + +When the names differ, provide the field path explicitly: + +```csharp +public static int GetShippingEstimate( + [Parent] Product product, + string zip, + [Require("weight")] float packageWeight) + => ShippingCalculator.Estimate(zip, packageWeight); +``` + +**Public facing composite schema (what clients see)** + +```graphql +type Product { + shippingEstimate(zip: String!): Int! +} +``` + +The `weight` argument is gone. Clients pass only `zip`. The gateway handles the resolution of `weight` transparently. + +### How the Gateway Resolves Requirements + +When a resolver declares a data requirement with `@require`, three things happen during composition and execution: + +1. **Composition** reads the `@require` directive and removes the annotated argument from the composite schema. Clients never see it. +2. **Query planning** detects the dependency. The gateway plans an additional fetch to retrieve the required fields from whichever subgraph can provide it. +3. **Execution** fetches the required data first, then passes it as a resolver argument when invoking the downstream subgraph. + +The resolver receives the data as if it were a normal argument. It does not know or care where the data came from. Services are not coupled to each other. The contract is between a resolver and the data it needs, not between one service and another. + +![The gateway fetches required data from the Products subgraph first, then passes it to the Shipping subgraph as resolved @require arguments](../../shared/fusion/data-requirements-require-flow.png) + +### Complex Requirements with Input Objects + +When a resolver needs multiple fields, you can gather them into a single input object using FieldSelectionMap syntax. This is a clean approach as it puts all the requirements for a resolver into a single argument. + +**GraphQL schema** + +```graphql +# Shipping subgraph +type Product { + id: ID! + deliveryEstimate( + zip: String! + dimension: ProductDimensionInput! + @require( + field: """ + { + weight, + length: dimensions.length, + width: dimensions.width, + height: dimensions.height + } + """ + ) + ): Int! +} + +input ProductDimensionInput { + weight: Float! + length: Float! + width: Float! + height: Float! +} +``` + +The FieldSelectionMap inside `@require` tells the gateway how to populate the `ProductDimensionInput` from fields on the `Product` entity: + +- `weight` maps directly because the input field name matches the entity field name. +- `length: dimensions.length` maps the input field `length` from the nested entity path `dimensions.length`. The same applies to `width` and `height`. + +The fields referenced in the map do not all have to come from the same subgraph. The gateway resolves each field from whichever subgraph owns it. Your resolver declares what data it needs, not which services to call. + +**C# resolver** + +```csharp +[ObjectType] +public static partial class ProductNode +{ + public static int GetDeliveryEstimate( + [Parent] Product product, + string zip, + [Require( + """ + { + weight, + length: dimensions.length, + width: dimensions.width, + height: dimensions.height + } + """)] + ProductDimensionInput dimension) + => ShippingCalculator.Estimate(zip, dimension); +} +``` + +```csharp +public sealed class ProductDimensionInput +{ + public float Weight { get; init; } + public float Length { get; init; } + public float Width { get; init; } + public float Height { get; init; } +} +``` + +**Public facing composite schema (what clients see)** + +```graphql +type Product { + deliveryEstimate(zip: String!): Int! +} +``` + +The `dimension` requirement argument is hidden from clients. Clients see only `zip`; the gateway resolves the nested fields (`weight`, `length`, `width`, `height`) automatically. + +### Multiple Scalar Requirements + +You can annotate multiple arguments with `@require` on the same field. Each one declares an independent data dependency. So, while you can aggregate them into a single input object like explained above you also can spread them across arguments. + +**GraphQL schema** + +```graphql +# Inventory subgraph +type Product { + id: ID! + shippingEstimate( + zip: String! + weight: Float! @require(field: "weight") + price: Float! @require(field: "price") + ): Int! +} +``` + +**C# resolver** + +```csharp +[ObjectType] +public static partial class ProductNode +{ + public static int GetShippingEstimate( + [Parent] Product product, + string zip, + [Require] float weight, + [Require] float price) + => weight > 500 || price > 1000 + ? ExpressShipping.Calculate(weight) + : StandardShipping.Calculate(weight); +} +``` + +### Nested Field Paths + +`@require` can reach into nested objects using dot notation. + +**GraphQL schema** + +```graphql +type Product { + id: ID! + taxEstimate( + countryCode: String! @require(field: "seller.address.countryCode") + price: Float! @require(field: "price") + ): Float! +} +``` + +The gateway traverses `seller.address.countryCode` on the entity and passes the resolved value as the `countryCode` argument. + +### List Aggregation Paths + +When an entity field is a list, you can use bracket notation to select a field from each element. The gateway collects the selected values into a flat list. + +**GraphQL schema** + +```graphql +type Product { + id: ID! + taxEstimate( + countryCodes: [String!]! @require(field: "seller.addresses[countryCode]") + price: Float! @require(field: "price") + ): Float! +} +``` + +The path `seller.addresses[countryCode]` means: navigate to `seller.addresses` (a list), then select `countryCode` from each element. If the seller has three addresses with country codes `"US"`, `"DE"`, and `"US"`, the resolver receives `["US", "DE", "US"]` as the `countryCodes` argument. + +## `@provides`: Declaring Contextually Available Fields + +Use `@provides` on a field that returns an entity to tell the gateway that certain subfields of that entity are available when resolved through this specific field. The subgraph does not own those fields globally, but it can provide them in this context. + +### When `@provides` Helps + +Consider a Reviews subgraph where the `author` field returns a `User` entity. The `User` type and its `username` field are owned by the Accounts subgraph. Normally the gateway would need to call the Accounts subgraph to fetch `username`. But the Reviews subgraph already has the author's username available when resolving `Review.author`. By annotating the `author` field with `@provides(fields: "username")`, the subgraph tells the gateway: "When you resolve `author` through the `Review` entity on my subgraph, I can also give you `username`." + +This is different from `@shareable`, which declares that a subgraph can always resolve a field. `@provides` is conditional: the data is only available when coming through a specific field path. + +**GraphQL schema** + +```graphql +# Reviews subgraph +type Review { + id: ID! + body: String! + author: User @provides(fields: "username") +} + +type User { + id: ID! + username: String! @external +} + +type Query { + reviewById(id: ID!): Review @lookup +} +``` + +The `@provides(fields: "username")` on `author` tells the gateway that when it resolves `author` from the Reviews subgraph, it can also get `username` without a separate call to the Accounts subgraph. + +The `@external` on `username` declares that this field is owned by another subgraph (Accounts), but the Reviews subgraph can provide it in the context of `Review.author`. + +**C# resolver** + +```csharp +[ObjectType] +public static partial class ReviewNode +{ + [Provides("username")] + public static User GetAuthor( + [Parent(requires: nameof(Review.AuthorId))] Review review) + => new User(review.AuthorId, review.AuthorUsername); +} +``` + +### Providing Multiple and Nested Fields + +`@provides` accepts a field selection set, so you can declare multiple fields or nested fields: + +```graphql +type Review { + product: Product @provides(fields: "name price") +} +``` + +```graphql +type Review { + product: Product @provides(fields: "sku variation { size color }") +} +``` + +### When to Use `@provides` + +Use `@provides` when: + +- Your subgraph stores denormalized data from another subgraph (e.g., a cached username) +- Avoiding the extra round-trip measurably improves performance +- The locally stored data is kept in sync with the owning subgraph + +Do not use `@provides` as a substitute for proper entity ownership. If your subgraph is the authoritative source for a field, that field should be defined as a regular field, not as `@external` with `@provides`. Similarly, if your subgraph can resolve a field on every path, use `@shareable` instead of marking it `@external` and adding `@provides` to each field that returns the entity. + +## FieldSelectionMap Syntax Reference + +`@require` uses the FieldSelectionMap scalar for its `field` argument. This is a mini-language for describing how to map entity fields to argument shapes. + +| Syntax | Example | Meaning | +| ---------------- | -------------------------------------- | ---------------------------------------------------------------- | +| Simple path | `"weight"` | Maps the `weight` field directly | +| Nested path | `"dimensions.weight"` | Traverses into `dimensions`, then selects `weight` | +| Object selection | `"{ weight, height }"` | Selects multiple fields into an object shape | +| Mapped selection | `"{ w: dimensions.weight }"` | Renames: maps entity's `dimensions.weight` to argument field `w` | +| Mixed selection | `"{ weight, len: dimensions.length }"` | Combines direct and renamed mappings | +| List aggregation | `"addresses[countryCode]"` | Selects `countryCode` from each element in the `addresses` list | +| List projection | `"dimensions[{ weight, height }]"` | Selects multiple fields from each list element into an object | + +### When to Use Which Syntax + +**Simple path.** Use when `@require` maps one argument to one field. + +```graphql +weight: Float! @require(field: "weight") +``` + +**Object selection.** Use when mapping multiple entity fields into a single input object argument. + +```graphql +dimension: ProductDimensionInput! @require(field: "{ weight, length, width, height }") +``` + +**Mapped selection.** Use when the input field names differ from the entity field names, or when you need to reach into nested fields. + +```graphql +dimension: ProductDimensionInput! @require(field: "{ w: weight, l: dimensions.length }") +``` + +**List aggregation.** Use when you need to collect a single field from each element of a list. + +```graphql +countryCodes: [String!]! @require(field: "seller.addresses[countryCode]") +``` + +**List projection.** Use when you need multiple fields from each element but want to drop the rest. + +```graphql +dims: [ProductDimensionInput!]! @require(field: "dimensions[{ weight, height }]") +``` + +> For the full FieldSelectionMap grammar, see the [Composite Schemas specification](https://graphql.github.io/composite-schemas-spec/draft/#sec-Appendix-A-Specification-of-FieldSelectionMap-Scalar). + +## Troubleshooting + +### `REQUIRE_INVALID_FIELDS`: Referenced field does not exist + +```text +Error: The @require directive on argument "weight" references field "weight" +which does not exist on type "Product". +``` + +The field path in `@require(field: "...")` points to a field that does not exist on the entity type after composition. Check that the field name matches exactly (GraphQL field names, not C# property names) and that the owning subgraph is included in composition. + +### `PROVIDES_INVALID_FIELDS`: Invalid field selection in `@provides` + +The `@provides(fields: "...")` selection references one or more fields that do not exist on the returned entity type. Verify each field path (including nested fields) against the GraphQL schema. + +### `PROVIDES_FIELDS_MISSING_EXTERNAL`: Provided field must be marked `@external` + +A field referenced by `@provides(fields: "...")` must be declared as `@external` in the same subgraph. Mark the provided field (and nested fields, when applicable) as `@external`, or remove it from `@provides` if this subgraph owns it globally. + +### `EXTERNAL_UNUSED`: External field is not referenced + +Every `@external` field must be referenced by a `@provides` directive. Remove unused `@external` declarations or add the corresponding `@provides` selection. + +### Required argument still visible in composite schema + +If a `@require` argument appears in the composite schema when it should not, check that: + +- The `@require` directive is on the argument, not on the field +- The `field` value is a valid FieldSelectionMap (invalid syntax triggers a `REQUIRE_INVALID_SYNTAX` composition error) + +## Next Steps + +- **Need entity identity and lookup patterns?** See [Entities and Lookups](/docs/fusion/v16/entities-and-lookups) for the full guide to keys, public vs. internal lookups, and composite keys. +- **Need field ownership and merging rules?** See [Composition](/docs/fusion/v16/composition) for how `@shareable`, `@inaccessible`, and composition validation work. +- **Need the directive and attribute quick reference?** See the [Attribute and Directive Reference](/docs/fusion/v16/attribute-and-directive-reference). diff --git a/website/src/docs/fusion/v16/entities-and-lookups.md b/website/src/docs/fusion/v16/entities-and-lookups.md index 624de302956..cd4863928ce 100644 --- a/website/src/docs/fusion/v16/entities-and-lookups.md +++ b/website/src/docs/fusion/v16/entities-and-lookups.md @@ -180,7 +180,7 @@ Use an **internal lookup** when: - You do not want clients to enter your subgraph through this lookup - The lookup just constructs a stub. It does not validate the existence of the entity. -For cross-subgraph resolution to work, each subgraph that extends an entity by contributing fields must provide a lookup for that entity. For example, if a subgraph extends `Product` but has no `Product` lookup, transitions into that subgraph are unsatisfiable. +For cross-subgraph resolution to work, a subgraph that contributes fields to an entity must expose a compatible lookup path for that entity. This can be a direct lookup field or a nested lookup under an internal root object. If no lookup path exists, transitions into that subgraph are unsatisfiable. ### Multiple Lookups Per Entity @@ -327,8 +327,8 @@ In most cases, you do not need to declare entity keys explicitly. The compositio Sometimes you need or want to declare the key explicitly. Use the `@key` directive when: -- Your subgraph extends an entity but does not have its own lookup for it - You want to be explicit about which fields form the key on the entity itself +- You want to declare key identity on a type independently of local lookup inference **GraphQL schema** @@ -345,7 +345,9 @@ type Product @key(fields: "id") { public sealed record Product([property: ID] int Id); ``` -The `@key(fields: "id")` directive explicitly declares that `Product` is identified by the `id` field. This is useful in a subgraph that extends `Product` but does not define its own lookup, so composition cannot infer the key from lookup arguments. +The `@key(fields: "id")` directive explicitly declares that `Product` is identified by the `id` field. This is useful when key identity should be declared explicitly instead of inferred from lookup arguments. + +`@key` declares identity. It does not replace lookup paths. If a subgraph contributes fields and needs to be entered during planning, it still needs a compatible lookup route. The `fields` value uses GraphQL field names, not C# member names. @@ -406,6 +408,6 @@ builder ## Next Steps - **Need field ownership rules?** See [Composition](/docs/fusion/v16/composition) for how field ownership, `@shareable`, and composition validation work. -- **Need argument mapping and cross-subgraph dependencies?** The `@is` and `@require` directives are covered in dedicated pages. +- **Need argument mapping and cross-subgraph dependencies?** See [Data Requirements and Mapping](/docs/fusion/v16/data-requirements-and-mapping) for `@is`, `@require`, and FieldSelectionMap patterns. - **Need runtime performance guidance?** See Hot Chocolate docs for DataLoader and batching patterns used inside lookup resolvers. - **Ready to go to production?** See [Authentication and Authorization](/docs/fusion/v16/authentication-and-authorization) for securing your gateway and subgraphs, or [Deployment and CI/CD](/docs/fusion/v16/deployment-and-ci-cd) for setting up independent subgraph deployments. diff --git a/website/src/docs/shared/fusion/data-requirements-require-flow.drawio b/website/src/docs/shared/fusion/data-requirements-require-flow.drawio new file mode 100644 index 00000000000..178aa795bcf --- /dev/null +++ b/website/src/docs/shared/fusion/data-requirements-require-flow.drawio @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website/src/docs/shared/fusion/data-requirements-require-flow.png b/website/src/docs/shared/fusion/data-requirements-require-flow.png new file mode 100644 index 00000000000..9be167125e6 Binary files /dev/null and b/website/src/docs/shared/fusion/data-requirements-require-flow.png differ