Skip to content

Update Validation function templates #623

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Changes from all 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
Original file line number Diff line number Diff line change
@@ -13,6 +13,11 @@ Scale the Functions resource limits based on the field's length.
"""
directive @scaleLimits(rate: Float!) on FIELD_DEFINITION

"""
Requires that exactly one field must be supplied and that field must not be `null`.
"""
directive @oneOf on INPUT_OBJECT

"""
A custom property. Attributes are used to store additional information about a Shopify resource, such as
products, customers, or orders. Attributes are stored as key-value pairs.
@@ -352,6 +357,27 @@ type CartLineCost {
totalAmount: MoneyV2!
}

"""
The fetch target result. Your Function must return this data structure when generating the request.
"""
input CartValidationsGenerateFetchResult {
"""
The attributes associated with an HTTP request.
"""
request: HttpRequest
}

"""
The output of the Function run target. The object contains the validation errors
that display to customers and prevent them from proceeding through checkout.
"""
input CartValidationsGenerateRunResult {
"""
The ordered list of operations to apply.
"""
operations: [Operation!]!
}

"""
Whether the product is in the specified collection.
@@ -3012,9 +3038,9 @@ type HttpResponse {
headers: [HttpResponseHeader!]! @deprecated(reason: "Use `header` instead.")

"""
The HTTP response body parsed as JSON.
If the body is valid JSON, it will be parsed and returned as a JSON object.
If parsing fails, then raw body is returned as a string.
The HTTP response body parsed as JSON.
If the body is valid JSON, it will be parsed and returned as a JSON object.
If parsing fails, then raw body is returned as a string.
Use this field when you expect the response to be JSON, or when you're dealing
with mixed response types, meaning both JSON and non-JSON.
Using this field reduces function instruction consumption and ensures that the data is formatted in logs.
@@ -3081,7 +3107,7 @@ type Input {
your fetch target, and that is passed as input to the run target. For more
information, refer to [network access for Shopify Functions](https://shopify.dev/docs/apps/build/functions/input-output/network-access).
"""
fetchResult: HttpResponse @restrictTarget(only: ["purchase.validation.run"])
fetchResult: HttpResponse @restrictTarget(only: ["purchase.validation.run", "cart.validations.generate.run"])

"""
The regional and language settings that determine how the Function
@@ -3955,7 +3981,7 @@ type Localization {
"""
The market of the active localized experience.
"""
market: Market!
market: Market! @deprecated(reason: "This `market` field will be removed in a future version of the API.")
}

"""
@@ -4223,7 +4249,7 @@ type MailingAddress {
"""
The market of the address.
"""
market: Market
market: Market @deprecated(reason: "This `market` field will be removed in a future version of the API.")

"""
The full name of the customer, based on firstName and lastName.
@@ -4382,6 +4408,26 @@ type MoneyV2 {
The root mutation for the API.
"""
type MutationRoot {
"""
Handles the Function result for the cart.validations.generate.fetch target.
"""
cartValidationsGenerateFetch(
"""
The result of the Function.
"""
result: CartValidationsGenerateFetchResult!
): Void!

"""
Handles the Function result for the cart.validations.generate.run target.
"""
cartValidationsGenerateRun(
"""
The result of the Function.
"""
result: CartValidationsGenerateRunResult!
): Void!

"""
Handles the Function result for the purchase.validation.fetch target.
"""
@@ -4413,6 +4459,16 @@ type MutationRoot {
): Void!
}

"""
An operation to apply.
"""
input Operation @oneOf {
"""
Add a performed validation.
"""
validationAdd: ValidationAddOperation
}

"""
The goods and services that merchants offer to customers. Products can include details such as
title, vendor, and custom data stored in [metafields](https://shopify.dev/docs/apps/build/custom-data).
@@ -4802,6 +4858,32 @@ type Validation implements HasMetafields {
): Metafield
}

"""
Add a performed validation.
"""
input ValidationAddOperation {
"""
Errors.
"""
errors: [ValidationError!]!
}

"""
A Function error for a path.
"""
input ValidationError {
"""
Returns a message describing the error.
"""
message: String!

"""
Specifies the path/target for use by the UI. See [Supported checkout field targets](https://shopify.dev/docs/api/functions/reference/cart-checkout-validation/graphql#supported-checkout-field-targets)
for a list of supported targets.
"""
target: String!
}

"""
A void type that can be used to return a null value from a mutation.
"""
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
api_version = "2025-01"
api_version = "2025-07"

[[extensions]]
name = "t:name"
@@ -8,11 +8,10 @@ type = "function"
description = "t:description"

[[extensions.targeting]]
target = "purchase.validation.run"
input_query = "src/run.graphql"
export = "run"
target = "cart.validations.generate.run"
input_query = "src/cart_validations_generate_run.graphql"
export = "cart-validations-generate-run"

[extensions.build]
command = ""
path = "dist/function.wasm"

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
query RunInput {
query CartValidationsGenerateRunInput {
cart {
lines {
quantity
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{%- if flavor contains "vanilla-js" -%}
// @ts-check

/**
* @typedef {import("../generated/api").CartValidationsGenerateRunInput} CartValidationsGenerateRunInput
* @typedef {import("../generated/api").CartValidationsGenerateRunResult} CartValidationsGenerateRunResult
*/

/**
* @param {CartValidationsGenerateRunInput} input
* @returns {CartValidationsGenerateRunResult}
*/
export function cartValidationsGenerateRun(input) {
const errors = input.cart.lines
.filter(({ quantity }) => quantity > 1)
.map(() => ({
message: "Not possible to order more than one of each",
target: "$.cart"
}));

const operations = [
{
validationAdd: {
errors
},
},
];

return { operations };
};
{%- elsif flavor contains "typescript" -%}
import type {
CartValidationsGenerateRunInput,
CartValidationsGenerateRunResult,
ValidationError,
} from "../generated/api";

export function cartValidationsGenerateRun(input: CartValidationsGenerateRunInput): CartValidationsGenerateRunResult {
const errors: ValidationError[] = input.cart.lines
.filter(({ quantity }) => quantity > 1)
.map(() => ({
message: "Not possible to order more than one of each",
target: "$.cart"
}));

const operations = [
{
validationAdd: {
errors
},
},
];

return { operations };
};
{%- endif -%}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
{%- if flavor contains "vanilla-js" -%}
import { describe, it, expect } from 'vitest';
import { cartValidationsGenerateRun } from './cart_validations_generate_run';

/**
* @typedef {import("../generated/api").CartValidationsGenerateRunResult} CartValidationsGenerateRunResult
*/

describe('cart checkout validation function', () => {
it('returns an error when quantity exceeds one', () => {
const result = cartValidationsGenerateRun({
cart: {
lines: [
{
quantity: 3
}
]
}
});
const expected = /** @type {CartValidationsGenerateRunResult} */ ({
operations: [
{
validationAdd: {
errors: [
{
message: "Not possible to order more than one of each",
target: "$.cart"
}
]
}
}
]
});

expect(result).toEqual(expected);
});

it('returns no errors when quantity is one', () => {
const result = cartValidationsGenerateRun({
cart: {
lines: [
{
quantity: 1
}
]
}
});
const expected = /** @type {CartValidationsGenerateRunResult} */ ({
operations: [
{
validationAdd: {
errors: []
}
}
]
});

expect(result).toEqual(expected);
});
});

{%- elsif flavor contains "typescript" -%}
import { describe, it, expect } from 'vitest';
import { cartValidationsGenerateRun } from './cart_validations_generate_run';
import { CartValidationsGenerateRunResult } from "../generated/api";

describe('cart checkout validation function', () => {
it('returns an error when quantity exceeds one', () => {
const result = cartValidationsGenerateRun({
cart: {
lines: [
{
quantity: 3
}
]
}
});
const expected: CartValidationsGenerateRunResult = {
operations: [
{
validationAdd: {
errors: [
{
message: "Not possible to order more than one of each",
target: "$.cart"
}
]
}
}
]
};

expect(result).toEqual(expected);
});

it('returns no errors when quantity is one', () => {
const result = cartValidationsGenerateRun({
cart: {
lines: [
{
quantity: 1
}
]
}
});
const expected: CartValidationsGenerateRunResult = {
operations: [
{
validationAdd: {
errors: []
}
}
]
};

expect(result).toEqual(expected);
});
});
{%- endif -%}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export * from './run';
export * from './cart_validations_generate_run';

This file was deleted.

This file was deleted.

94 changes: 88 additions & 6 deletions checkout/rust/cart-checkout-validation/default/schema.graphql
Original file line number Diff line number Diff line change
@@ -13,6 +13,11 @@ Scale the Functions resource limits based on the field's length.
"""
directive @scaleLimits(rate: Float!) on FIELD_DEFINITION

"""
Requires that exactly one field must be supplied and that field must not be `null`.
"""
directive @oneOf on INPUT_OBJECT

"""
A custom property. Attributes are used to store additional information about a Shopify resource, such as
products, customers, or orders. Attributes are stored as key-value pairs.
@@ -352,6 +357,27 @@ type CartLineCost {
totalAmount: MoneyV2!
}

"""
The fetch target result. Your Function must return this data structure when generating the request.
"""
input CartValidationsGenerateFetchResult {
"""
The attributes associated with an HTTP request.
"""
request: HttpRequest
}

"""
The output of the Function run target. The object contains the validation errors
that display to customers and prevent them from proceeding through checkout.
"""
input CartValidationsGenerateRunResult {
"""
The ordered list of operations to apply.
"""
operations: [Operation!]!
}

"""
Whether the product is in the specified collection.
@@ -3012,9 +3038,9 @@ type HttpResponse {
headers: [HttpResponseHeader!]! @deprecated(reason: "Use `header` instead.")

"""
The HTTP response body parsed as JSON.
If the body is valid JSON, it will be parsed and returned as a JSON object.
If parsing fails, then raw body is returned as a string.
The HTTP response body parsed as JSON.
If the body is valid JSON, it will be parsed and returned as a JSON object.
If parsing fails, then raw body is returned as a string.
Use this field when you expect the response to be JSON, or when you're dealing
with mixed response types, meaning both JSON and non-JSON.
Using this field reduces function instruction consumption and ensures that the data is formatted in logs.
@@ -3081,7 +3107,7 @@ type Input {
your fetch target, and that is passed as input to the run target. For more
information, refer to [network access for Shopify Functions](https://shopify.dev/docs/apps/build/functions/input-output/network-access).
"""
fetchResult: HttpResponse @restrictTarget(only: ["purchase.validation.run"])
fetchResult: HttpResponse @restrictTarget(only: ["purchase.validation.run", "cart.validations.generate.run"])

"""
The regional and language settings that determine how the Function
@@ -3955,7 +3981,7 @@ type Localization {
"""
The market of the active localized experience.
"""
market: Market!
market: Market! @deprecated(reason: "This `market` field will be removed in a future version of the API.")
}

"""
@@ -4223,7 +4249,7 @@ type MailingAddress {
"""
The market of the address.
"""
market: Market
market: Market @deprecated(reason: "This `market` field will be removed in a future version of the API.")

"""
The full name of the customer, based on firstName and lastName.
@@ -4382,6 +4408,26 @@ type MoneyV2 {
The root mutation for the API.
"""
type MutationRoot {
"""
Handles the Function result for the cart.validations.generate.fetch target.
"""
cartValidationsGenerateFetch(
"""
The result of the Function.
"""
result: CartValidationsGenerateFetchResult!
): Void!

"""
Handles the Function result for the cart.validations.generate.run target.
"""
cartValidationsGenerateRun(
"""
The result of the Function.
"""
result: CartValidationsGenerateRunResult!
): Void!

"""
Handles the Function result for the purchase.validation.fetch target.
"""
@@ -4413,6 +4459,16 @@ type MutationRoot {
): Void!
}

"""
An operation to apply.
"""
input Operation @oneOf {
"""
Add a performed validation.
"""
validationAdd: ValidationAddOperation
}

"""
The goods and services that merchants offer to customers. Products can include details such as
title, vendor, and custom data stored in [metafields](https://shopify.dev/docs/apps/build/custom-data).
@@ -4802,6 +4858,32 @@ type Validation implements HasMetafields {
): Metafield
}

"""
Add a performed validation.
"""
input ValidationAddOperation {
"""
Errors.
"""
errors: [ValidationError!]!
}

"""
A Function error for a path.
"""
input ValidationError {
"""
Returns a message describing the error.
"""
message: String!

"""
Specifies the path/target for use by the UI. See [Supported checkout field targets](https://shopify.dev/docs/api/functions/reference/cart-checkout-validation/graphql#supported-checkout-field-targets)
for a list of supported targets.
"""
target: String!
}

"""
A void type that can be used to return a null value from a mutation.
"""
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
api_version = "2025-01"
api_version = "2025-07"

[[extensions]]
name = "t:name"
@@ -8,12 +8,11 @@ type = "function"
description = "t:description"

[[extensions.targeting]]
target = "purchase.validation.run"
input_query = "src/run.graphql"
export = "run"
target = "cart.validations.generate.run"
input_query = "src/cart_validations_generate_run.graphql"
export = "cart_validations_generate_run"

[extensions.build]
command = "cargo build --target=wasm32-wasip1 --release"
path = "target/wasm32-wasip1/release/{{handle | replace: " ", "-" | downcase}}.wasm"
watch = ["src/**/*.rs"]

Original file line number Diff line number Diff line change
@@ -6,8 +6,9 @@ use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Default, PartialEq)]
struct Config {}

#[shopify_function_target(query_path = "src/run.graphql", schema_path = "schema.graphql")]
fn run(input: input::ResponseData) -> Result<output::FunctionRunResult> {
#[shopify_function_target(query_path = "src/cart_validations_generate_run.graphql", schema_path = "schema.graphql")]
fn cart_validations_generate_run(input: input::ResponseData) -> Result<output::CartValidationsGenerateRunResult> {
let mut operations = Vec::new();
let mut errors = Vec::new();

if input
@@ -17,12 +18,16 @@ fn run(input: input::ResponseData) -> Result<output::FunctionRunResult> {
.map(|line| line.quantity)
.any(|quantity| quantity > 1)
{
errors.push(output::FunctionError {
localized_message: "Not possible to order more than one of each".to_owned(),
errors.push(output::ValidationError {
message: "Not possible to order more than one of each".to_owned(),
target: "$.cart".to_owned(),
})
}
Ok(output::FunctionRunResult { errors })

let operation = output::ValidationAddOperation { errors };
operations.push(output::Operation::ValidationAdd(operation));

Ok(output::CartValidationsGenerateRunResult { operations })
}

#[cfg(test)]
@@ -32,10 +37,10 @@ mod tests {

#[test]
fn test_result_contains_single_error_when_quantity_exceeds_one() -> Result<()> {
use run::output::*;
use cart_validations_generate_run::output::*;

let result = run_function_with_input(
run,
cart_validations_generate_run,
r#"
{
"cart": {
@@ -48,11 +53,13 @@ mod tests {
}
"#,
)?;
let expected = FunctionRunResult {
errors: vec![FunctionError {
localized_message: "Not possible to order more than one of each".to_owned(),
target: "$.cart".to_owned(),
}],
let expected = CartValidationsGenerateRunResult {
operations: vec![Operation::ValidationAdd(ValidationAddOperation {
errors: vec![ValidationError {
message: "Not possible to order more than one of each".to_owned(),
target: "$.cart".to_owned(),
}],
})],
};

assert_eq!(result, expected);
@@ -61,10 +68,10 @@ mod tests {

#[test]
fn test_result_contains_no_errors_when_quantity_is_one() -> Result<()> {
use run::output::*;
use cart_validations_generate_run::output::*;

let result = run_function_with_input(
run,
cart_validations_generate_run,
r#"
{
"cart": {
@@ -77,7 +84,11 @@ mod tests {
}
"#,
)?;
let expected = FunctionRunResult { errors: vec![] };
let expected = CartValidationsGenerateRunResult {
operations: vec![Operation::ValidationAdd(ValidationAddOperation {
errors: vec![],
})],
};

assert_eq!(result, expected);
Ok(())
2 changes: 1 addition & 1 deletion checkout/rust/cart-checkout-validation/default/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::process;
pub mod run;
pub mod cart_validations_generate_run;

fn main() {
eprintln!("Please invoke a named export.");
94 changes: 88 additions & 6 deletions checkout/wasm/cart-checkout-validation/default/schema.graphql
Original file line number Diff line number Diff line change
@@ -13,6 +13,11 @@ Scale the Functions resource limits based on the field's length.
"""
directive @scaleLimits(rate: Float!) on FIELD_DEFINITION

"""
Requires that exactly one field must be supplied and that field must not be `null`.
"""
directive @oneOf on INPUT_OBJECT

"""
A custom property. Attributes are used to store additional information about a Shopify resource, such as
products, customers, or orders. Attributes are stored as key-value pairs.
@@ -352,6 +357,27 @@ type CartLineCost {
totalAmount: MoneyV2!
}

"""
The fetch target result. Your Function must return this data structure when generating the request.
"""
input CartValidationsGenerateFetchResult {
"""
The attributes associated with an HTTP request.
"""
request: HttpRequest
}

"""
The output of the Function run target. The object contains the validation errors
that display to customers and prevent them from proceeding through checkout.
"""
input CartValidationsGenerateRunResult {
"""
The ordered list of operations to apply.
"""
operations: [Operation!]!
}

"""
Whether the product is in the specified collection.
@@ -3012,9 +3038,9 @@ type HttpResponse {
headers: [HttpResponseHeader!]! @deprecated(reason: "Use `header` instead.")

"""
The HTTP response body parsed as JSON.
If the body is valid JSON, it will be parsed and returned as a JSON object.
If parsing fails, then raw body is returned as a string.
The HTTP response body parsed as JSON.
If the body is valid JSON, it will be parsed and returned as a JSON object.
If parsing fails, then raw body is returned as a string.
Use this field when you expect the response to be JSON, or when you're dealing
with mixed response types, meaning both JSON and non-JSON.
Using this field reduces function instruction consumption and ensures that the data is formatted in logs.
@@ -3081,7 +3107,7 @@ type Input {
your fetch target, and that is passed as input to the run target. For more
information, refer to [network access for Shopify Functions](https://shopify.dev/docs/apps/build/functions/input-output/network-access).
"""
fetchResult: HttpResponse @restrictTarget(only: ["purchase.validation.run"])
fetchResult: HttpResponse @restrictTarget(only: ["purchase.validation.run", "cart.validations.generate.run"])

"""
The regional and language settings that determine how the Function
@@ -3955,7 +3981,7 @@ type Localization {
"""
The market of the active localized experience.
"""
market: Market!
market: Market! @deprecated(reason: "This `market` field will be removed in a future version of the API.")
}

"""
@@ -4223,7 +4249,7 @@ type MailingAddress {
"""
The market of the address.
"""
market: Market
market: Market @deprecated(reason: "This `market` field will be removed in a future version of the API.")

"""
The full name of the customer, based on firstName and lastName.
@@ -4382,6 +4408,26 @@ type MoneyV2 {
The root mutation for the API.
"""
type MutationRoot {
"""
Handles the Function result for the cart.validations.generate.fetch target.
"""
cartValidationsGenerateFetch(
"""
The result of the Function.
"""
result: CartValidationsGenerateFetchResult!
): Void!

"""
Handles the Function result for the cart.validations.generate.run target.
"""
cartValidationsGenerateRun(
"""
The result of the Function.
"""
result: CartValidationsGenerateRunResult!
): Void!

"""
Handles the Function result for the purchase.validation.fetch target.
"""
@@ -4413,6 +4459,16 @@ type MutationRoot {
): Void!
}

"""
An operation to apply.
"""
input Operation @oneOf {
"""
Add a performed validation.
"""
validationAdd: ValidationAddOperation
}

"""
The goods and services that merchants offer to customers. Products can include details such as
title, vendor, and custom data stored in [metafields](https://shopify.dev/docs/apps/build/custom-data).
@@ -4802,6 +4858,32 @@ type Validation implements HasMetafields {
): Metafield
}

"""
Add a performed validation.
"""
input ValidationAddOperation {
"""
Errors.
"""
errors: [ValidationError!]!
}

"""
A Function error for a path.
"""
input ValidationError {
"""
Returns a message describing the error.
"""
message: String!

"""
Specifies the path/target for use by the UI. See [Supported checkout field targets](https://shopify.dev/docs/api/functions/reference/cart-checkout-validation/graphql#supported-checkout-field-targets)
for a list of supported targets.
"""
target: String!
}

"""
A void type that can be used to return a null value from a mutation.
"""
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
api_version = "2025-01"
api_version = "2025-07"

[[extensions]]
name = "t:name"
@@ -8,12 +8,11 @@ type = "function"
description = "t:description"

[[extensions.targeting]]
target = "purchase.validation.run"
input_query = "run.graphql"
export = "run"
target = "cart.validations.generate.run"
input_query = "cart_validations_generate_run.graphql"
export = "cart_validations_generate_run"

[extensions.build]
command = "echo 'build the wasm'"
path = ""
watch = []