diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml
index 716b1dee5..8db6c1d40 100755
--- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml
@@ -2322,6 +2322,51 @@ paths:
#content:
# - application/json
+ /data-stores:
+ get:
+ operationId: spiffworkflow_backend.routes.data_store_controller.data_store_list
+ summary: Return a list of the data store objects.
+ tags:
+ - Data Stores
+ responses:
+ "200":
+ description: The list of currently defined data store objects
+ /data-stores/{data_store_type}/{name}:
+ parameters:
+ - name: data_store_type
+ in: path
+ required: true
+ description: The type of datastore, such as "typeahead"
+ schema:
+ type: string
+ - name: name
+ in: path
+ required: true
+ description: The name of the datastore, such as "cities"
+ schema:
+ type: string
+ - name: page
+ in: query
+ required: false
+ description: The page number to return. Defaults to page 1.
+ schema:
+ type: integer
+ - name: per_page
+ in: query
+ required: false
+ description: The page number to return. Defaults to page 1.
+ schema:
+ type: integer
+ get:
+ operationId: spiffworkflow_backend.routes.data_store_controller.data_store_item_list
+ summary: Returns a paginated list of the contents of a data store.
+ tags:
+ - Data Stores
+ responses:
+ "200":
+ description: A list of the data stored in the requested data store.
+
+
components:
securitySchemes:
jwt:
diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/data_store_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/data_store_controller.py
new file mode 100644
index 000000000..5b824a93c
--- /dev/null
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/data_store_controller.py
@@ -0,0 +1,46 @@
+"""APIs for dealing with process groups, process models, and process instances."""
+
+import flask.wrappers
+from flask import jsonify
+from flask import make_response
+
+from spiffworkflow_backend import db
+from spiffworkflow_backend.exceptions.api_error import ApiError
+from spiffworkflow_backend.models.typeahead import TypeaheadModel
+
+
+def data_store_list() -> flask.wrappers.Response:
+ """Returns a list of the names of all the data stores."""
+ data_stores = []
+
+ # Right now the only data store we support is type ahead
+
+ for cat in db.session.query(TypeaheadModel.category).distinct(): # type: ignore
+ data_stores.append({"name": cat[0], "type": "typeahead"})
+
+ return make_response(jsonify(data_stores), 200)
+
+
+def data_store_item_list(
+ data_store_type: str, name: str, page: int = 1, per_page: int = 100
+) -> flask.wrappers.Response:
+ """Returns a list of the items in a data store."""
+ if data_store_type == "typeahead":
+ data_store_query = TypeaheadModel.query.filter_by(category=name)
+ data = data_store_query.paginate(page=page, per_page=per_page, error_out=False)
+ results = []
+ for typeahead in data.items:
+ result = typeahead.result
+ result["search_term"] = typeahead.search_term
+ results.append(result)
+ response_json = {
+ "results": results,
+ "pagination": {
+ "count": len(data.items),
+ "total": data.total,
+ "pages": data.pages,
+ },
+ }
+ return make_response(jsonify(response_json), 200)
+ else:
+ raise ApiError("unknown_data_store", f"Unknown data store type: {data_store_type}", status_code=400)
diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py
index 7c2198d05..058c6e4de 100644
--- a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py
@@ -558,6 +558,8 @@ class AuthorizationService:
for permission in ["create", "read", "update", "delete"]:
permissions_to_assign.append(PermissionToAssign(permission=permission, target_uri="/process-instances/*"))
permissions_to_assign.append(PermissionToAssign(permission=permission, target_uri="/secrets/*"))
+
+ permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/data-stores/*"))
return permissions_to_assign
@classmethod
diff --git a/spiffworkflow-backend/tests/data/data_stores/data_stores.bpmn b/spiffworkflow-backend/tests/data/data_stores/data_stores.bpmn
new file mode 100644
index 000000000..7b0d06a2e
--- /dev/null
+++ b/spiffworkflow-backend/tests/data/data_stores/data_stores.bpmn
@@ -0,0 +1,1861 @@
+
+
+
+
+
+ Flow_0tn4ed3
+
+
+
+ Flow_0lz96a1
+
+
+
+
+
+
+ {
+"typeahead": {},
+ "cities_result": [
+ {
+ "id": 52,
+ "name": "Ashkāsham",
+ "state_id": 3901,
+ "state_code": "BDS",
+ "state_name": "Badakhshan",
+ "country_id": 1,
+ "country_code": "AF",
+ "country_name": "Afghanistan",
+ "latitude": "36.68333000",
+ "longitude": "71.53333000",
+ "wikiDataId": "Q4805192"
+ },
+ {
+ "id": 68,
+ "name": "Fayzabad",
+ "state_id": 3901,
+ "state_code": "BDS",
+ "state_name": "Badakhshan",
+ "country_id": 1,
+ "country_code": "AF",
+ "country_name": "Afghanistan",
+ "latitude": "37.11664000",
+ "longitude": "70.58002000",
+ "wikiDataId": "Q156558"
+ },
+ {
+ "id": 78,
+ "name": "Jurm",
+ "state_id": 3901,
+ "state_code": "BDS",
+ "state_name": "Badakhshan",
+ "country_id": 1,
+ "country_code": "AF",
+ "country_name": "Afghanistan",
+ "latitude": "36.86477000",
+ "longitude": "70.83421000",
+ "wikiDataId": "Q10308323"
+ }
+ ]
+}
+ {
+ "typeahead": {
+ "cities": [
+ {
+ "search_term": "Ashkāsham",
+ "result": {
+ "name": "Ashkāsham",
+ "country": "Afghanistan",
+ "state": "Badakhshan"
+ }
+ },
+ {
+ "search_term": "Fayzabad",
+ "result": {
+ "name": "Fayzabad",
+ "country": "Afghanistan",
+ "state": "Badakhshan"
+ }
+ },
+ {
+ "search_term": "Jurm",
+ "result": {
+ "name": "Jurm",
+ "country": "Afghanistan",
+ "state": "Badakhshan"
+ }
+ }
+ ],
+ "countries": [
+ {
+ "result": {
+ "name": "Afghanistan"
+ },
+ "search_term": "Afghanistan"
+ }
+ ],
+ "states": [
+ {
+ "result": {
+ "country": "Afghanistan",
+ "name": "Badakhshan"
+ },
+ "search_term": "Badakhshan"
+ }
+ ]
+ }
+}
+
+
+
+ Flow_0tn4ed3
+ Flow_0lz96a1
+
+ DataStoreReference_1oronwq
+
+ results = [
+ {
+ "name": "100% Bran",
+ "manufacturer": "Nabisco",
+ "type": "Cold",
+ "calories": 70,
+ "protein": 4,
+ "fat": 1,
+ "sodium": 130,
+ "fiber": 10,
+ "carbo": 5,
+ "sugars": 6,
+ "potass": 280,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 0.33,
+ "rating": 68.402973
+ },
+ {
+ "name": "100% Natural Bran",
+ "manufacturer": "Quaker Oats",
+ "type": "Cold",
+ "calories": 120,
+ "protein": 3,
+ "fat": 5,
+ "sodium": 15,
+ "fiber": 2,
+ "carbo": 8,
+ "sugars": 8,
+ "potass": 135,
+ "vitamins": 0,
+ "weight": 1,
+ "cups": 1,
+ "rating": 33.983679
+ },
+ {
+ "name": "All-Bran",
+ "manufacturer": "Kelloggs",
+ "type": "Cold",
+ "calories": 70,
+ "protein": 4,
+ "fat": 1,
+ "sodium": 260,
+ "fiber": 9,
+ "carbo": 7,
+ "sugars": 5,
+ "potass": 320,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 0.33,
+ "rating": 59.425505
+ },
+ {
+ "name": "All-Bran with Extra Fiber",
+ "manufacturer": "Kelloggs",
+ "type": "Cold",
+ "calories": 50,
+ "protein": 4,
+ "fat": 0,
+ "sodium": 140,
+ "fiber": 14,
+ "carbo": 8,
+ "sugars": 0,
+ "potass": 330,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 0.5,
+ "rating": 93.704912
+ },
+ {
+ "name": "Almond Delight",
+ "manufacturer": "Ralston Purina",
+ "type": "Cold",
+ "calories": 110,
+ "protein": 2,
+ "fat": 2,
+ "sodium": 200,
+ "fiber": 1,
+ "carbo": 14,
+ "sugars": 8,
+ "potass": -1,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 0.75,
+ "rating": 34.384843
+ },
+ {
+ "name": "Apple Cinnamon Cheerios",
+ "manufacturer": "General Mills",
+ "type": "Cold",
+ "calories": 110,
+ "protein": 2,
+ "fat": 2,
+ "sodium": 180,
+ "fiber": 1.5,
+ "carbo": 10.5,
+ "sugars": 10,
+ "potass": 70,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 0.75,
+ "rating": 29.509541
+ },
+ {
+ "name": "Apple Jacks",
+ "manufacturer": "Kelloggs",
+ "type": "Cold",
+ "calories": 110,
+ "protein": 2,
+ "fat": 0,
+ "sodium": 125,
+ "fiber": 1,
+ "carbo": 11,
+ "sugars": 14,
+ "potass": 30,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 1,
+ "rating": 33.174094
+ },
+ {
+ "name": "Basic 4",
+ "manufacturer": "General Mills",
+ "type": "Cold",
+ "calories": 130,
+ "protein": 3,
+ "fat": 2,
+ "sodium": 210,
+ "fiber": 2,
+ "carbo": 18,
+ "sugars": 8,
+ "potass": 100,
+ "vitamins": 25,
+ "weight": 1.33,
+ "cups": 0.75,
+ "rating": 37.038562
+ },
+ {
+ "name": "Bran Chex",
+ "manufacturer": "Ralston Purina",
+ "type": "Cold",
+ "calories": 90,
+ "protein": 2,
+ "fat": 1,
+ "sodium": 200,
+ "fiber": 4,
+ "carbo": 15,
+ "sugars": 6,
+ "potass": 125,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 0.67,
+ "rating": 49.120253
+ },
+ {
+ "name": "Bran Flakes",
+ "manufacturer": "Post",
+ "type": "Cold",
+ "calories": 90,
+ "protein": 3,
+ "fat": 0,
+ "sodium": 210,
+ "fiber": 5,
+ "carbo": 13,
+ "sugars": 5,
+ "potass": 190,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 0.67,
+ "rating": 53.313813
+ },
+ {
+ "name": "Cap'n'Crunch",
+ "manufacturer": "Quaker Oats",
+ "type": "Cold",
+ "calories": 120,
+ "protein": 1,
+ "fat": 2,
+ "sodium": 220,
+ "fiber": 0,
+ "carbo": 12,
+ "sugars": 12,
+ "potass": 35,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 0.75,
+ "rating": 18.042851
+ },
+ {
+ "name": "Cheerios",
+ "manufacturer": "General Mills",
+ "type": "Cold",
+ "calories": 110,
+ "protein": 6,
+ "fat": 2,
+ "sodium": 290,
+ "fiber": 2,
+ "carbo": 17,
+ "sugars": 1,
+ "potass": 105,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 1.25,
+ "rating": 50.764999
+ },
+ {
+ "name": "Cinnamon Toast Crunch",
+ "manufacturer": "General Mills",
+ "type": "Cold",
+ "calories": 120,
+ "protein": 1,
+ "fat": 3,
+ "sodium": 210,
+ "fiber": 0,
+ "carbo": 13,
+ "sugars": 9,
+ "potass": 45,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 0.75,
+ "rating": 19.823573
+ },
+ {
+ "name": "Clusters",
+ "manufacturer": "General Mills",
+ "type": "Cold",
+ "calories": 110,
+ "protein": 3,
+ "fat": 2,
+ "sodium": 140,
+ "fiber": 2,
+ "carbo": 13,
+ "sugars": 7,
+ "potass": 105,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 0.5,
+ "rating": 40.400208
+ },
+ {
+ "name": "Cocoa Puffs",
+ "manufacturer": "General Mills",
+ "type": "Cold",
+ "calories": 110,
+ "protein": 1,
+ "fat": 1,
+ "sodium": 180,
+ "fiber": 0,
+ "carbo": 12,
+ "sugars": 13,
+ "potass": 55,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 1,
+ "rating": 22.736446
+ },
+ {
+ "name": "Corn Chex",
+ "manufacturer": "Ralston Purina",
+ "type": "Cold",
+ "calories": 110,
+ "protein": 2,
+ "fat": 0,
+ "sodium": 280,
+ "fiber": 0,
+ "carbo": 22,
+ "sugars": 3,
+ "potass": 25,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 1,
+ "rating": 41.445019
+ },
+ {
+ "name": "Corn Flakes",
+ "manufacturer": "Kelloggs",
+ "type": "Cold",
+ "calories": 100,
+ "protein": 2,
+ "fat": 0,
+ "sodium": 290,
+ "fiber": 1,
+ "carbo": 21,
+ "sugars": 2,
+ "potass": 35,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 1,
+ "rating": 45.863324
+ },
+ {
+ "name": "Corn Pops",
+ "manufacturer": "Kelloggs",
+ "type": "Cold",
+ "calories": 110,
+ "protein": 1,
+ "fat": 0,
+ "sodium": 90,
+ "fiber": 1,
+ "carbo": 13,
+ "sugars": 12,
+ "potass": 20,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 1,
+ "rating": 35.782791
+ },
+ {
+ "name": "Count Chocula",
+ "manufacturer": "General Mills",
+ "type": "Cold",
+ "calories": 110,
+ "protein": 1,
+ "fat": 1,
+ "sodium": 180,
+ "fiber": 0,
+ "carbo": 12,
+ "sugars": 13,
+ "potass": 65,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 1,
+ "rating": 22.396513
+ },
+ {
+ "name": "Cracklin' Oat Bran",
+ "manufacturer": "Kelloggs",
+ "type": "Cold",
+ "calories": 110,
+ "protein": 3,
+ "fat": 3,
+ "sodium": 140,
+ "fiber": 4,
+ "carbo": 10,
+ "sugars": 7,
+ "potass": 160,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 0.5,
+ "rating": 40.448772
+ },
+ {
+ "name": "Cream of Wheat (Quick)",
+ "manufacturer": "Nabisco",
+ "type": "Hot",
+ "calories": 100,
+ "protein": 3,
+ "fat": 0,
+ "sodium": 80,
+ "fiber": 1,
+ "carbo": 21,
+ "sugars": 0,
+ "potass": -1,
+ "vitamins": 0,
+ "weight": 1,
+ "cups": 1,
+ "rating": 64.533816
+ },
+ {
+ "name": "Crispix",
+ "manufacturer": "Kelloggs",
+ "type": "Cold",
+ "calories": 110,
+ "protein": 2,
+ "fat": 0,
+ "sodium": 220,
+ "fiber": 1,
+ "carbo": 21,
+ "sugars": 3,
+ "potass": 30,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 1,
+ "rating": 46.895644
+ },
+ {
+ "name": "Crispy Wheat & Raisins",
+ "manufacturer": "General Mills",
+ "type": "Cold",
+ "calories": 100,
+ "protein": 2,
+ "fat": 1,
+ "sodium": 140,
+ "fiber": 2,
+ "carbo": 11,
+ "sugars": 10,
+ "potass": 120,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 0.75,
+ "rating": 36.176196
+ },
+ {
+ "name": "Double Chex",
+ "manufacturer": "Ralston Purina",
+ "type": "Cold",
+ "calories": 100,
+ "protein": 2,
+ "fat": 0,
+ "sodium": 190,
+ "fiber": 1,
+ "carbo": 18,
+ "sugars": 5,
+ "potass": 80,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 0.75,
+ "rating": 44.330856
+ },
+ {
+ "name": "Froot Loops",
+ "manufacturer": "Kelloggs",
+ "type": "Cold",
+ "calories": 110,
+ "protein": 2,
+ "fat": 1,
+ "sodium": 125,
+ "fiber": 1,
+ "carbo": 11,
+ "sugars": 13,
+ "potass": 30,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 1,
+ "rating": 32.207582
+ },
+ {
+ "name": "Frosted Flakes",
+ "manufacturer": "Kelloggs",
+ "type": "Cold",
+ "calories": 110,
+ "protein": 1,
+ "fat": 0,
+ "sodium": 200,
+ "fiber": 1,
+ "carbo": 14,
+ "sugars": 11,
+ "potass": 25,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 0.75,
+ "rating": 31.435973
+ },
+ {
+ "name": "Frosted Mini-Wheats",
+ "manufacturer": "Kelloggs",
+ "type": "Cold",
+ "calories": 100,
+ "protein": 3,
+ "fat": 0,
+ "sodium": 0,
+ "fiber": 3,
+ "carbo": 14,
+ "sugars": 7,
+ "potass": 100,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 0.8,
+ "rating": 58.345141
+ },
+ {
+ "name": "Fruit & Fibre Dates; Walnuts; and Oats",
+ "manufacturer": "Post",
+ "type": "Cold",
+ "calories": 120,
+ "protein": 3,
+ "fat": 2,
+ "sodium": 160,
+ "fiber": 5,
+ "carbo": 12,
+ "sugars": 10,
+ "potass": 200,
+ "vitamins": 25,
+ "weight": 1.25,
+ "cups": 0.67,
+ "rating": 40.917047
+ },
+ {
+ "name": "Fruitful Bran",
+ "manufacturer": "Kelloggs",
+ "type": "Cold",
+ "calories": 120,
+ "protein": 3,
+ "fat": 0,
+ "sodium": 240,
+ "fiber": 5,
+ "carbo": 14,
+ "sugars": 12,
+ "potass": 190,
+ "vitamins": 25,
+ "weight": 1.33,
+ "cups": 0.67,
+ "rating": 41.015492
+ },
+ {
+ "name": "Fruity Pebbles",
+ "manufacturer": "Post",
+ "type": "Cold",
+ "calories": 110,
+ "protein": 1,
+ "fat": 1,
+ "sodium": 135,
+ "fiber": 0,
+ "carbo": 13,
+ "sugars": 12,
+ "potass": 25,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 0.75,
+ "rating": 28.025765
+ },
+ {
+ "name": "Golden Crisp",
+ "manufacturer": "Post",
+ "type": "Cold",
+ "calories": 100,
+ "protein": 2,
+ "fat": 0,
+ "sodium": 45,
+ "fiber": 0,
+ "carbo": 11,
+ "sugars": 15,
+ "potass": 40,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 0.88,
+ "rating": 35.252444
+ },
+ {
+ "name": "Golden Grahams",
+ "manufacturer": "General Mills",
+ "type": "Cold",
+ "calories": 110,
+ "protein": 1,
+ "fat": 1,
+ "sodium": 280,
+ "fiber": 0,
+ "carbo": 15,
+ "sugars": 9,
+ "potass": 45,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 0.75,
+ "rating": 23.804043
+ },
+ {
+ "name": "Grape Nuts Flakes",
+ "manufacturer": "Post",
+ "type": "Cold",
+ "calories": 100,
+ "protein": 3,
+ "fat": 1,
+ "sodium": 140,
+ "fiber": 3,
+ "carbo": 15,
+ "sugars": 5,
+ "potass": 85,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 0.88,
+ "rating": 52.076897
+ },
+ {
+ "name": "Grape-Nuts",
+ "manufacturer": "Post",
+ "type": "Cold",
+ "calories": 110,
+ "protein": 3,
+ "fat": 0,
+ "sodium": 170,
+ "fiber": 3,
+ "carbo": 17,
+ "sugars": 3,
+ "potass": 90,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 0.25,
+ "rating": 53.371007
+ },
+ {
+ "name": "Great Grains Pecan",
+ "manufacturer": "Post",
+ "type": "Cold",
+ "calories": 120,
+ "protein": 3,
+ "fat": 3,
+ "sodium": 75,
+ "fiber": 3,
+ "carbo": 13,
+ "sugars": 4,
+ "potass": 100,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 0.33,
+ "rating": 45.811716
+ },
+ {
+ "name": "Honey Graham Ohs",
+ "manufacturer": "Quaker Oats",
+ "type": "Cold",
+ "calories": 120,
+ "protein": 1,
+ "fat": 2,
+ "sodium": 220,
+ "fiber": 1,
+ "carbo": 12,
+ "sugars": 11,
+ "potass": 45,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 1,
+ "rating": 21.871292
+ },
+ {
+ "name": "Honey Nut Cheerios",
+ "manufacturer": "General Mills",
+ "type": "Cold",
+ "calories": 110,
+ "protein": 3,
+ "fat": 1,
+ "sodium": 250,
+ "fiber": 1.5,
+ "carbo": 11.5,
+ "sugars": 10,
+ "potass": 90,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 0.75,
+ "rating": 31.072217
+ },
+ {
+ "name": "Honey-comb",
+ "manufacturer": "Post",
+ "type": "Cold",
+ "calories": 110,
+ "protein": 1,
+ "fat": 0,
+ "sodium": 180,
+ "fiber": 0,
+ "carbo": 14,
+ "sugars": 11,
+ "potass": 35,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 1.33,
+ "rating": 28.742414
+ },
+ {
+ "name": "Just Right Crunchy Nuggets",
+ "manufacturer": "Kelloggs",
+ "type": "Cold",
+ "calories": 110,
+ "protein": 2,
+ "fat": 1,
+ "sodium": 170,
+ "fiber": 1,
+ "carbo": 17,
+ "sugars": 6,
+ "potass": 60,
+ "vitamins": 100,
+ "weight": 1,
+ "cups": 1,
+ "rating": 36.523683
+ },
+ {
+ "name": "Just Right Fruit & Nut",
+ "manufacturer": "Kelloggs",
+ "type": "Cold",
+ "calories": 140,
+ "protein": 3,
+ "fat": 1,
+ "sodium": 170,
+ "fiber": 2,
+ "carbo": 20,
+ "sugars": 9,
+ "potass": 95,
+ "vitamins": 100,
+ "weight": 1.3,
+ "cups": 0.75,
+ "rating": 36.471512
+ },
+ {
+ "name": "Kix",
+ "manufacturer": "General Mills",
+ "type": "Cold",
+ "calories": 110,
+ "protein": 2,
+ "fat": 1,
+ "sodium": 260,
+ "fiber": 0,
+ "carbo": 21,
+ "sugars": 3,
+ "potass": 40,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 1.5,
+ "rating": 39.241114
+ },
+ {
+ "name": "Life",
+ "manufacturer": "Quaker Oats",
+ "type": "Cold",
+ "calories": 100,
+ "protein": 4,
+ "fat": 2,
+ "sodium": 150,
+ "fiber": 2,
+ "carbo": 12,
+ "sugars": 6,
+ "potass": 95,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 0.67,
+ "rating": 45.328074
+ },
+ {
+ "name": "Lucky Charms",
+ "manufacturer": "General Mills",
+ "type": "Cold",
+ "calories": 110,
+ "protein": 2,
+ "fat": 1,
+ "sodium": 180,
+ "fiber": 0,
+ "carbo": 12,
+ "sugars": 12,
+ "potass": 55,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 1,
+ "rating": 26.734515
+ },
+ {
+ "name": "Maypo",
+ "manufacturer": "American Home Food Products",
+ "type": "Hot",
+ "calories": 100,
+ "protein": 4,
+ "fat": 1,
+ "sodium": 0,
+ "fiber": 0,
+ "carbo": 16,
+ "sugars": 3,
+ "potass": 95,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 1,
+ "rating": 54.850917
+ },
+ {
+ "name": "Muesli Raisins; Dates; & Almonds",
+ "manufacturer": "Ralston Purina",
+ "type": "Cold",
+ "calories": 150,
+ "protein": 4,
+ "fat": 3,
+ "sodium": 95,
+ "fiber": 3,
+ "carbo": 16,
+ "sugars": 11,
+ "potass": 170,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 1,
+ "rating": 37.136863
+ },
+ {
+ "name": "Muesli Raisins; Peaches; & Pecans",
+ "manufacturer": "Ralston Purina",
+ "type": "Cold",
+ "calories": 150,
+ "protein": 4,
+ "fat": 3,
+ "sodium": 150,
+ "fiber": 3,
+ "carbo": 16,
+ "sugars": 11,
+ "potass": 170,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 1,
+ "rating": 34.139765
+ },
+ {
+ "name": "Mueslix Crispy Blend",
+ "manufacturer": "Kelloggs",
+ "type": "Cold",
+ "calories": 160,
+ "protein": 3,
+ "fat": 2,
+ "sodium": 150,
+ "fiber": 3,
+ "carbo": 17,
+ "sugars": 13,
+ "potass": 160,
+ "vitamins": 25,
+ "weight": 1.5,
+ "cups": 0.67,
+ "rating": 30.313351
+ },
+ {
+ "name": "Multi-Grain Cheerios",
+ "manufacturer": "General Mills",
+ "type": "Cold",
+ "calories": 100,
+ "protein": 2,
+ "fat": 1,
+ "sodium": 220,
+ "fiber": 2,
+ "carbo": 15,
+ "sugars": 6,
+ "potass": 90,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 1,
+ "rating": 40.105965
+ },
+ {
+ "name": "Nut&Honey Crunch",
+ "manufacturer": "Kelloggs",
+ "type": "Cold",
+ "calories": 120,
+ "protein": 2,
+ "fat": 1,
+ "sodium": 190,
+ "fiber": 0,
+ "carbo": 15,
+ "sugars": 9,
+ "potass": 40,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 0.67,
+ "rating": 29.924285
+ },
+ {
+ "name": "Nutri-Grain Almond-Raisin",
+ "manufacturer": "Kelloggs",
+ "type": "Cold",
+ "calories": 140,
+ "protein": 3,
+ "fat": 2,
+ "sodium": 220,
+ "fiber": 3,
+ "carbo": 21,
+ "sugars": 7,
+ "potass": 130,
+ "vitamins": 25,
+ "weight": 1.33,
+ "cups": 0.67,
+ "rating": 40.69232
+ },
+ {
+ "name": "Nutri-grain Wheat",
+ "manufacturer": "Kelloggs",
+ "type": "Cold",
+ "calories": 90,
+ "protein": 3,
+ "fat": 0,
+ "sodium": 170,
+ "fiber": 3,
+ "carbo": 18,
+ "sugars": 2,
+ "potass": 90,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 1,
+ "rating": 59.642837
+ },
+ {
+ "name": "Oatmeal Raisin Crisp",
+ "manufacturer": "General Mills",
+ "type": "Cold",
+ "calories": 130,
+ "protein": 3,
+ "fat": 2,
+ "sodium": 170,
+ "fiber": 1.5,
+ "carbo": 13.5,
+ "sugars": 10,
+ "potass": 120,
+ "vitamins": 25,
+ "weight": 1.25,
+ "cups": 0.5,
+ "rating": 30.450843
+ },
+ {
+ "name": "Post Nat. Raisin Bran",
+ "manufacturer": "Post",
+ "type": "Cold",
+ "calories": 120,
+ "protein": 3,
+ "fat": 1,
+ "sodium": 200,
+ "fiber": 6,
+ "carbo": 11,
+ "sugars": 14,
+ "potass": 260,
+ "vitamins": 25,
+ "weight": 1.33,
+ "cups": 0.67,
+ "rating": 37.840594
+ },
+ {
+ "name": "Product 19",
+ "manufacturer": "Kelloggs",
+ "type": "Cold",
+ "calories": 100,
+ "protein": 3,
+ "fat": 0,
+ "sodium": 320,
+ "fiber": 1,
+ "carbo": 20,
+ "sugars": 3,
+ "potass": 45,
+ "vitamins": 100,
+ "weight": 1,
+ "cups": 1,
+ "rating": 41.50354
+ },
+ {
+ "name": "Puffed Rice",
+ "manufacturer": "Quaker Oats",
+ "type": "Cold",
+ "calories": 50,
+ "protein": 1,
+ "fat": 0,
+ "sodium": 0,
+ "fiber": 0,
+ "carbo": 13,
+ "sugars": 0,
+ "potass": 15,
+ "vitamins": 0,
+ "weight": 0.5,
+ "cups": 1,
+ "rating": 60.756112
+ },
+ {
+ "name": "Puffed Wheat",
+ "manufacturer": "Quaker Oats",
+ "type": "Cold",
+ "calories": 50,
+ "protein": 2,
+ "fat": 0,
+ "sodium": 0,
+ "fiber": 1,
+ "carbo": 10,
+ "sugars": 0,
+ "potass": 50,
+ "vitamins": 0,
+ "weight": 0.5,
+ "cups": 1,
+ "rating": 63.005645
+ },
+ {
+ "name": "Quaker Oat Squares",
+ "manufacturer": "Quaker Oats",
+ "type": "Cold",
+ "calories": 100,
+ "protein": 4,
+ "fat": 1,
+ "sodium": 135,
+ "fiber": 2,
+ "carbo": 14,
+ "sugars": 6,
+ "potass": 110,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 0.5,
+ "rating": 49.511874
+ },
+ {
+ "name": "Quaker Oatmeal",
+ "manufacturer": "Quaker Oats",
+ "type": "Hot",
+ "calories": 100,
+ "protein": 5,
+ "fat": 2,
+ "sodium": 0,
+ "fiber": 2.7,
+ "carbo": -1,
+ "sugars": -1,
+ "potass": 110,
+ "vitamins": 0,
+ "weight": 1,
+ "cups": 0.67,
+ "rating": 50.828392
+ },
+ {
+ "name": "Raisin Bran",
+ "manufacturer": "Kelloggs",
+ "type": "Cold",
+ "calories": 120,
+ "protein": 3,
+ "fat": 1,
+ "sodium": 210,
+ "fiber": 5,
+ "carbo": 14,
+ "sugars": 12,
+ "potass": 240,
+ "vitamins": 25,
+ "weight": 1.33,
+ "cups": 0.75,
+ "rating": 39.259197
+ },
+ {
+ "name": "Raisin Nut Bran",
+ "manufacturer": "General Mills",
+ "type": "Cold",
+ "calories": 100,
+ "protein": 3,
+ "fat": 2,
+ "sodium": 140,
+ "fiber": 2.5,
+ "carbo": 10.5,
+ "sugars": 8,
+ "potass": 140,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 0.5,
+ "rating": 39.7034
+ },
+ {
+ "name": "Raisin Squares",
+ "manufacturer": "Kelloggs",
+ "type": "Cold",
+ "calories": 90,
+ "protein": 2,
+ "fat": 0,
+ "sodium": 0,
+ "fiber": 2,
+ "carbo": 15,
+ "sugars": 6,
+ "potass": 110,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 0.5,
+ "rating": 55.333142
+ },
+ {
+ "name": "Rice Chex",
+ "manufacturer": "Ralston Purina",
+ "type": "Cold",
+ "calories": 110,
+ "protein": 1,
+ "fat": 0,
+ "sodium": 240,
+ "fiber": 0,
+ "carbo": 23,
+ "sugars": 2,
+ "potass": 30,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 1.13,
+ "rating": 41.998933
+ },
+ {
+ "name": "Rice Krispies",
+ "manufacturer": "Kelloggs",
+ "type": "Cold",
+ "calories": 110,
+ "protein": 2,
+ "fat": 0,
+ "sodium": 290,
+ "fiber": 0,
+ "carbo": 22,
+ "sugars": 3,
+ "potass": 35,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 1,
+ "rating": 40.560159
+ },
+ {
+ "name": "Shredded Wheat",
+ "manufacturer": "Nabisco",
+ "type": "Cold",
+ "calories": 80,
+ "protein": 2,
+ "fat": 0,
+ "sodium": 0,
+ "fiber": 3,
+ "carbo": 16,
+ "sugars": 0,
+ "potass": 95,
+ "vitamins": 0,
+ "weight": 0.83,
+ "cups": 1,
+ "rating": 68.235885
+ },
+ {
+ "name": "Shredded Wheat 'n'Bran",
+ "manufacturer": "Nabisco",
+ "type": "Cold",
+ "calories": 90,
+ "protein": 3,
+ "fat": 0,
+ "sodium": 0,
+ "fiber": 4,
+ "carbo": 19,
+ "sugars": 0,
+ "potass": 140,
+ "vitamins": 0,
+ "weight": 1,
+ "cups": 0.67,
+ "rating": 74.472949
+ },
+ {
+ "name": "Shredded Wheat spoon size",
+ "manufacturer": "Nabisco",
+ "type": "Cold",
+ "calories": 90,
+ "protein": 3,
+ "fat": 0,
+ "sodium": 0,
+ "fiber": 3,
+ "carbo": 20,
+ "sugars": 0,
+ "potass": 120,
+ "vitamins": 0,
+ "weight": 1,
+ "cups": 0.67,
+ "rating": 72.801787
+ },
+ {
+ "name": "Smacks",
+ "manufacturer": "Kelloggs",
+ "type": "Cold",
+ "calories": 110,
+ "protein": 2,
+ "fat": 1,
+ "sodium": 70,
+ "fiber": 1,
+ "carbo": 9,
+ "sugars": 15,
+ "potass": 40,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 0.75,
+ "rating": 31.230054
+ },
+ {
+ "name": "Special K",
+ "manufacturer": "Kelloggs",
+ "type": "Cold",
+ "calories": 110,
+ "protein": 6,
+ "fat": 0,
+ "sodium": 230,
+ "fiber": 1,
+ "carbo": 16,
+ "sugars": 3,
+ "potass": 55,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 1,
+ "rating": 53.131324
+ },
+ {
+ "name": "Strawberry Fruit Wheats",
+ "manufacturer": "Nabisco",
+ "type": "Cold",
+ "calories": 90,
+ "protein": 2,
+ "fat": 0,
+ "sodium": 15,
+ "fiber": 3,
+ "carbo": 15,
+ "sugars": 5,
+ "potass": 90,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 1,
+ "rating": 59.363993
+ },
+ {
+ "name": "Total Corn Flakes",
+ "manufacturer": "General Mills",
+ "type": "Cold",
+ "calories": 110,
+ "protein": 2,
+ "fat": 1,
+ "sodium": 200,
+ "fiber": 0,
+ "carbo": 21,
+ "sugars": 3,
+ "potass": 35,
+ "vitamins": 100,
+ "weight": 1,
+ "cups": 1,
+ "rating": 38.839746
+ },
+ {
+ "name": "Total Raisin Bran",
+ "manufacturer": "General Mills",
+ "type": "Cold",
+ "calories": 140,
+ "protein": 3,
+ "fat": 1,
+ "sodium": 190,
+ "fiber": 4,
+ "carbo": 15,
+ "sugars": 14,
+ "potass": 230,
+ "vitamins": 100,
+ "weight": 1.5,
+ "cups": 1,
+ "rating": 28.592785
+ },
+ {
+ "name": "Total Whole Grain",
+ "manufacturer": "General Mills",
+ "type": "Cold",
+ "calories": 100,
+ "protein": 3,
+ "fat": 1,
+ "sodium": 200,
+ "fiber": 3,
+ "carbo": 16,
+ "sugars": 3,
+ "potass": 110,
+ "vitamins": 100,
+ "weight": 1,
+ "cups": 1,
+ "rating": 46.658844
+ },
+ {
+ "name": "Triples",
+ "manufacturer": "General Mills",
+ "type": "Cold",
+ "calories": 110,
+ "protein": 2,
+ "fat": 1,
+ "sodium": 250,
+ "fiber": 0,
+ "carbo": 21,
+ "sugars": 3,
+ "potass": 60,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 0.75,
+ "rating": 39.106174
+ },
+ {
+ "name": "Trix",
+ "manufacturer": "General Mills",
+ "type": "Cold",
+ "calories": 110,
+ "protein": 1,
+ "fat": 1,
+ "sodium": 140,
+ "fiber": 0,
+ "carbo": 13,
+ "sugars": 12,
+ "potass": 25,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 1,
+ "rating": 27.753301
+ },
+ {
+ "name": "Wheat Chex",
+ "manufacturer": "Ralston Purina",
+ "type": "Cold",
+ "calories": 100,
+ "protein": 3,
+ "fat": 1,
+ "sodium": 230,
+ "fiber": 3,
+ "carbo": 17,
+ "sugars": 3,
+ "potass": 115,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 0.67,
+ "rating": 49.787445
+ },
+ {
+ "name": "Wheaties",
+ "manufacturer": "General Mills",
+ "type": "Cold",
+ "calories": 100,
+ "protein": 3,
+ "fat": 1,
+ "sodium": 200,
+ "fiber": 3,
+ "carbo": 17,
+ "sugars": 3,
+ "potass": 110,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 1,
+ "rating": 51.592193
+ },
+ {
+ "name": "Wheaties Honey Gold",
+ "manufacturer": "General Mills",
+ "type": "Cold",
+ "calories": 110,
+ "protein": 2,
+ "fat": 1,
+ "sodium": 200,
+ "fiber": 1,
+ "carbo": 16,
+ "sugars": 8,
+ "potass": 60,
+ "vitamins": 25,
+ "weight": 1,
+ "cups": 0.75,
+ "rating": 36.187559
+ }
+]
+
+raw_albums = [
+ {
+ "album": "Mama Said Knock You Out",
+ "artist": "LL Cool J",
+ "year": 1990
+ },
+ {
+ "album": "Nowhere",
+ "artist": "Ride",
+ "year": 1990
+ },
+ {
+ "album": "People's Instinctive Paths",
+ "artist": "A Tribe Called Quest",
+ "year": 1990
+ },
+ {
+ "album": "Pills 'n' Thrills 'n' Bellyaches",
+ "artist": "Happy Mondays",
+ "year": 1990
+ },
+ {
+ "album": "Ragged Glory",
+ "artist": "Neil Young",
+ "year": 1990
+ },
+ {
+ "album": "Repeater",
+ "artist": "Fugazi",
+ "year": 1990
+ },
+ {
+ "album": "Ritual De Lo Habitual",
+ "artist": "Jane's Addiction",
+ "year": 1990
+ },
+ {
+ "album": "Rust In Peace",
+ "artist": "Megadeth",
+ "year": 1990
+ },
+ {
+ "album": "Sex Packets",
+ "artist": "Digital Underground",
+ "year": 1990
+ },
+ {
+ "album": "Shake Your Money Maker",
+ "artist": "The Black Crowes",
+ "year": 1990
+ },
+ {
+ "album": "The La's",
+ "artist": "The La's",
+ "year": 1990
+ },
+ {
+ "album": "The White Room",
+ "artist": "KLF",
+ "year": 1990
+ },
+ {
+ "album": "Violator",
+ "artist": "Depeche Mode",
+ "year": 1990
+ },
+ {
+ "album": "World Clique",
+ "artist": "Deee-Lite",
+ "year": 1990
+ },
+ {
+ "album": "Achtung Baby",
+ "artist": "U2",
+ "year": 1991
+ },
+ {
+ "album": "Apocalypse '91 … The Enemy Strikes Black",
+ "artist": "Public Enemy",
+ "year": 1991
+ },
+ {
+ "album": "Arise",
+ "artist": "Sepultura",
+ "year": 1991
+ },
+ {
+ "album": "Bandwagonesque",
+ "artist": "Teenage Fanclub",
+ "year": 1991
+ },
+ {
+ "album": "Blood Sugar Sex Magik",
+ "artist": "Red Hot Chili Peppers",
+ "year": 1991
+ },
+ {
+ "album": "Blue Lines",
+ "artist": "Massive Attack",
+ "year": 1991
+ },
+ {
+ "album": "Cypress Hill",
+ "artist": "Cypress Hill",
+ "year": 1991
+ },
+ {
+ "album": "Every Good Boy Deserves Fudge",
+ "artist": "Mudhoney",
+ "year": 1991
+ },
+ {
+ "album": "Foxbase Alpha",
+ "artist": "Saint Etienne",
+ "year": 1991
+ },
+ {
+ "album": "Haut de Gamme–Koweit Rive Gauche",
+ "artist": "Koffi Olomide",
+ "year": 1991
+ },
+ {
+ "album": "Loveless",
+ "artist": "My Bloody Valentine",
+ "year": 1991
+ },
+ {
+ "album": "Metallica",
+ "artist": "Metallica",
+ "year": 1991
+ },
+ {
+ "album": "Nevermind",
+ "artist": "Nirvana",
+ "year": 1991
+ },
+ {
+ "album": "O.G. Original Gangster",
+ "artist": "Ice-T",
+ "year": 1991
+ },
+ {
+ "album": "Peggy Suicide",
+ "artist": "Julian Cope",
+ "year": 1991
+ },
+ {
+ "album": "Rising Above Bedlam",
+ "artist": "Jah Wobble And the Invaders of the Heart",
+ "year": 1991
+ },
+ {
+ "album": "Screamadelica",
+ "artist": "Primal Scream",
+ "year": 1991
+ },
+ {
+ "album": "Spiderland",
+ "artist": "Slint",
+ "year": 1991
+ },
+ {
+ "album": "Step in the Arena",
+ "artist": "Gang Starr",
+ "year": 1991
+ },
+ {
+ "album": "The Low End Theory",
+ "artist": "A Tribe Called Quest",
+ "year": 1991
+ },
+ {
+ "album": "Woodface",
+ "artist": "Crowded House",
+ "year": 1991
+ },
+ {
+ "album": "A Vulgar Display Of Power",
+ "artist": "Pantera",
+ "year": 1992
+ },
+ {
+ "album": "Automatic for the People",
+ "artist": "REM",
+ "year": 1992
+ },
+ {
+ "album": "Bizarre Ride II The Pharcyde",
+ "artist": "The Pharcyde",
+ "year": 1992
+ },
+ {
+ "album": "Bone Machine",
+ "artist": "Tom Waits",
+ "year": 1992
+ },
+ {
+ "album": "Connected",
+ "artist": "Stereo MC's",
+ "year": 1992
+ },
+ {
+ "album": "Copper Blue",
+ "artist": "Sugar",
+ "year": 1992
+ },
+ {
+ "album": "Devotional Songs",
+ "artist": "Nusrat Fateh Ali Khan & Party",
+ "year": 1992
+ },
+ {
+ "album": "Dirt",
+ "artist": "Alice in Chains",
+ "year": 1992
+ },
+ {
+ "album": "Dirty",
+ "artist": "Sonic Youth",
+ "year": 1992
+ },
+ {
+ "album": "Dry",
+ "artist": "P.J. Harvey",
+ "year": 1992
+ },
+ {
+ "album": "Henry's Dream",
+ "artist": "Nick Cave & the Bad Seeds",
+ "year": 1992
+ },
+ {
+ "album": "Hiphoprisy Is The Greatest Luxury",
+ "artist": "Disposable Heroes of Hiphoprisy",
+ "year": 1992
+ },
+ {
+ "album": "Ingénue",
+ "artist": "k.d. lang",
+ "year": 1992
+ },
+ {
+ "album": "It's A Shame About Ray",
+ "artist": "The Lemonheads",
+ "year": 1992
+ },
+ {
+ "album": "Lam Toro",
+ "artist": "Baaba Maal",
+ "year": 1992
+ },
+ {
+ "album": "Lazer Guided Melodies",
+ "artist": "Spiritualized",
+ "year": 1992
+ },
+ {
+ "album": "Little Earthquakes",
+ "artist": "Tori Amos",
+ "year": 1992
+ },
+ {
+ "album": "Psalm 69",
+ "artist": "Ministry",
+ "year": 1992
+ },
+ {
+ "album": "Selected Ambient Works '85–'92",
+ "artist": "Aphex Twin",
+ "year": 1992
+ },
+ {
+ "album": "Ten",
+ "artist": "Pearl Jam",
+ "year": 1992
+ },
+ {
+ "album": "Three Years, Five Months, And Two Days In The Life Of...",
+ "artist": "Arrested Development",
+ "year": 1992
+ },
+ {
+ "album": "Your Arsenal",
+ "artist": "Morrissey",
+ "year": 1992
+ },
+ {
+ "album": "Aimee Mann",
+ "artist": "Whatever",
+ "year": 1993
+ },
+ {
+ "album": "Bubble & Scrape",
+ "artist": "Sebadoh",
+ "year": 1993
+ },
+ {
+ "album": "Debut",
+ "artist": "Björk",
+ "year": 1993
+ },
+ {
+ "album": "Doggy Style",
+ "artist": "Snoop Doggy Dogg",
+ "year": 1993
+ },
+ {
+ "album": "Emergency on Planet Earth",
+ "artist": "Jamiroquai",
+ "year": 1993
+ },
+ {
+ "album": "Enter The Wu-Tang",
+ "artist": "Wu-Tang Clan",
+ "year": 1993
+ },
+ {
+ "album": "Fuzzy",
+ "artist": "Grant Lee Buffalo",
+ "year": 1993
+ },
+ {
+ "album": "Gentleman",
+ "artist": "The Afghan Whigs",
+ "year": 1993
+ },
+ {
+ "album": "Giant Steps",
+ "artist": "The Boo Radleys",
+ "year": 1993
+ },
+ {
+ "album": "In Utero",
+ "artist": "Nirvana",
+ "year": 1993
+ },
+ {
+ "album": "Modern Life is Rubbish",
+ "artist": "Blur",
+ "year": 1993
+ },
+ {
+ "album": "New Wave",
+ "artist": "Auteurs",
+ "year": 1993
+ },
+ {
+ "album": "Orbital II",
+ "artist": "Orbital",
+ "year": 1993
+ },
+ {
+ "album": "Qui Seme Le Vent Recolte Le Tempo",
+ "artist": "MC Solar",
+ "year": 1993
+ },
+ {
+ "album": "Rage Against The Machine",
+ "artist": "Rage Against The Machine",
+ "year": 1993
+ },
+ {
+ "album": "Rid Of Me",
+ "artist": "P.J. Harvey",
+ "year": 1993
+ },
+ {
+ "album": "Siamese Dream",
+ "artist": "Smashing Pumpkins",
+ "year": 1993
+ },
+ {
+ "album": "Slanted And Enchanted",
+ "artist": "Pavement",
+ "year": 1993
+ },
+ {
+ "album": "Strange Cargo III",
+ "artist": "William Orbit",
+ "year": 1993
+ }
+]
+
+
+cereals = []
+for result in results:
+ cereals.append({"search_term": result["name"], "result": result})
+
+albums = []
+for a in raw_albums:
+ albums.append({"search_term": a["album"], "result": a})
+
+typeahead = {"cereals": cereals, "albums": albums}
+
+del(cereals)
+del(results)
+del(raw_albums)
+del(albums)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_data_stores.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_data_stores.py
new file mode 100644
index 000000000..57216f480
--- /dev/null
+++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_data_stores.py
@@ -0,0 +1,93 @@
+from flask import Flask
+from flask.testing import FlaskClient
+from spiffworkflow_backend.models.typeahead import TypeaheadModel
+from spiffworkflow_backend.models.user import UserModel
+
+from tests.spiffworkflow_backend.helpers.base_test import BaseTest
+
+
+class TestDataStores(BaseTest):
+ def load_data_store(
+ self,
+ app: Flask,
+ client: FlaskClient,
+ with_db_and_bpmn_file_cleanup: None,
+ with_super_admin_user: UserModel,
+ ) -> None:
+ """
+ Populate a datastore with some mock data using a BPMN process that will load information
+ using the typeahead data store. This should add 77 entries to the typeahead table.
+ """
+ process_group_id = "data_stores"
+ process_model_id = "cereals_data_store"
+ bpmn_file_location = "data_stores"
+ process_model = self.create_group_and_model_with_bpmn(
+ client,
+ with_super_admin_user,
+ process_group_id=process_group_id,
+ process_model_id=process_model_id,
+ bpmn_file_location=bpmn_file_location,
+ )
+
+ headers = self.logged_in_headers(with_super_admin_user)
+ response = self.create_process_instance_from_process_model_id_with_api(client, process_model.id, headers)
+ assert response.json is not None
+ process_instance_id = response.json["id"]
+
+ client.post(
+ f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance_id}/run",
+ headers=self.logged_in_headers(with_super_admin_user),
+ )
+
+ def test_create_data_store_populates_db(
+ self,
+ app: Flask,
+ client: FlaskClient,
+ with_db_and_bpmn_file_cleanup: None,
+ with_super_admin_user: UserModel,
+ ) -> None:
+ """Assure that when we run this workflow it will autofill the typeahead data store."""
+ self.load_data_store(app, client, with_db_and_bpmn_file_cleanup, with_super_admin_user)
+ typeaheads = TypeaheadModel.query.all()
+ assert len(typeaheads) == 153
+
+ def test_get_list_of_data_stores(
+ self,
+ app: Flask,
+ client: FlaskClient,
+ with_db_and_bpmn_file_cleanup: None,
+ with_super_admin_user: UserModel,
+ ) -> None:
+ """
+ It should be possible to get a list of the data store categories that are available.
+ """
+ results = client.get("/v1.0/data-stores", headers=self.logged_in_headers(with_super_admin_user))
+ assert results.json == []
+
+ self.load_data_store(app, client, with_db_and_bpmn_file_cleanup, with_super_admin_user)
+ results = client.get("/v1.0/data-stores", headers=self.logged_in_headers(with_super_admin_user))
+ assert results.json == [{"name": "albums", "type": "typeahead"}, {"name": "cereals", "type": "typeahead"}]
+
+ def test_get_data_store_returns_paginated_results(
+ self,
+ app: Flask,
+ client: FlaskClient,
+ with_db_and_bpmn_file_cleanup: None,
+ with_super_admin_user: UserModel,
+ ) -> None:
+ self.load_data_store(app, client, with_db_and_bpmn_file_cleanup, with_super_admin_user)
+ response = client.get(
+ "/v1.0/data-stores/typeahead/albums?per_page=10", headers=self.logged_in_headers(with_super_admin_user)
+ )
+
+ assert response.json is not None
+ assert len(response.json["results"]) == 10
+ assert response.json["pagination"]["count"] == 10
+ assert response.json["pagination"]["total"] == 76
+ assert response.json["pagination"]["pages"] == 8
+ assert response.json["results"][0] == {
+ "search_term": "Mama Said Knock You Out",
+ "year": 1990,
+ "album": "Mama Said Knock You Out",
+ "artist": "LL Cool J",
+ }
diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py
index 60cef8b2e..b6070c616 100644
--- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py
+++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py
@@ -315,6 +315,7 @@ class TestAuthorizationService(BaseTest):
[
("/authentications", "read"),
("/can-run-privileged-script/*", "create"),
+ ("/data-stores/*", "read"),
("/debug/*", "create"),
("/event-error-details/*", "read"),
("/logs/*", "read"),
diff --git a/spiffworkflow-frontend/src/components/DataStoreList.tsx b/spiffworkflow-frontend/src/components/DataStoreList.tsx
new file mode 100644
index 000000000..990a9021c
--- /dev/null
+++ b/spiffworkflow-frontend/src/components/DataStoreList.tsx
@@ -0,0 +1,127 @@
+import { useEffect, useState } from 'react';
+import {
+ Dropdown,
+ Table,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@carbon/react';
+import { TableBody, TableCell } from '@mui/material';
+import { useSearchParams } from 'react-router-dom';
+import HttpService from '../services/HttpService';
+import { DataStore, DataStoreRecords, PaginationObject } from '../interfaces';
+import PaginationForTable from './PaginationForTable';
+import { getPageInfoFromSearchParams } from '../helpers';
+
+export default function DataStoreList() {
+ const [dataStores, setDataStores] = useState([]);
+ const [dataStore, setDataStore] = useState(null);
+ const [pagination, setPagination] = useState(null);
+ const [results, setResults] = useState([]);
+ const [searchParams, setSearchParams] = useSearchParams();
+
+ useEffect(() => {
+ HttpService.makeCallToBackend({
+ path: `/data-stores`,
+ successCallback: (newStores: DataStore[]) => {
+ setDataStores(newStores);
+ },
+ });
+ }, []); // Do this once so we have a list of data stores to select from.
+
+ useEffect(() => {
+ const { page, perPage } = getPageInfoFromSearchParams(
+ searchParams,
+ 10,
+ 1,
+ 'datastore'
+ );
+ console.log();
+ const dataStoreType = searchParams.get('type') || '';
+ const dataStoreName = searchParams.get('name') || '';
+
+ if (dataStoreType === '' || dataStoreName === '') {
+ return;
+ }
+ if (dataStores && dataStoreName && dataStoreType) {
+ dataStores.forEach((ds) => {
+ if (ds.name === dataStoreName && ds.type === dataStoreType) {
+ setDataStore(ds);
+ }
+ });
+ }
+ const queryParamString = `per_page=${perPage}&page=${page}`;
+ HttpService.makeCallToBackend({
+ path: `/data-stores/${dataStoreType}/${dataStoreName}?${queryParamString}`,
+ successCallback: (response: DataStoreRecords) => {
+ setResults(response.results);
+ setPagination(response.pagination);
+ },
+ });
+ }, [dataStores, searchParams]);
+
+ const getTable = () => {
+ if (results.length === 0) {
+ return null;
+ }
+ const firstResult = results[0];
+ console.log('Results', results);
+ const tableHeaders: any[] = [];
+ const keys = Object.keys(firstResult);
+ keys.forEach((key) => tableHeaders.push({key}));
+
+ return (
+
+
+ {tableHeaders}
+
+
+ {results.map((object) => {
+ return (
+
+ {keys.map((key) => {
+ return {object[key]};
+ })}
+
+ );
+ })}
+
+
+ );
+ };
+
+ const { page, perPage } = getPageInfoFromSearchParams(
+ searchParams,
+ 10,
+ 1,
+ 'datastore'
+ );
+ return (
+ <>
+ (ds ? `${ds.name} (${ds.type})` : '')}
+ onChange={(event: any) => {
+ setDataStore(event.selectedItem);
+ searchParams.set('datastore_page', '1');
+ searchParams.set('datastore_per_page', '10');
+ searchParams.set('type', event.selectedItem.type);
+ searchParams.set('name', event.selectedItem.name);
+ setSearchParams(searchParams);
+ }}
+ />
+
+ >
+ );
+}
diff --git a/spiffworkflow-frontend/src/components/NavigationBar.tsx b/spiffworkflow-frontend/src/components/NavigationBar.tsx
index 26a75b3ba..243149326 100644
--- a/spiffworkflow-frontend/src/components/NavigationBar.tsx
+++ b/spiffworkflow-frontend/src/components/NavigationBar.tsx
@@ -52,6 +52,7 @@ export default function NavigationBar() {
[targetUris.authenticationListPath]: ['GET'],
[targetUris.messageInstanceListPath]: ['GET'],
[targetUris.secretListPath]: ['GET'],
+ [targetUris.dataStoreListPath]: ['GET'],
};
const { ability } = usePermissionFetcher(permissionRequestData);
@@ -76,6 +77,8 @@ export default function NavigationBar() {
newActiveKey = '/admin/process-instances';
} else if (location.pathname.match(/^\/admin\/configuration\b/)) {
newActiveKey = '/admin/configuration';
+ } else if (location.pathname.match(/^\/admin\/datastore\b/)) {
+ newActiveKey = '/admin/datastore';
} else if (location.pathname === '/') {
newActiveKey = '/';
} else if (location.pathname.match(/^\/tasks\b/)) {
@@ -228,6 +231,14 @@ export default function NavigationBar() {
Messages
+
+
+ Data Stores
+
+
{configurationElement()}
>
);
diff --git a/spiffworkflow-frontend/src/components/PaginationForTable.tsx b/spiffworkflow-frontend/src/components/PaginationForTable.tsx
index 1c86b703a..2544589a5 100644
--- a/spiffworkflow-frontend/src/components/PaginationForTable.tsx
+++ b/spiffworkflow-frontend/src/components/PaginationForTable.tsx
@@ -44,6 +44,17 @@ export default function PaginationForTable({
};
if (pagination) {
+ const maxPages = 1000;
+ const pagesUnknown = pagination.pages > maxPages;
+ const totalItems =
+ pagination.pages < maxPages ? pagination.total : maxPages * perPage;
+ const itemText = () => {
+ const start = (page - 1) * perPage + 1;
+ return `Items ${start} to ${start + pagination.count} of ${
+ pagination.total
+ }`;
+ };
+
return (
<>
{tableToDisplay}
@@ -55,10 +66,12 @@ export default function PaginationForTable({
itemsPerPageText="Items per page:"
page={page}
pageNumberText="Page Number"
+ itemText={itemText}
pageSize={perPage}
pageSizes={perPageOptions || PER_PAGE_OPTIONS}
- totalItems={pagination.total}
+ totalItems={totalItems}
onChange={updateRows}
+ pagesUnknown={pagesUnknown}
/>
>
);
diff --git a/spiffworkflow-frontend/src/hooks/UriListForPermissions.tsx b/spiffworkflow-frontend/src/hooks/UriListForPermissions.tsx
index dfce135d0..8b35755c2 100644
--- a/spiffworkflow-frontend/src/hooks/UriListForPermissions.tsx
+++ b/spiffworkflow-frontend/src/hooks/UriListForPermissions.tsx
@@ -7,6 +7,7 @@ export const useUriListForPermissions = () => {
return {
authenticationListPath: `/v1.0/authentications`,
messageInstanceListPath: '/v1.0/messages',
+ dataStoreListPath: '/v1.0/data-stores',
processGroupListPath: '/v1.0/process-groups',
processGroupShowPath: `/v1.0/process-groups/${params.process_group_id}`,
processInstanceActionPath: `/v1.0/process-instances/${params.process_model_id}/${params.process_instance_id}`,
diff --git a/spiffworkflow-frontend/src/interfaces.ts b/spiffworkflow-frontend/src/interfaces.ts
index d6a28d5bc..952ab49cb 100644
--- a/spiffworkflow-frontend/src/interfaces.ts
+++ b/spiffworkflow-frontend/src/interfaces.ts
@@ -393,3 +393,13 @@ export interface TestCaseResults {
failing: TestCaseResult[];
passing: TestCaseResult[];
}
+
+export interface DataStoreRecords {
+ results: any[];
+ pagination: PaginationObject;
+}
+
+export interface DataStore {
+ name: string;
+ type: string;
+}
diff --git a/spiffworkflow-frontend/src/routes/AdminRoutes.tsx b/spiffworkflow-frontend/src/routes/AdminRoutes.tsx
index f42a7de6e..b60eb717e 100644
--- a/spiffworkflow-frontend/src/routes/AdminRoutes.tsx
+++ b/spiffworkflow-frontend/src/routes/AdminRoutes.tsx
@@ -21,6 +21,7 @@ import ProcessModelNewExperimental from './ProcessModelNewExperimental';
import ProcessInstanceFindById from './ProcessInstanceFindById';
import ProcessInterstitialPage from './ProcessInterstitialPage';
import MessageListPage from './MessageListPage';
+import DataStorePage from './DataStorePage';
export default function AdminRoutes() {
const location = useLocation();
@@ -125,6 +126,7 @@ export default function AdminRoutes() {
element={}
/>
} />
+ } />
);
diff --git a/spiffworkflow-frontend/src/routes/DataStorePage.tsx b/spiffworkflow-frontend/src/routes/DataStorePage.tsx
new file mode 100644
index 000000000..7d04732b2
--- /dev/null
+++ b/spiffworkflow-frontend/src/routes/DataStorePage.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+import DataStoreList from '../components/DataStoreList';
+
+export default function DataStorePage() {
+ return (
+ <>
+ Data Stores
+
+ >
+ );
+}