Keeping Data Updated

How to keep your database copies of Apicbase objects up to date?

We do not provide webhook-based solutions for recipes, ingredients or similar objects to notify API clients of changes to these objects.

When external apps need to keep a local database copy of these items up to date, the recommended approach is to periodically poll the Apicbase API for updates, filtering on the modification date of the item, and updating all items that have been modified since the last periodic update.

The modified_date__gt filter that can be applied to most list endpoints (for example, the recipe list endpoint) allows clients to only fetch items that have been recently modified. It is crucial to apply this filter because fetching a full assortment of recipe details from large databases can take hours!

Laying this process out in steps, it goes like this:

  1. Query the list endpoint with the modified_date__gt filter, specifying the date of the last sync.
  2. Poll the detail endpoint (specified in the url attribute) of each item in the filtered list.
  3. Update the database copy with the data found in the detail endpoint.

Here's a sample script for a periodic sync that achieves this task:

import requests
import datetime

# Set up API authentication
access_token = 'your_access_token'
headers = {'Authorization': f'Bearer {access_token}'}

# Set up last sync date
last_sync_date = '2023-04-30'  # replace with your actual last sync date

# Define API endpoints
recipe_list_url = 'https://app.apicbase.com/v1/products/recipes/'

# Query the API recipe list endpoint with the modified_date__gt query param
params = {'modified_date__gt': last_sync_date}
response = requests.get(recipe_list_url, headers=headers, params=params)
recipe_list = response.json()['results']

# Query the detail endpoint of each item in the filtered list
for recipe in recipe_list:
    recipe_url = recipe['url']
    response = requests.get(recipe_url, headers=headers)
    recipe_detail = response.json()

    # Update local database copy of each recipe with the data returned by the API
    # replace the print statement with your actual database update code
    print(f'Updating recipe with ID {recipe_id}')
    print(recipe_detail) # this is just an example, replace with your actual update code

const fetch = require('node-fetch');

// Set up API authentication
const accessToken = 'your_access_token';
const headers = { 'Authorization': `Bearer ${accessToken}` };

// Set up last sync date
const lastSyncDate = '2023-04-30'; // replace with your actual last sync date

// Define API endpoints
const recipeListUrl = 'https://api.apicbase.com/v1/products/recipes/';

// Query the API recipe list endpoint with the modified_date__gt query param
const params = { 'modified_date__gt': lastSyncDate };
fetch(`${recipeListUrl}?${new URLSearchParams(params)}`, { headers })
  .then(response => response.json())
  .then(responseJson => {
    const recipeList = responseJson.results;

    // Query the detail endpoint of each item in the filtered list
    recipeList.forEach(recipe => {
      const recipeUrl = recipe.url;
      fetch(recipeUrl, { headers })
        .then(response => response.json())
        .then(recipeDetail => {
          // Update local database copy of each recipe with the data returned by the API
          // replace the console.log statement with your actual database update code
          console.log(`Updating recipe with ID ${recipeDetail.id}`);
          console.log(recipeDetail); // this is just an example, replace with your actual update code
        });
    });
  });

<?php

// Set up API authentication
$access_token = 'your_access_token';
$headers = array('Authorization' => 'Bearer ' . $access_token);

// Set up last sync date
$last_sync_date = '2023-04-30'; // replace with your actual last sync date

// Define API endpoints
$recipe_list_url = 'https://api.apicbase.com/v1/products/recipes/';

// Query the API recipe list endpoint with the modified_date__gt query param
$params = array('modified_date__gt' => $last_sync_date);
$response = Requests::get($recipe_list_url, $headers, $params);
$recipe_list = json_decode($response->body)->results;

// Query the detail endpoint of each item in the filtered list
foreach ($recipe_list as $recipe) {
    $recipe_url = $recipe->url;
    $response = Requests::get($recipe_url, $headers);
    $recipe_detail = json_decode($response->body);

    // Update local database copy of each recipe with the data returned by the API
    // replace the print statement with your actual database update code
    echo 'Updating recipe with ID ' . $recipe_detail->id . PHP_EOL;
    var_dump($recipe_detail); // this is just an example, replace with your actual update code
}
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;

