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.
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.
Anchor to What you'll learnWhat you'll learn
In this tutorial, you'll learn how to do the following tasks:
- Create a metafield definition.
- Add metafields to your GraphQL input query.
- Use metafield values in your function logic.
Anchor to RequirementsRequirements
- You've completed the Getting started with building location rules tutorial.
Anchor to Step 1: Create the metafield definitionStep 1: Create the metafield definition
To make your function reusable, you can replace hardcoded values in your function with metafield values.
For security reasons, it's recommended to create a metafield definition under a reserved namespace. The Shopify admin requires additional permissions to handle reserved namespaces in order to broker metafield changes on behalf of your application.
Update shopify.server.ts
file with the following code inside the afterAuth
hook to create a metafield definition and grant additional access to Shopify admin:
shopify.server.ts
Your code should look like this:
shopify.server.ts
Anchor to Step 2: Configure the functionStep 2: Configure the function
After you create the metafield definition, you can update your function to use metafield values set by the user.
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.
-
Navigate to your function in
extensions/location-rule
.Terminal
cd extensions/location-rule -
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: "location-rule", key: "preferred-country") {jsonValue},}fulfillmentGroups {handleinventoryLocationHandles}locations {handleaddress {countryCode}}}query RunInput {locationRule {metafield(namespace: "location-rule", key: "preferred-country") {jsonValue},}fulfillmentGroups {handleinventoryLocationHandles}locations {handleaddress {countryCode}}}query Input { locationRule { metafield(namespace: "location-rule", key: "preferred-country") { jsonValue }, } fulfillmentGroups { handle inventoryLocationHandles } locations { handle address { countryCode } } }
query RunInput { locationRule { metafield(namespace: "location-rule", key: "preferred-country") { jsonValue }, } fulfillmentGroups { handle inventoryLocationHandles } locations { handle address { countryCode } } }
-
If you're using JavaScript, then run the following command to regenerate types based on your input query:
Terminal
shopify app function typegen -
If you're using Rust replace the
src/main.rs
file with the following code that will convert the metafield into a data structure in the Rust program.main.rs
src/main.rsuse shopify_function::prelude::*;pub mod run;pub mod schema {pub mod run {}}fn main() {eprintln!("Please invoke a named export.");std::process::exit(1);} -
Replace the
src/run.rs
orsrc/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 configurationpub struct Configuration {preferred_country_code: String,}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 ownerlet 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 valuelet 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 operationsOk(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 grouphandles.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 1schema::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 onelet operations = input.fulfillmentGroups.map(fulfillmentGroup => /** @type {Operation} */({fulfillment_group_location_ranking_add: buildRankOperation(fulfillmentGroup, input.locations, configuration.preferredCountryCode)}));// Return the operationsreturn { 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 groupreturn 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 1rank: location?.address.countryCode === preferredCountryCode ? 0 : 1,}});};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, } }); };
Anchor to Next stepsNext steps
- Build a location rule user interface using Admin UI extensions.
- Learn how to use variables in your input query.