Skip to main content

Add configuration to your location rule function

You can use metafields to store configuration values for your location rule function. Metafields provide greater flexibility to use functions, and are a prerequisite to creating a merchant user interface for configuring functions.

Beta

Location rules is a new feature that's only available by request. Reach out to Shopify Plus Support to know more about your eligibility and the requirements for the beta program.


In this tutorial, you'll learn how to do the following tasks:

  • Add configuration to your location rule function using metafields.
  • Use a TOML declarative custom data definition to define configuration metafields.
  • Add metafields to your GraphQL input query.
  • Use metafield values in your function logic.


Anchor to Step 1: Define the configuration metafieldStep 1: Define the configuration metafield

To make your function reusable, you can replace hardcoded values in your function with metafield values. Use TOML declarative custom data definitions to define a metafield that will store the preferred country configuration for your location rule function.

  1. In your app's shopify.app.toml file, add the following metafield definition:

    shopify.app.toml

    [order_routing_location_rule.metafields.app.preferred-country]
    type = "json"
    name = "Preferred Country"
    description = "The preferred country for location routing"
    access.admin = "merchant_read_write"

    This TOML configuration creates a metafield definition that:

    • Stores JSON data containing the preferred country configuration
    • Allows merchants to configure location routing preferences through the admin
    • Is automatically available across all stores where your app is installed
    • Uses the app-scoped namespace to ensure data ownership and security

Anchor to Step 2: Configure the functionStep 2: Configure the function

Now that you've defined the metafield structure with TOML, you can update your function to read from it.