class Program
{
    static async Task Main(string[] args)
    {
        // Set up API authentication
        string access_token = "your_access_token";
        var headers = new AuthenticationHeaderValue("Bearer", access_token);

        // Set up last sync date
        string last_sync_date = "2023-04-30";  // replace with your actual last sync date

        // Define API endpoints
        string recipe_list_url = "https://app.apicbase.com/v1/products/recipes/";

        // Create HTTP client
        var client = new HttpClient();
        client.DefaultRequestHeaders.Authorization = headers;

        // Query the API recipe list endpoint with the modified_date__gt query param
        string urlWithParams = $"{recipe_list_url}?modified_date__gt={last_sync_date}";
        var response = await client.GetAsync(urlWithParams);
        response.EnsureSuccessStatusCode();

        var responseBody = await response.Content.ReadAsStringAsync();
        dynamic recipe_list = JObject.Parse(responseBody).results;

        // Query the detail endpoint of each item in the filtered list
        foreach (var recipe in recipe_list)
        {
            string recipe_url = recipe.url;
            response = await client.GetAsync(recipe_url);
            response.EnsureSuccessStatusCode();
            responseBody = await response.Content.ReadAsStringAsync();
            dynamic recipe_detail = JObject.Parse(responseBody);

            // Update local database copy of each recipe with the data returned by the API
            // replace the Console.WriteLine statements with your actual database update code
            Console.WriteLine($"Updating recipe with ID {recipe_detail.id}");
            Console.WriteLine(recipe_detail); // this is just an example, replace with your actual update code
        }
    }
}

The modification date and calculated attributes

The modification date in the API reflects changes made to native attributes of an object. It does not include changes made to calculated attributes that may depend on other objects. For instance, updating the price of a stock item could impact the cost price of multiple recipes, but it will not update the modification date of those recipes.

To build a solution for these cases, you need to think a bit further based on where the information that you need to track is coming from, and filter on the modification date of that base item.

If you're tracking the cost prices of recipes, for example, you would filter on stock items by modification date, then get their ingredients, and from each ingredient, get the list of recipes where they're being used (although for this specific example, it would be better to just get the last entry in the get recipe financial data endpoint).

Ingredients and recipes have a used_in_recipes field that you can use to navigate this structure upwards and find the items that may need to be updated. Make sure to also check the used_in_recipes attribute of a recipe to find cases where it is itself used as a subrecipe.

❗️

This is how it works for allergens and nutritional info, too.

In a recipe, allergens and nutritional data are calculated fields based on the ingredients. So you need to track the modification dates of the ingredients in order to keep the recipe copies in your local database up to date.

Here's one example of such a script that performs this task recursively:

import requests

def update_recipes(last_sync):
    # Get list of ingredients changed since the last sync
    ingredients_changed = request_api('products/ingredients', {"modified_date__gt": last_sync})
 
    for ingredient in ingredients_changed:
        recipes_that_use_it = ingredient['used_in_recipes']
        
        # Update every recipe that uses this ingredient
        for recipe_url in recipes_that_use_it:
            recipe_detail = request_api(recipe_url)
            update_recipe_detail(recipe_detail)
  
    # Now, get a list of recipes themselves that changed since the last sync
    # Because allergens can also be defined on the recipe itself
    recipes_changed = request_api('products/recipes', {"modified_date__gt": last_sync})
    for recipe in recipes_changed:
        recipe_detail = request_api(recipe['url'])
        update_recipe_detail(recipe_detail)

def update_recipe_detail(recipe):
    # Update recipe copy
    my_local_recipe = get_recipe_from_db(recipe['id'])
    my_local_recipe.allergens = recipe['allergens']
    
    # Also update every recipe that uses it as a subrecipe
    recipes_that_use_it = recipe['used_in_recipes']
    for recipe_url in recipes_that_use_it:
        recipe_detail = request_api(recipe_url)
        update_recipe_detail(recipe_detail)

def request_api(endpoint, params=None):
    url = f"https://app.apicbase.com/api/v1/{endpoint}/"
    headers = {'Authorization': 'Bearer your-api-token'}
    response = requests.get(url, headers=headers, params=params)
    return response.json()

const requestApi = (endpoint, params) => {
  const url = `https://app.apicbase.com/api/v1/${endpoint}/`;
  const headers = { Authorization: 'Bearer your-api-token' };
  const response = await fetch(new URL(url), { headers, params });
  return await response.json();
};

const updateRecipeDetail = (recipe) => {
  // Update recipe copy
  const myLocalRecipe = getRecipeFromDb(recipe.id);
  myLocalRecipe.allergens = recipe.allergens;

  // Also update every recipe that uses it as a subrecipe
  const recipesThatUseIt = recipe.used_in_recipes;
  for (const recipeUrl of recipesThatUseIt) {
    const recipeDetail = requestApi(recipeUrl);
    updateRecipeDetail(recipeDetail);
  }
};

const updateRecipes = async (lastSync) => {
  // Get list of ingredients changed since the last sync
  const ingredientsChanged = await requestApi('products/ingredients', {
    modified_date__gt: lastSync,
  });

  for (const ingredient of ingredientsChanged) {
    const recipesThatUseIt = ingredient.used_in_recipes;

    // Update every recipe that uses this ingredient
    for (const recipeUrl of recipesThatUseIt) {
      const recipeDetail = await requestApi(recipeUrl);
      updateRecipeDetail(recipeDetail);
    }
  }

  // Now, get a list of recipes themselves that changed since the last sync
  // Because allergens can also be defined on the recipe itself
  const recipesChanged = await requestApi('products/recipes', {
    modified_date__gt: lastSync,
  });
  for (const recipe of recipesChanged) {
    const recipeDetail = await requestApi(recipe.url);
    updateRecipeDetail(recipeDetail);
  }
};

