Orchestr is laioutrs API layer which is used to provide queries and mutations. This document describes the query-part of the api.
At the core of the query-system are entities, which can represent various elements within an online shop, such as products, categories or users. Each entity is an object, defined by:
Orchestr provides a set of canonical entities, which are entities with a set of predefined canoncial components. These canonical entities streamline development by offering a standard structure for commonly used entity types like products. However, developers are also free to attach non-canonical components to these entities or providing custom non-canonical entites, enabling further customization and extensibility of the data model.
Orchestr's API allows developers to query for entities using a highly flexible structure. Developers can specify not only the entity types they want to retrieve but also the exact components they need for each entity type. This reduces over-fetching and improves performance, as only relevant data is returned.
// EntityComponentToken
export default defineEntityComponentToken('LtrBase', {
entityType: 'LtrProduct',
// Has to be a zod-schema so we have type-safety in query-resolvers and are able to generate a typescript-type in the build-process.
// Also ideal for reflections in the studio.
schema: z.object({
name: z.string(),
sku: z.string(),
}),
});
// QueryHandlerDefinition
export default defineQueryHandler({
label: 'Shopware PLP',
query: 'ltrProductsByCategory',
arguments: {
// LaioutrFieldsetDefinition
},
});
// ComponentResolver
{
"label": "Shopware Product Connector",
"entityType": "LtrProduct",
"provides": ["LtrBase", "LtrAvailability", "LtrPrice"]
}
// other ComponentResolver
import LtrRating from "../components/LtrRating";
export default defineComponentResolver({
label: "reviews.io Product Rating Connector",
entityType: "LtrProduct",
provides: [LtrRating],
resolve: async ({ entityId, requestedComponents, arguments, context }) => {
// ...
}
});
// LinkHandler
export default defineLinkHandler({
label: 'Prudsys Product Recommendation Connector',
linkName: 'LtrCrossAlsoBought',
sourceEntityType: 'LtrProduct',
targetEntityType: 'LtrProduct',
arguments: {
// LaioutrFieldsetDefinition
},
});
// other LinkHandler
export default defineLinkHandler({
label: 'reviews.io Product Review Connector',
linkName: 'LtrReviews',
sourceEntityType: 'LtrProduct',
targetEntityType: 'LtrProductReview',
arguments: {
// LaioutrFieldsetDefinition
},
});
This is the internal reflection api which is used to fetch the type-graph. It is also used by the studio to render the fieldset for the query.
// (Orchestr)QueryTypeGraph
{
"query": "ltrProductsByCategory",
"label": "Shopware PLP",
"arguments": {
// LaioutrFieldsetDefinition
// Must include complete fieldset-definition as this will also be used in studio to render the fieldset
},
"entities": {
"LtrProduct": {
"components": ["LtrBase", "LtrAvailability", "LtrPrice", "LtrRating"],
"links": {
"LtrReviews": {
"entity": "LtrProductReview"
},
"LtrCrossAlsoBought": {
"entity": "LtrProduct"
},
"LtrCrossSimilar": {
"entity": "LtrProduct"
}
}
},
"LtrProductReview": {
"components": ["LtrBase"]
}
}
}
// (Laioutr)BlockDefinition
{
"label": "Product Details",
"component": "Laioutr Product Details",
"schema": [
{
"label": "Product Data",
"fields": [
{
"label": "Source",
"type": "query",
"name": "products",
"entityType": "LtrProduct",
// The block has to define what it needs from the entity
"fetch": [
"LtrBase",
"LtrAvailability",
"LtrPrice",
"LtrRating",
["LtrReviews", { "fetch": ["LtrBase"] }],
["LtrCrossAlsoBought", { "fetch": ["LtrBase", "LtrPrice"] }],
["LtrCrossSimilar", { "fetch": ["LtrBase"] }]
]
}
]
}
]
}
A RcQuery is the configuration of a single query in the laioutrrc.json. The block-configuration itself only contains a link to the RcQuery via its id.
// (Orchestr)RcQuery
[
{
"id": "cp17r0j24ts002324tv1",
"urlAlias": "_",
"queryName": "ltrProductsByCategory",
"arguments": {
"categoryId": {
"type": "LtrShopwareSeoResolver",
"arguments": {
"prefix": "c/"
}
}
},
"fetchConfiguration": {
"LtrReviews": {
"urlAlias": "reviews",
"pagination": {
"offset": 0,
"limit": 10
}
}
}
}
]
URL Alias will be used to prefix filter, sorting and pagination query-params. Also works for linked queries like reviews for products here:
// PLP, filter, sorting and pagination
// https://demo.laioutr.com/c/deko-wohnen?_[filter][color]=123&_[sort]=popularity:desc&_[p]=1
// NestedSearchParams
=> {
"_": {
"filter": {
"color": 123
},
"sort": "popularity:desc",
"p": 1
}
}
// PDP, page 2 of reviews (linked query)
// https://demo.laioutr.com/p/deko-figur-dackel-dario?_[0190bad79ec17d60a77caa86beaf7e04][reviews][p]=2
type QueryWireRequestQuery = {
id: RcQuery['id'];
queryName: RcQuery['queryName'];
arguments: ResolveRcQueryArguments<RcQuery['arguments']>;
sort: UrlQueryArguments['sort'];
pagination: UrlQueryArguments['pagination'];
filter: UrlQueryArguments['filter'];
components: LaioutrFieldDefinitionQuery['components'];
links: LaioutrFieldDefinitionQuery['links'];
};
// (Orchestr)QueryWireResponse
{
"results": {
"ltrProductsByCategory:cp17r0j24ts002324tv1": {
"type": "EntityCollection",
"status": "ok",
"errors": [
{
"message": "Not found",
"code": "EENTITYNOTFOUND",
"path": null
}
],
"urlAlias": "_",
"urlSearchParam": "_",
"entityTotal": 73,
"availableSortings": [
"popularity:asc",
"price:asc",
"price:desc",
"releaseDate:desc",
"properties.storageSpace:desc",
"properties.storageSpace:asc"
],
"availableFilters": [],
"entities": ["LtrProduct:0190bad79ec17d60a77caa86beaf7e04"]
}
},
"entities": {
"LtrProduct:0190bad79ec17d60a77caa86beaf7e04": {
"uri": "LtrProduct:0190bad79ec17d60a77caa86beaf7e04",
"id": "0190bad79ec17d60a77caa86beaf7e04",
"type": "LtrProduct",
"components": {
"LtrBase": {
"name": "Deko-Figur Dackel Dario, magnetisch",
"sku": "UGI0075240"
},
"LtrAvailability": {
"stock": 10,
"deliveryTime": {
"min": 1,
"max": 3,
"unit": "days",
"label": "1 - 3 Tage"
}
},
"LtrPrice": {
"price": 899,
"priceNet": 840,
"orgPrice": 1299,
"orgPriceNet": 1214
},
"LtrRating": {
"value": 4,
"count": 250
}
}
}
}
}
// Returned in typescript when using the data for the query `ltrProductsByCategory`
// EntityCollections are marked as loading if not all components or linked queries have been fully loaded.
// OrchestrEntityCollection / OrchestrEntityCollectionLoading? / OrchestrLazyQueryResponse
{
"type": "EntityCollection",
"status": "ok",
"isLoading": true,
"errors": [],
"urlAlias": "_",
"urlSearchParam": "_",
"pagination": {},
"entities": [
{
"uri": "LtrProduct:0190bad79ec17d60a77caa86beaf7e04",
"id": "0190bad79ec17d60a77caa86beaf7e04",
"type": "LtrProduct",
"components": {
"LtrBase": {
"isLoading": false,
"name": "Deko-Figur Dackel Dario, magnetisch",
"sku": "UGI0075240"
},
"LtrAvailability": {
"isLoading": true
}
}
}
]
}
There is room for improvement in the following scenario:
{
"nonce": "asd",
"entities": "34Ad221=="
}
A possible solution might be the assigning of short-ids to the entities themselves. See Composite Map Keys in JavaScript with Bitsets.
Every time the server sends entities to the client, the server will store the sha256(sort(unique(componentUris))) in the loadedEntitiesCacheId field which will be sent to the client. The server uses this cache-id to set a key in the kv-storage, where the value is the original sort(unique(componentUris)). Now when the client makes a new request we can use those componentUris to only send the client the entities/components it doesn't know about.