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:

  • Create a metafield definition.
  • Add metafields to your GraphQL input query.
  • Use metafield values in your function logic.


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

const definition = {
access: {
admin: "MERCHANT_READ_WRITE"
},
description: "The preferred country",
key: "preferred-country",
name: "Preferred Country",
namespace: " $app:location-rule",
ownerType: "ORDER_ROUTING_LOCATION_RULE",
type: "json",
};

const response = await admin.graphql(
`#graphql
mutation CreateMetafieldDefinition($definition) {
metafieldDefinitionCreate(definition: $definition) {
createdDefinition {
id
}
userErrors {
field
message
code
}
}
}`,
{
variables: {
definition
}
}
);

Your code should look like this:

shopify.server.ts

import "@shopify/shopify-app-remix/adapters/node";
import {
AppDistribution,
DeliveryMethod,
shopifyApp,
LATEST_API_VERSION,
} from "@shopify/shopify-app-remix/server";
import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma";
import { restResources } from "@shopify/shopify-api/rest/admin/2023-10";
import prisma from "./db.server";

const shopify = shopifyApp({
apiKey: process.env.SHOPIFY_API_KEY,
apiSecretKey: process.env.SHOPIFY_API_SECRET || "",
apiVersion: LATEST_API_VERSION,
scopes: process.env.SCOPES?.split(","),
appUrl: process.env.SHOPIFY_APP_URL || "",
authPathPrefix: "/auth",
sessionStorage: new PrismaSessionStorage(prisma),
distribution: AppDistribution.AppStore,
restResources,
webhooks: {
APP_UNINSTALLED: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
},
hooks: {
afterAuth: async ({ session, admin }) => {
shopify.registerWebhooks({ session });

const definition = {
access: {
admin: "MERCHANT_READ_WRITE"
},
description: "The preferred country",
key: "preferred-country",
name: "Preferred Country",
namespace: " $app:location-rule",
ownerType: "ORDER_ROUTING_LOCATION_RULE",
type: "json",
};

const response = await admin.graphql(
`#graphql
mutation CreateMetafieldDefinition($definition) {
metafieldDefinitionCreate(definition: $definition) {
createdDefinition {
id
}
userErrors {
field
message
code
}
}
}`,
{
variables: {
definition
}
}
);
},
},
future: {
v3_webhookAdminContext: true,
v3_authenticatePublic: true,
},
...(process.env.SHOP_CUSTOM_DOMAIN
? { customShopDomains: [process.env.SHOP_CUSTOM_DOMAIN] }
: {}),
});

export default shopify;
export const apiVersion = LATEST_API_VERSION;
export const addDocumentResponseHeaders = shopify.addDocumentResponseHeaders;
export const authenticate = shopify.authenticate;
export const unauthenticated = shopify.unauthenticated;
export const login = shopify.login;
export const registerWebhooks = shopify.registerWebhooks;
export const sessionStorage = shopify.sessionStorage;

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.

  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: "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
    }
    }
    }
  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 will convert 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() {
    eprintln!("Please invoke a named export.");
    std::process::exit(1);
    }
  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?