<?php

function update_recipes($last_sync) {
  // Get list of ingredients changed since the last sync
  $ingredients_changed = request_api('products/ingredients', array('modified_date__gt' => $last_sync));
 
  foreach ($ingredients_changed as $ingredient) {
    $recipes_that_use_it = $ingredient['used_in_recipes'];
    
    // Update every recipe that uses this ingredient
    foreach ($recipes_that_use_it as $recipe_url) {
      $recipe_detail = request_api($recipe_url);
      update_recipe_detail($recipe_detail);
    }
  }
  
  // Now, get a list of recipes themselves that changed since the last sync
  // Because allergens can also be defined on the recipe itself
  $recipes_changed = request_api('products/recipes', array('modified_date__gt' => $last_sync));
  foreach ($recipes_changed as $recipe) {
    $recipe_detail = request_api($recipe['url']);
    update_recipe_detail($recipe_detail);
  }
}

function update_recipe_detail($recipe) {
  // Update recipe copy
  $my_local_recipe = get_recipe_from_db($recipe['id']);
  $my_local_recipe['allergens'] = $recipe['allergens'];
  
  // Also update every recipe that uses it as a subrecipe
  $recipes_that_use_it = $recipe['used_in_recipes'];
  foreach ($recipes_that_use_it as $recipe_url) {
    $recipe_detail = request_api($recipe_url);
    update_recipe_detail($recipe_detail);
  }
}

function request_api($endpoint, $params = null) {
  $url = "https://app.apicbase.com/api/v1/$endpoint/";
  $headers = array('Authorization' => 'Bearer your-api-token');
  $response = requests\get($url, array('headers' => $headers, 'query' => $params));
  return json_decode($response->getBody(), true);
}

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace YourNamespace.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class RecipeController : ControllerBase
    {
        private readonly HttpClient _httpClient;

        public RecipeController(IHttpClientFactory httpClientFactory)
        {
            _httpClient = httpClientFactory.CreateClient();
            _httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer your-access-token");
        }

        [HttpGet("UpdateRecipes/{lastSyncDate}")]
        public async Task<IActionResult> UpdateRecipes(DateTime lastSyncDate)
        {
            var ingredientsChanged = await RequestApiAsync<List<object>>("products/ingredients", new Dictionary<string, string>
            {
                {"modified_date__gt", lastSyncDate.ToString("yyyy-MM-dd")}
            });

            foreach (var ingredient in ingredientsChanged)
            {
                var recipesThatUseIt = (List<string>)ingredient["used_in_recipes"];

                foreach (var recipeUrl in recipesThatUseIt)
                {
                    var recipeDetail = await RequestApiAsync<object>(recipeUrl);
                    await UpdateRecipeDetailAsync(recipeDetail);
                }
            }

            var recipesChanged = await RequestApiAsync<List<object>>("products/recipes", new Dictionary<string, string>
            {
                {"modified_date__gt", lastSyncDate.ToString("yyyy-MM-dd")}
            });

            foreach (var recipe in recipesChanged)
            {
                var recipeDetail = await RequestApiAsync<object>((string)recipe["url"]);
                await UpdateRecipeDetailAsync(recipeDetail);
            }

            return Ok();
        }

        private async Task<T> RequestApiAsync<T>(string endpoint, IDictionary<string, string> queryParams = null)
        {
            var queryString = "";
            if (queryParams != null)
            {
                foreach (var kvp in queryParams)
                {
                    queryString += $"{kvp.Key}={kvp.Value}&";
                }
            }

            var url = $"https://app.apicbase.com/api/v1/{endpoint}/?{queryString.TrimEnd('&')}";
            var response = await _httpClient.GetAsync(url);
            response.EnsureSuccessStatusCode();
            var json = await response.Content.ReadAsStringAsync();
            return Newtonsoft.Json.JsonConvert.DeserializeObject<T>(json);
        }

        private async Task UpdateRecipeDetailAsync(object recipe)
        {
            // Update recipe copy
            var myLocalRecipe = await GetRecipeFromDbAsync((string)recipe["id"]);
            myLocalRecipe.allergens = (List<string>)recipe["allergens"];

            // Also update every recipe that uses it as a subrecipe
            var recipesThatUseIt = (List<string>)recipe["used_in_recipes"];
            foreach (var recipeUrl in recipesThatUseIt)
            {
                var recipeDetail = await RequestApiAsync<object>(recipeUrl);
                await UpdateRecipeDetailAsync(recipeDetail);
            }
        }

        private async Task<object> GetRecipeFromDbAsync(string id)
        {
            // Replace with actual database call
            await Task.CompletedTask;
            return null;
        }
    }
}