Working with BigCommerce data
You will often want to store BigCommerce data in your Gadget database after subscribing to BigCommerce webhooks.
This guide will show you how to read and write data to BigCommerce using Gadget, and how you can store BigCommerce data in your Gadget database.
Calling the BigCommerce API
Before you can store data, you need to fetch it from BigCommerce! BigCommerce webhooks often only provide a resource ID, so you will need to use the BigCommerce API to fetch the full resource data.
The best way to call the BigCommerce API is to use the included BigCommerce API client.
Here's a simple example of using the API client to read products from BigCommerce in a global action.
1export const run: ActionRun = async ({ params, logger, api, connections }) => {2 // get BigCommerce API client for the store3 const bigcommerce = await connections.bigcommerce.forStoreHash(4 "<add-your-store-hash>"5 );6 // use the API client to fetch 5 products, and return7 const products = await bigcommerce?.v3.get("/catalog/products", {8 query: { limit: 5 },9 });10 return products;11};
1export const run: ActionRun = async ({ params, logger, api, connections }) => {2 // get BigCommerce API client for the store3 const bigcommerce = await connections.bigcommerce.forStoreHash(4 "<add-your-store-hash>"5 );6 // use the API client to fetch 5 products, and return7 const products = await bigcommerce?.v3.get("/catalog/products", {8 query: { limit: 5 },9 });10 return products;11};
You can also use the API client to write data back to a BigCommerce store:
1export const run: ActionRun = async ({ params, logger, api, connections }) => {2 // get BigCommerce API client for the store3 const bigcommerce = await connections.bigcommerce.forStoreHash(4 "<add-your-store-hash>"5 );6 // use the API client create a new product7 await bigcommerce?.v3.post("/catalog/products", {8 body: {9 name: "My new product",10 type: "physical",11 price: 10,12 weight: 1,13 },14 });15};
1export const run: ActionRun = async ({ params, logger, api, connections }) => {2 // get BigCommerce API client for the store3 const bigcommerce = await connections.bigcommerce.forStoreHash(4 "<add-your-store-hash>"5 );6 // use the API client create a new product7 await bigcommerce?.v3.post("/catalog/products", {8 body: {9 name: "My new product",10 type: "physical",11 price: 10,12 weight: 1,13 },14 });15};
Data stored in Gadget models is not automatically synced with BigCommerce. The best way to keep data in BigCommerce and Gadget in sync is to write data back to BigCommerce using the API client, and then allow webhooks to update data in Gadget.
Store context for API client
The connections.bigcommerce
object provides access to a BigCommerce API client. You can use this client to read and write data to BigCommerce.
Depending on how an action was triggered, the current store context may be available on the connections
object. For example, if you trigger a global action from a BigCommerce webhook, or call an action from a single-click frontend, the current store context will be available with connections.bigcommerce.current
:
const bigcommerce = connections.bigcommerce.current;logger.info(await bigcommerce?.v3.get("/catalog/products", { query: { limit: 1 } }),"read 1 bigcommerce product");
const bigcommerce = connections.bigcommerce.current;logger.info(await bigcommerce?.v3.get("/catalog/products", { query: { limit: 1 } }),"read 1 bigcommerce product");
If the current store context is not available, such as calling an action from another action in Gadget, you can use the forStoreHash
method to get the API client for a specific store hash:
1const bigcommerce = await connections.bigcommerce.forStoreHash(2 "<add-your-store-hash>"3);4logger.info(5 await bigcommerce?.v3.get("/catalog/products", { query: { limit: 1 } }),6 "read 1 product from BigCommerce"7);
1const bigcommerce = await connections.bigcommerce.forStoreHash(2 "<add-your-store-hash>"3);4logger.info(5 await bigcommerce?.v3.get("/catalog/products", { query: { limit: 1 } }),6 "read 1 product from BigCommerce"7);
This is common when calling model actions from webhook-triggered global actions.
Read more about connections.bigcommerce
in the gadget-server
reference.
Storing data in Gadget
You can store data read from BigCommerce in Gadget data models. This allows you to access all data required to power your application in a single, highly available and performant, non-rate limited database.
When you set up your BigCommerce connection, a store
data model was created at api/models/bigcommerce/store
. You can also add custom models to the bigcommerce
namespace.
It's also important to relate any additional BigCommerce resource models to the bigcommerce/store
model so that you can associate data
with a specific store!
To add a custom BigCommerce data model:
- Right-click on the
api/models/bigcommerce
directory in the Gadget editor and select Add model - Give your model a name, for example
product
ororder
- Add a field to store the BigCommerce
id
- Add a relationship field between the new model and the
bigcommerce/store
model so thatbigcommerce/store
has manybigcommerce/newModel
- Add fields to store the rest of the BigCommerce data you need to power your app
You can use the auto-generated CRUD (create, read, update, delete) API to interact with your new model.
For example, you can call a bigcommerce/product
model's create
action to store a new record in Gadget:
1import { ActionOptions } from "gadget-server";23export const run: ActionRun = async ({ params, logger, api, connections }) => {4 // get the BigCommerce API client for the current store5 const bigcommerce = connections.bigcommerce.current;67 if (!bigcommerce) {8 throw new Error("Missing bigcommerce connection");9 }1011 // fetch the product data12 const product = await bigcommerce.v3.get("/catalog/products/{product_id}", {13 path: {14 product_id: params.id,15 },16 });1718 if (!product) {19 throw new Error("Missing product");20 }2122 // store product data in the product data model23 await api.bigcommerce.product.create({24 bigcommerceId: product.id,25 store: {26 // get the bigcommerce/store id for the record stored in Gadget27 _link: connections.bigcommerce.currentStoreId,28 },29 name: product.name,30 // ... any other product fields that need to be stored31 });32};3334export const options: ActionOptions = {35 triggers: {36 bigcommerce: {37 webhooks: ["store/product/created"],38 },39 },40};
1import { ActionOptions } from "gadget-server";23export const run: ActionRun = async ({ params, logger, api, connections }) => {4 // get the BigCommerce API client for the current store5 const bigcommerce = connections.bigcommerce.current;67 if (!bigcommerce) {8 throw new Error("Missing bigcommerce connection");9 }1011 // fetch the product data12 const product = await bigcommerce.v3.get("/catalog/products/{product_id}", {13 path: {14 product_id: params.id,15 },16 });1718 if (!product) {19 throw new Error("Missing product");20 }2122 // store product data in the product data model23 await api.bigcommerce.product.create({24 bigcommerceId: product.id,25 store: {26 // get the bigcommerce/store id for the record stored in Gadget27 _link: connections.bigcommerce.currentStoreId,28 },29 name: product.name,30 // ... any other product fields that need to be stored31 });32};3334export const options: ActionOptions = {35 triggers: {36 bigcommerce: {37 webhooks: ["store/product/created"],38 },39 },40};
Storing BigCommerce IDs
It is important to store BigCommerce resource IDs in Gadget! This is the unique identifier for the record in BigCommerce, and is required to handle update or delete webhooks, and associate data stored in Gadget with BigCommerce records.
It's also a good idea to add both the Required and Uniqueness validations to the bigcommerceId
field on your models! For multi-tenant applications, you should also scope this uniqueness validation to the store
relationship so that each store can only have one record with a given bigcommerceId
.
Upserting data and handling /updated
webhooks
When you are storing data from BigCommerce in Gadget, you may want to update existing records if they already exist, or create a new record if it doesn't. This is known as upserting data. This is particularly useful when handling /updated
webhooks, where a record may or may not already exist in your Gadget database.
Your Gadget API provides an upsert
meta action that manages the upsert logic for you. For BigCommerce data, you often want to upsert based on the bigcommerceId
field. For multi-tenant applications, you should also scope the upsert to the store
relationship. This ensures that you only update or create records for the current store.
Here is an example of how you could upsert product data in Gadget when a store/product/updated
webhook is received:
1export const run: ActionRun = async ({ params, api, connections }) => {2 // get the BigCommerce API client for the current store3 const bigcommerce = connections.bigcommerce.current;45 if (!bigcommerce) {6 throw new Error("Missing bigcommerce connection");7 }8 // fetch the product data9 const product = await bigcommerce.v3.get("/catalog/products/{product_id}", {10 path: {11 product_id: params.id,12 },13 });1415 if (!product) {16 throw new Error("Missing product");17 }1819 // upsert product data in the product data model20 await api.bigcommerce.product.upsert({21 bigcommerceId: product.id,22 store: {23 // get the bigcommerce/store id for the record stored in Gadget24 _link: connections.bigcommerce.currentStoreId,25 },26 name: product.name,27 // ... any other product fields that need to be stored28 // use bigcommerceId and store to identify unique records for upsert29 on: ["bigcommerceId", "store"],30 });31};3233export const options: ActionOptions = {34 triggers: {35 bigcommerce: {36 webhooks: ["store/product/updated"],37 },38 },39};
1export const run: ActionRun = async ({ params, api, connections }) => {2 // get the BigCommerce API client for the current store3 const bigcommerce = connections.bigcommerce.current;45 if (!bigcommerce) {6 throw new Error("Missing bigcommerce connection");7 }8 // fetch the product data9 const product = await bigcommerce.v3.get("/catalog/products/{product_id}", {10 path: {11 product_id: params.id,12 },13 });1415 if (!product) {16 throw new Error("Missing product");17 }1819 // upsert product data in the product data model20 await api.bigcommerce.product.upsert({21 bigcommerceId: product.id,22 store: {23 // get the bigcommerce/store id for the record stored in Gadget24 _link: connections.bigcommerce.currentStoreId,25 },26 name: product.name,27 // ... any other product fields that need to be stored28 // use bigcommerceId and store to identify unique records for upsert29 on: ["bigcommerceId", "store"],30 });31};3233export const options: ActionOptions = {34 triggers: {35 bigcommerce: {36 webhooks: ["store/product/updated"],37 },38 },39};
This will check to see if a product with the given bigcommerceId
and store
relationship already exists in Gadget. If it does, it will update the existing record. If it doesn't, it will create a new record.
Storing data from child models
You can store data from child models in Gadget by adding relationships between models. When you create a new model, you can add a relationship field to associate the new model with the parent model.
An example of this relationship is products and product variants. You can create a bigcommerce/productVariant
model and associate it with the bigcommerce/product
model using a relationship field.
Whether you are syncing or creating data from a webhook, you can include the required child data when reading from BigCommerce so it is returned in the same payload.
1const product = await bigcommerce.v3.get("/catalog/products/{product_id}", {2 path: {3 product_id: parseInt(params.id!),4 },5 query: {6 include: ["variants"],7 },8});
1const product = await bigcommerce.v3.get("/catalog/products/{product_id}", {2 path: {3 product_id: parseInt(params.id!),4 },5 query: {6 include: ["variants"],7 },8});
You also have the option to make a separate API call to retrieve the child data and store it in Gadget. Refer to the BigCommerce API reference for more information on retrieving child data.
Once you have the child data, you can store it in the child model using the CRUD API.
For example, here is how you could store product variant data in Gadget when a store/product/created
webhook is received:
1import { ActionOptions } from "gadget-server";23export const run: ActionRun = async ({ params, logger, api, connections }) => {4 // get the BigCommerce API client for the current store5 const bigcommerce = connections.bigcommerce.current;67 if (!bigcommerce) {8 throw new Error("Missing bigcommerce connection");9 }1011 // fetch the product data12 const product = await bigcommerce.v3.get("/catalog/products/{product_id}", {13 path: {14 product_id: params.id,15 },16 query: {17 include: ["variants"],18 },19 });2021 if (!product) {22 throw new Error("Missing product");23 }2425 // log the product data and webhook payload26 logger.info({ product, params }, "product data and webhook payload");2728 // iterate over the variants and store them in an array29 const variants = [];30 for (const variant of product.variants) {31 variants.push({32 bigcommerceId: variant.id,33 store: {34 _link: connections.bigcommerce.currentStoreId,35 },36 // ... any other product variant fields that need to be stored37 });38 }3940 // store product data in the product data model41 const productRecord = await api.bigcommerce.product.upsert({42 bigcommerceId: product.id,43 name: product.name,44 store: {45 // get the bigcommerce/store id for the record stored in Gadget46 _link: connections.bigcommerce.currentStoreId,47 },48 variants: {49 // use a _converge to relate the variants to the product and add them to the db50 _converge: {51 values: variants,52 },53 },54 });55};5657export const options: ActionOptions = {58 triggers: {59 bigcommerce: {60 webhooks: ["store/product/created"],61 },62 },63};
1import { ActionOptions } from "gadget-server";23export const run: ActionRun = async ({ params, logger, api, connections }) => {4 // get the BigCommerce API client for the current store5 const bigcommerce = connections.bigcommerce.current;67 if (!bigcommerce) {8 throw new Error("Missing bigcommerce connection");9 }1011 // fetch the product data12 const product = await bigcommerce.v3.get("/catalog/products/{product_id}", {13 path: {14 product_id: params.id,15 },16 query: {17 include: ["variants"],18 },19 });2021 if (!product) {22 throw new Error("Missing product");23 }2425 // log the product data and webhook payload26 logger.info({ product, params }, "product data and webhook payload");2728 // iterate over the variants and store them in an array29 const variants = [];30 for (const variant of product.variants) {31 variants.push({32 bigcommerceId: variant.id,33 store: {34 _link: connections.bigcommerce.currentStoreId,35 },36 // ... any other product variant fields that need to be stored37 });38 }3940 // store product data in the product data model41 const productRecord = await api.bigcommerce.product.upsert({42 bigcommerceId: product.id,43 name: product.name,44 store: {45 // get the bigcommerce/store id for the record stored in Gadget46 _link: connections.bigcommerce.currentStoreId,47 },48 variants: {49 // use a _converge to relate the variants to the product and add them to the db50 _converge: {51 values: variants,52 },53 },54 });55};5657export const options: ActionOptions = {58 triggers: {59 bigcommerce: {60 webhooks: ["store/product/created"],61 },62 },63};
Syncing all existing data
If you need to sync all existing data from a BigCommerce store, you can use the BigCommerce API client to fetch all data and store it in Gadget. There are a couple of things you need to be aware of and handle properly, including the BigCommerce API rate limits and max page size for fetching data.
Gadget's built-in background actions queue can help you manage this process. Background actions allow developers to set custom concurrency controls to adhere to a rate limit, and have built-in retry logic for failed actions, ensuring that all data is synced.
To maximize the speed of the sync, you can use bulk actions to enqueue multiple actions at once. For a data sync, you will often want to use Gadget's upsert
meta API so that you can update existing records and create new records in a single action.
Here is an example of how you could sync all product data from a BigCommerce store:
1import { ActionOptions } from "gadget-server";23export const run: ActionRun = async ({ params, logger, api, connections }) => {4 // set the batch size to 50 for bulk upsert5 const BATCH_SIZE = 50;67 const bigcommerce = connections.bigcommerce.current;89 if (!bigcommerce) {10 throw new Error("Missing bigcommerce connection");11 }1213 // use the API client to fetch all products, and return14 const products = await bigcommerce.v3.list(`/catalog/products`);1516 const productPayload = [];17 // use a for await loop to iterate over the AsyncIterables, add to an array18 for await (const product of products) {19 productPayload.push({20 // store the BigCommerce ID21 bigcommerceId: product.id,22 // associate the product with the current store23 store: {24 _link: connections.bigcommerce.currentStoreId,25 },26 name: product.name,27 // ... add more fields as needed28 // use bigcommerceId and store to identify unique records for upsert29 on: ["bigcommerceId", "store"],30 });3132 // enqueue 50 actions at a time33 if (productPayload.length >= BATCH_SIZE) {34 const section = productPayload.splice(0, BATCH_SIZE);35 // bulk enqueue create action36 await api.enqueue(api.bigcommerce.product.bulkUpsert, section, {37 queue: { name: "product-sync" },38 });39 }40 }4142 // enqueue any remaining products43 await api.enqueue(api.bigcommerce.product.bulkUpsert, productPayload, {44 queue: { name: "product-sync" },45 });46};4748export const options: ActionOptions = {49 // 15 minute timeout for the sync50 timeoutMS: 900000,51};
1import { ActionOptions } from "gadget-server";23export const run: ActionRun = async ({ params, logger, api, connections }) => {4 // set the batch size to 50 for bulk upsert5 const BATCH_SIZE = 50;67 const bigcommerce = connections.bigcommerce.current;89 if (!bigcommerce) {10 throw new Error("Missing bigcommerce connection");11 }1213 // use the API client to fetch all products, and return14 const products = await bigcommerce.v3.list(`/catalog/products`);1516 const productPayload = [];17 // use a for await loop to iterate over the AsyncIterables, add to an array18 for await (const product of products) {19 productPayload.push({20 // store the BigCommerce ID21 bigcommerceId: product.id,22 // associate the product with the current store23 store: {24 _link: connections.bigcommerce.currentStoreId,25 },26 name: product.name,27 // ... add more fields as needed28 // use bigcommerceId and store to identify unique records for upsert29 on: ["bigcommerceId", "store"],30 });3132 // enqueue 50 actions at a time33 if (productPayload.length >= BATCH_SIZE) {34 const section = productPayload.splice(0, BATCH_SIZE);35 // bulk enqueue create action36 await api.enqueue(api.bigcommerce.product.bulkUpsert, section, {37 queue: { name: "product-sync" },38 });39 }40 }4142 // enqueue any remaining products43 await api.enqueue(api.bigcommerce.product.bulkUpsert, productPayload, {44 queue: { name: "product-sync" },45 });46};4748export const options: ActionOptions = {49 // 15 minute timeout for the sync50 timeoutMS: 900000,51};
This will sync all products from a BigCommerce store to Gadget. It may take some time before all records are synced!
This action has a timeout of 15 minutes, which is the maximum time an action can run in Gadget. Contact the Gadget team on Discord if your sync takes longer than 15 minutes!
If you need to sync on a regular schedule, you can use a scheduled action to run the sync at a set interval.
Handle BigCommerce rate limits
Rate limits in BigCommerce are per store and shared across all apps installed on a store. This means that 429 response codes must be handled properly.
The included BigCommerce API client will automatically handle rate limits for you and will retry requests that return 429 or 5XX response codes using an exponential backoff.
Gadget's background actions can also be used to manage rate limits. You can set the concurrency of a background action to match the rate limit of the BigCommerce API and use the built-in retry logic to handle rate limit errors.
To do this, use the maxConcurrency
option in the enqueue
function to limit how many actions are run concurrently.
For example, to create new products in BigCommerce, you would run the following action in a background action:
1export const run: ActionRun = async ({ params, logger, api, connections }) => {2 // get BigCommerce API client for the store3 const bigcommerce = await connections.bigcommerce.forStoreHash(params.storeHash!);45 // use the API client to fetch 5 products, and return6 await bigcommerce?.v3.post("/catalog/products", {7 body: {8 name: "My new product",9 type: "physical",10 price: 10,11 weight: 1,12 },13 });14};1516// define product param17export const params = {18 product: {19 type: "object",20 properties: {21 name: { type: "string" },22 price: { type: "number" },23 weight: { type: "number" },24 },25 },26 storeHash: { type: "string" },27};
1export const run: ActionRun = async ({ params, logger, api, connections }) => {2 // get BigCommerce API client for the store3 const bigcommerce = await connections.bigcommerce.forStoreHash(params.storeHash!);45 // use the API client to fetch 5 products, and return6 await bigcommerce?.v3.post("/catalog/products", {7 body: {8 name: "My new product",9 type: "physical",10 price: 10,11 weight: 1,12 },13 });14};1516// define product param17export const params = {18 product: {19 type: "object",20 properties: {21 name: { type: "string" },22 price: { type: "number" },23 weight: { type: "number" },24 },25 },26 storeHash: { type: "string" },27};
And then enqueue the action with a maxConcurrency
option:
1export const run: ActionRun = async ({ params, logger, api, connections }) => {2 if (!params.products) {3 throw new Error("The `products` param is required to run this action");4 }5 // loop over products from passed in params6 for (const product of params.products) {7 await api.enqueue(8 api.bigcommerce.createProduct,9 {10 product,11 storeHash: connections.bigcommerce.currentStoreHash,12 },13 {14 // use maxConcurrency to limit how fast enqueued actions are run15 queue: {16 name: "create-products",17 maxConcurrency: 2,18 },19 }20 );21 }22};2324export const params = {25 products: {26 type: "array",27 items: {28 type: "object",29 properties: {30 name: { type: "string" },31 price: { type: "number" },32 weight: { type: "number" },33 },34 },35 },36};
1export const run: ActionRun = async ({ params, logger, api, connections }) => {2 if (!params.products) {3 throw new Error("The `products` param is required to run this action");4 }5 // loop over products from passed in params6 for (const product of params.products) {7 await api.enqueue(8 api.bigcommerce.createProduct,9 {10 product,11 storeHash: connections.bigcommerce.currentStoreHash,12 },13 {14 // use maxConcurrency to limit how fast enqueued actions are run15 queue: {16 name: "create-products",17 maxConcurrency: 2,18 },19 }20 );21 }22};2324export const params = {25 products: {26 type: "array",27 items: {28 type: "object",29 properties: {30 name: { type: "string" },31 price: { type: "number" },32 weight: { type: "number" },33 },34 },35 },36};
When using background actions, you will need to pass the storeHash
to the enqueued action as a parameter. This is because the current
session and data tenancy is not passed when you use the api
to make requests.
Working with BigCommerce metafields
Similar to any other resource in BigCommerce, you can read and write metafields using the BigCommerce API client. You might wish to store metafield data in line with the resource data in Gadget.
There are two ways to store metafield data next to your model data:
Subscribe to metafield webhooks in a global action and update the model data in Gadget when a metafield is created, updated, or deleted. Resources that can store metafield data have their own metafield webhook topics, so you can subscribe to these topics and update your model data accordingly.
Fetch metafield data when you fetch the resource data and store it in the model. This would be required when syncing data into your Gadget database and the code would look similar to the sync example.
Subscribe to metafield webhooks
Metafield payloads always have a metafield_id
and a resource_id
field, which you can use to update the correct record in your model:
json1{2 "metafield_id": 2,3 "namespace": "Sales Department",4 "resource_id": "133",5 "storeHash": "bbcolvaxts"6}
Both the resource_id
and metafield_id
should be stored in your model to be able to update and delete metafields.
Here is an example of a store/product/metafield/created
webhook subscription that updates the bigcommerce/product
model when a metafield is created for a product. The product
model stores resource_id
as bigcommerceId
and metafield_id
as metafieldId
.
1import { ActionOptions } from "gadget-server";23export const run: ActionRun = async ({ params, logger, api, connections }) => {4 // get the BigCommerce API client5 const bigcommerce = connections.bigcommerce.current;67 if (!bigcommerce) {8 throw new Error("Missing bigcommerce connection");9 }1011 // get ids from webhook payload12 const productId = params.resource_id;13 const metafieldId = params.metafield_id;1415 // fetch the metafield value from BigCommerce16 const metafield = await bigcommerce.v3.get(17 "/catalog/products/{product_id}/metafields/{metafield_id}",18 {19 path: {20 product_id: productId,21 metafield_id: metafieldId,22 },23 }24 );2526 if (!metafield) {27 throw new Error("Missing metafield");28 }2930 // see if the product exists in the Gadget db31 let product = await api.bigcommerce.product.maybeFindByBigCommerceId(32 parseInt(productId)33 );3435 // if the product doesn't exist in Gagdet, fetch it36 if (!product) {37 product = await bigcommerce.v3.get("/catalog/products/{product_id}", {38 path: {39 product_id: productId,40 },41 });4243 // and add the product to the Gadget db44 return await api.bigcommerce.product.create({45 bigcommerceId: product.id,46 store: {47 // get the bigcommerce/store id for the record stored in Gadget48 _link: connections.bigcommerce.currentStoreId,49 },50 name: product.name,51 metafieldId,52 metafieldValue: metafield.value,53 });54 }5556 // update the existing product57 return await api.bigcommerce.product.update(product.id, {58 metafieldId,59 metafieldValue: metafield.value,60 });61};6263export const options: ActionOptions = {64 triggers: {65 api: false,66 bigcommerce: {67 webhooks: ["store/product/metafield/created"],68 },69 },70};
1import { ActionOptions } from "gadget-server";23export const run: ActionRun = async ({ params, logger, api, connections }) => {4 // get the BigCommerce API client5 const bigcommerce = connections.bigcommerce.current;67 if (!bigcommerce) {8 throw new Error("Missing bigcommerce connection");9 }1011 // get ids from webhook payload12 const productId = params.resource_id;13 const metafieldId = params.metafield_id;1415 // fetch the metafield value from BigCommerce16 const metafield = await bigcommerce.v3.get(17 "/catalog/products/{product_id}/metafields/{metafield_id}",18 {19 path: {20 product_id: productId,21 metafield_id: metafieldId,22 },23 }24 );2526 if (!metafield) {27 throw new Error("Missing metafield");28 }2930 // see if the product exists in the Gadget db31 let product = await api.bigcommerce.product.maybeFindByBigCommerceId(32 parseInt(productId)33 );3435 // if the product doesn't exist in Gagdet, fetch it36 if (!product) {37 product = await bigcommerce.v3.get("/catalog/products/{product_id}", {38 path: {39 product_id: productId,40 },41 });4243 // and add the product to the Gadget db44 return await api.bigcommerce.product.create({45 bigcommerceId: product.id,46 store: {47 // get the bigcommerce/store id for the record stored in Gadget48 _link: connections.bigcommerce.currentStoreId,49 },50 name: product.name,51 metafieldId,52 metafieldValue: metafield.value,53 });54 }5556 // update the existing product57 return await api.bigcommerce.product.update(product.id, {58 metafieldId,59 metafieldValue: metafield.value,60 });61};6263export const options: ActionOptions = {64 triggers: {65 api: false,66 bigcommerce: {67 webhooks: ["store/product/metafield/created"],68 },69 },70};
You can also use the namespace and key to identify the metafield, which is useful when you have multiple metafields for a resource.
Data security and multi-tenancy
If you are building a public app, you will need to ensure that data is stored securely and that one store's data is not accessible by another store. Gadget provides tools that help manage multi-tenancy and row-level security (RLS) for models and actions.
Securing models and model actions
Gadget provides a set of data access tools that can be used to secure models and their actions. These tools include:
- relationship fields on models so that you can associate data with a
store
record - a Uniqueness validation to make sure that each store has only one record with a given ID
- built-in functions for preventing cross-shop data access in model actions
- filtered model permissions that restrict access based on the related
store
Follow these steps to set up these tools and enforce multi-tenancy on a model:
- If you haven't already done so, add a relationship field to any custom model that associates the model with a
store
record
For example, add a belongs to relationship field on the bigcommerce/product
model. The field should be named store
and should reference the bigcommerce/store
model, so that product belongsTo store
. Then define the inverse of the relationship so that store hasMany products
.
Add a Uniqueness validation to the BigCommerce resource ID field on your model and scope it to the
store
relationship. This is important to ensure that stored IDs are unique per store. See storing BigCommerce IDs for more information.In your custom model, import
preventCrossStoreDataAccess
fromgadget-server/bigcommerce
and use it in your model actions
For example, in the bigcommerce/product
model, you can call preventCrossStoreDataAccess
in the run
function of the create
action to ensure that a product can only be created for the current store:
1import { applyParams, save, ActionOptions } from "gadget-server";2// import the preventCrossStoreDataAccess function3import { preventCrossStoreDataAccess } from "gadget-server/bigcommerce";45export const run: ActionRun = async ({6 params,7 record,8 logger,9 api,10 connections,11}) => {12 applyParams(params, record);13 // use the preventCrossStoreDataAccess function to make sure14 // the product is created for the current store15 await preventCrossStoreDataAccess(params, record);16 await save(record);17};1819export const options: ActionOptions = {20 actionType: "create",21};
1import { applyParams, save, ActionOptions } from "gadget-server";2// import the preventCrossStoreDataAccess function3import { preventCrossStoreDataAccess } from "gadget-server/bigcommerce";45export const run: ActionRun = async ({6 params,7 record,8 logger,9 api,10 connections,11}) => {12 applyParams(params, record);13 // use the preventCrossStoreDataAccess function to make sure14 // the product is created for the current store15 await preventCrossStoreDataAccess(params, record);16 await save(record);17};1819export const options: ActionOptions = {20 actionType: "create",21};
Each time preventCrossStoreDataAccess
runs, it will make sure the given record has the correct storeId
for the store processing this action. For existing records, that means it will verify that the storeId
of the saved record the matches the storeId
on the incoming record
.
- Add a tenancy filter to the
read
action for the custom model inaccessControl/permissions
.
Tenancy filters are expressed with Gelly, Gadget's data access language. These filters check the store ID of the current session against the ID of the store that the data being read belongs to.
Most tenancy filters for BigCommerce look similar to this:
accessControl/filters/bigcommerce/product.gellygellyfilter ($session: Session) on BigCommerceStore [where storeId == $session.bigcommerceStoreId]
When you set up a relationship field in Gadget, an id
field for the related model is automatically generated on the
belongs to side of the relationship. For example, when a relationship field named store
is added a storeId
field will
also be available on the child model. This field is used to store the ID of the associated store
record and is useful in tenancy filters
or when manually filtering API requests per store.
Securing global actions and HTTP routes
When using global actions or HTTP routes to read or write data, you cannot use Gelly tenancy filters or functions like preventCrossStoreDataAccess
because there is no backing model or relationship to a store
record.
For global actions or HTTP routes, you often need to filter data based on the bigcommerceStoreId
, which is included as a properly on the connections
parameter. Filtering is built into your app's API and available on most field types. Docs for filtering are available in the API reference.
For example, if I am fetching bigcommerce/order
data in a global action before doing some processing and returning the results, I would do the following:
1export const run: ActionRun = async ({ params, logger, api, connections }) => {2 const bigcommerceStoreId = connections.bigcommerce.currentStoreId;34 // filter results by the store ID5 const orders = await api.bigcommerce.order.findMany({6 filter: {7 storeId: {8 equals: bigcommerceStoreId,9 },10 },11 });1213 return orders;14};
1export const run: ActionRun = async ({ params, logger, api, connections }) => {2 const bigcommerceStoreId = connections.bigcommerce.currentStoreId;34 // filter results by the store ID5 const orders = await api.bigcommerce.order.findMany({6 filter: {7 storeId: {8 equals: bigcommerceStoreId,9 },10 },11 });1213 return orders;14};
This will only work when there is an authenticated session
!
Otherwise, you will need to manually fetch the store ID from bigcommerce/store
using the store hash:
1export const run: ActionRun = async ({ params, logger, api, connections }) => {2 // get the store ID from the record in Gadget3 const bigcommerceStore = await api.bigcommerce.store.findByStoreHash(4 params.storeHash!,5 {6 // only select the id field7 select: {8 id: true,9 },10 }11 );1213 // filter results by the store ID14 const orders = await api.bigcommerce.order.findMany({15 filter: {16 storeId: {17 equals: bigcommerceStore.id,18 },19 },20 });2122 return orders;23};2425// allow the storeHash to be passed as a parameter26export const params = {27 storeHash: { type: "string" },28};
1export const run: ActionRun = async ({ params, logger, api, connections }) => {2 // get the store ID from the record in Gadget3 const bigcommerceStore = await api.bigcommerce.store.findByStoreHash(4 params.storeHash!,5 {6 // only select the id field7 select: {8 id: true,9 },10 }11 );1213 // filter results by the store ID14 const orders = await api.bigcommerce.order.findMany({15 filter: {16 storeId: {17 equals: bigcommerceStore.id,18 },19 },20 });2122 return orders;23};2425// allow the storeHash to be passed as a parameter26export const params = {27 storeHash: { type: "string" },28};
The same pattern can be used for HTTP routes, where you would fetch the store ID from the bigcommerce/store
model and then filter the results based on the store ID.