Back to Arky

Categories

A store's classifications with filter fields, which products, booking services and resources answer

A category is a classification with filter fields, defined once per store. A fabric shop’s fabrics category has the fields composition and weight; a fabric product carries its own answers, such as composition silk and weight light, and storefronts filter by them. Products, booking services and booking resources keep their answers inside themselves, in categories: CategoryEntry[].

import type { EpochMilliseconds } from "arky-sdk";

interface Category {
	id: string;
	store_id: string;
	key: string;
	parent_id: string | null;
	slugs: Record<string, string>;
	blocks: Block[];
	schema: CategorySchema[];
	status: { type: "active" } | { type: "draft" } | { type: "archived" } | { type: "deleting" };
	created_at: EpochMilliseconds;
	updated_at: EpochMilliseconds;
}

type CategorySchema =
	| { type: "select_one"; id: string; key: string; label: LocalizedText; options: { key: string; label: LocalizedText }[] }
	| { type: "select_many"; id: string; key: string; label: LocalizedText; options: { key: string; label: LocalizedText }[] }
	| { type: "number"; id: string; key: string; label: LocalizedText; min: number | null; max: number | null }
	| { type: "boolean"; id: string; key: string; label: LocalizedText }
	| { type: "geo_location"; id: string; key: string; label: LocalizedText };

interface CategoryEntry {
	category_id: string;
	fields: (
		| { type: "select_one"; field_id: string; option_key: string }
		| { type: "select_many"; field_id: string; option_keys: string[] }
		| { type: "number"; field_id: string; value: number }
		| { type: "boolean"; field_id: string; value: boolean }
		| { type: "geo_location"; field_id: string; value: { lat: number; lon: number } }
	)[];
}

slugs and blocks give the category its URL, name, description and picture per language. Options have a key and a label per language; owners store the option keys, so renaming a label never leaves an answer stale.

Admin

POST /v1/stores/{store_id}/categories
SDK: sdk.category.create()
GET /v1/stores/{store_id}/categories
SDK: sdk.category.find()
GET /v1/stores/{store_id}/categories/{id}
SDK: sdk.category.get()
GET /v1/stores/{store_id}/categories/{id}/children
SDK: sdk.category.findChildren()
PUT /v1/stores/{store_id}/categories/{id}
SDK: sdk.category.update()
DELETE /v1/stores/{store_id}/categories/{id}?expected_updated_at=
SDK: sdk.category.delete()
const fabrics = await sdk.category.create({
	store_id: storeId,
	id: crypto.randomUUID(),
	key: "fabrics",
	parent_id: null,
	slugs: { en: "fabrics", de: "stoffe" },
	blocks: [
		{ id: crypto.randomUUID(), key: "name", type: "localized_text", value: { en: "Fabrics", de: "Stoffe" } },
	],
	schema: [
		{
			type: "select_one",
			id: crypto.randomUUID(),
			key: "composition",
			label: { en: "Composition", de: "Zusammensetzung" },
			options: [
				{ key: "silk", label: { en: "Silk", de: "Seide" } },
				{ key: "linen", label: { en: "Linen", de: "Leinen" } },
			],
		},
		{ type: "number", id: crypto.randomUUID(), key: "weight", label: { en: "Weight (g/m²)" }, min: 0, max: null },
	],
});

A product answers with an entry:

const entry: CategoryEntry = {
	category_id: fabrics.id,
	fields: [
		{ type: "select_one", field_id: compositionFieldId, option_key: "silk" },
		{ type: "number", field_id: weightFieldId, value: 90 },
	],
};
  • An owner holds at most one entry per category, of an active category of the same store. Each answer names its schema field by field_id and matches its type; a select_one holds one option key and a select_many one or more. An owner save that leaves out categories keeps its entries.
  • The key is unique in the store (409 CATEGORY.KEY_TAKEN) and the slugs per store and language (409 CATEGORY.SLUG_TAKEN). A parent is a category of the same store that isn’t archived, and the hierarchy has no cycles.
  • Option keys are unique within a field.
  • New stores start with no categories.

Changing A Category

A field keeps its type for good: changing the type under the same field id is refused. Its key, label, options and bounds may change, and fields may be added or removed. Answers that no longer fit, such as an option that was removed or a number now out of bounds, stay on their owner: Admin flags them, storefront reads and filters leave them out, and the owner’s next save that changes anything but its status is refused until they fit.

Deleting

Deleting is refused while the category has child categories (409 CATEGORY.HAS_CHILDREN), or while anything but products, booking services, booking resources and promotions points at it (409 REFERENCE.STILL_REFERENCED). Otherwise the category becomes deleting, Arky removes its entries from those owners and its id from promotion targets in batches, and then removes the record. While it is deleting, nothing new can point at it.

Webhooks post category.created, category.updated and category.deleted.

Storefront

GET /v1/storefront/categories/{id_or_slug}
SDK: arky.category.get()
GET /v1/storefront/categories/by-key/{key}
SDK: arky.category.getByKey()
GET /v1/storefront/categories/{id}/children
SDK: arky.category.findChildren()
const category = await arky.category.getByKey({ key: "fabrics" });
const bySlug = await arky.category.get({ slug: "fabrics" });
const children = await arky.category.findChildren({ id: category.id, limit: 50 });

const silk = await arky.eshop.product.find({
	catalog_id: catalog.id,
	category_query: [
		{ category_id: category.id, query: [{ type: "select", key: "composition", option_keys: ["silk"] }] },
	],
});
Note

On the storefront, get takes an id or a slug, and reads a slug in the client’s language (X-Arky-Locale) only: a miss answers 404, and is never retried as a key. getByKey reads a category by its key, in any language. Products and booking services are read the same way. Filtering happens on the owners’ lists, such as products, with a category_query. A filter names a field by its key; Arky matches the answers by that field’s id.