Update your input query to request a metafield value on the created location rule, which is the function owner for this function API. You can then use that value in your function logic.

  1. Navigate to your function in extensions/location-rule.

    Terminal

    cd extensions/location-rule
  2. Replace the code in the src/run.graphql file with the following code.

    This update to the input query adds a metafield from the locationRule object, which is the function owner.

    The query differs slightly in Rust and JavaScript due to code generation requirements.

    run.graphql

    src/run.graphql

    query Input {
    locationRule {
    metafield(namespace: "$app", key: "preferred-country") {
    jsonValue
    },
    }
    fulfillmentGroups {
    handle
    inventoryLocationHandles
    }
    locations {
    handle
    address {
    countryCode
    }
    }
    }
    query RunInput {
    locationRule {
    metafield(namespace: "$app", key: "preferred-country") {
    jsonValue
    },
    }
    fulfillmentGroups {
    handle
    inventoryLocationHandles
    }
    locations {
    handle
    address {
    countryCode
    }
    }
    }
  3. If you're using JavaScript, then run the following command to regenerate types based on your input query:

    Terminal

    shopify app function typegen
  4. If you're using Rust, replace the src/main.rs file with the following code that converts the metafield into a data structure in the Rust program.

    main.rs

    src/main.rs
    use shopify_function::prelude::*;

    pub mod run;

    #[typegen("schema.graphql")]
    pub mod schema {
    #[query("src/run.graphql",
    custom_scalar_overrides = {
    "Input.locationRule.metafield.jsonValue" => super::run::Configuration,
    }
    )]
    pub mod run {}
    }

    fn main() {
    log!("Please invoke a named export.");
    std::process::abort();
    }
  5. Replace the src/run.rs or src/run.js file with the following code.

    This update includes parsing the JSON metafield value, and using values from that JSON in the function logic instead of hardcoded values.

    This change is automatically reflected as long as you're running dev.

    File

    src/run.rs

    use super::schema;
    use shopify_function::prelude::*;
    use shopify_function::Result;
    use crate::schema::Handle;

    // Create a structure that matches the JSON structure that you'll use for your configuration
    #[derive( Deserialize, Default, PartialEq)]
    #[shopify_function(rename_all = "camelCase")]
    pub struct Configuration {
    preferred_country_code: String,
    }

    #[shopify_function]
    fn run(input: schema::run::Input) -> Result<schema::CartFulfillmentGroupsLocationRankingsGenerateRunResult> {
    let no_changes = schema::FunctionRunResult { operations: vec![] };
    // Get the configuration from the metafield on your function owner
    let config: Configuration = match input.location_rule().metafield() {
    Some(metafield) => metafield.json_value(),
    None => return Ok(no_changes),
    };

    // Load the fulfillment groups and generate the rank operations for each one using
    // the configured preferred country code instead of a hardcoded value
    let operations = input
    .fulfillment_groups()
    .into_iter()
    .map(|fulfillment_group| schema::Operation {
    fulfillment_group_location_ranking_add: build_rank_operation(fulfillment_group, &input.locations(), config.preferred_country_code.clone()),
    })
    .collect();
    // Return the operations
    Ok(schema::CartFulfillmentGroupsLocationRankingsGenerateRunResult { operations })
    }

    fn build_rank_operation(
    input: &schema::run::input::FulfillmentGroups,
    locations: &[schema::run::input::Locations],
    preferred_country_code: String,
    ) -> schema::FulfillmentGroupLocationRankingAddOperation {
    schema::FulfillmentGroupLocationRankingAddOperation {
    fulfillment_group_handle: input.handle().to_string(),
    rankings: prioritize_locations(input.inventory_location_handles().to_vec(), locations, preferred_country_code),
    }
    }

    fn prioritize_locations(
    handles: Vec<Handle>,
    locations: &[schema::run::input::Locations],
    preferred_country_code: String,
    ) -> Vec<schema::RankedLocation> {
    // Load the inventory locations for the fulfillment group
    handles
    .into_iter()
    .map(|location_handle| {
    let location = locations.iter().find(|&loc| *loc.handle() == location_handle);
    // Rank the location as 0 if the country code matches the preferred one, otherwise rank it as 1
    schema::RankedLocation {
    location_handle,
    rank: match location {
    Some(location) => {
    if location.address().country_code() == Some(&preferred_country_code.to_string()) {
    0
    } else {
    1
    }
    }
    None => 1,
    },
    }
    })
    .collect()
    }
    // @ts-check

    /**
    * @typedef {import("../generated/api").RunInput} RunInput
    * @typedef {import("../generated/api").RunInput["fulfillmentGroups"][0]} FulfillmentGroup
    * @typedef {import("../generated/api").RunInput["locations"][0]} Location
    * @typedef {import("../generated/api").CartFulfillmentGroupsLocationRankingsGenerateRunResult} CartFulfillmentGroupsLocationRankingsGenerateRunResult
    * @typedef {import("../generated/api").Operation} Operation
    * @typedef {import("../generated/api").FulfillmentGroupLocationRankingAddOperation} FulfillmentGroupLocationRankingAddOperation
    * @typedef {import("../generated/api").RankedLocation} RankedLocation
    */

    /**
    * @param {RunInput} input
    * @returns {CartFulfillmentGroupsLocationRankingsGenerateRunResult}
    */
    export function run(input) {
    // Define a type for your configuration, and parse it from the metafield
    /**
    * @type {{
    * preferredCountryCode: string
    * }}
    */
    const configuration = input?.locationRule?.metafield?.jsonValue ?? {};

    // Load the fulfillment groups and generate the rank operations for each one
    let operations = input.fulfillmentGroups
    .map(fulfillmentGroup => /** @type {Operation} */(
    {
    fulfillment_group_location_ranking_add: buildRankOperation(fulfillmentGroup, input.locations, configuration.preferredCountryCode)
    }
    ));

    // Return the operations
    return { operations: operations };
    };

    /**
    * @param {FulfillmentGroup} fulfillmentGroup
    * @param {Location[]} locations
    * @param {String} preferredCountryCode
    * @returns {FulfillmentGroupLocationRankingAddOperation}
    */
    function buildRankOperation(fulfillmentGroup, locations, preferredCountryCode) {
    return {
    fulfillmentGroupHandle: fulfillmentGroup.handle,
    rankings: prioritizeLocations(fulfillmentGroup.inventoryLocationHandles, locations, preferredCountryCode),
    };
    };

    /**
    * @param {string[]} locationHandles
    * @param {Location[]} locations
    * @param {String} preferredCountryCode
    * @returns {RankedLocation[]}
    */
    function prioritizeLocations(locationHandles, locations, preferredCountryCode) {
    // Load the inventory locations for the fulfillment group
    return locationHandles.map(locationHandle => {
    const location = locations.find((loc) => loc.handle == locationHandle);
    return {
    locationHandle,
    // Rank the location as 0 if the country code matches the preferred one, otherwise rank it as 1
    rank: location?.address.countryCode === preferredCountryCode ? 0 : 1,
    }
    });
    };


Was this page helpful?