# Get content by IDs, or handles Source: https://docs.enterspeed.com/api-reference/delivery/get-content-by-ids-or-handles /enterspeed/enterspeedv1Api.yaml post /v2 The Post version of the Delivery API is used to fetch many view by IDs or handles. *Note: The maximum handles and ids combined in one request is limited to 1000* # Get content by URL, IDs, or handles Source: https://docs.enterspeed.com/api-reference/delivery/get-content-by-url-ids-or-handles /enterspeed/enterspeedv1Api.yaml get /v2 Used to fetch views by a single URL and/or multiple IDs or handles in a single request. *Note: The maximum url length is 4096 characters* # Delete entities Source: https://docs.enterspeed.com/api-reference/ingest/delete-entities /enterspeed/enterspeedv1Api.yaml delete /ingest/v2 Starting the process for deleting entities. *Note: The bulk endpoint can take up to 50 entities in a single request.* # Delete entity Source: https://docs.enterspeed.com/api-reference/ingest/delete-entity /enterspeed/enterspeedv1Api.yaml delete /ingest/v2/{originId} Starting the process for deleting the entity. # Save entities Source: https://docs.enterspeed.com/api-reference/ingest/save-entities /enterspeed/enterspeedv1Api.yaml post /ingest/v2 Starting the process for saving entities. *Note: The bulk endpoint can take up to 50 entities in a single request.* *Note: The total size of the request body must not exceed 200 MB.* # Save entity Source: https://docs.enterspeed.com/api-reference/ingest/save-entity /enterspeed/enterspeedv1Api.yaml post /ingest/v2/{originId} Starting the process for saving the entity. This endpoint can be used in two ways: - **Raw**: The body contains the raw source entity and the required meta data is provided as header parameters. - **Simplified**: The source entity is given `properties` of the body which also includes meta data. # Multi query items Source: https://docs.enterspeed.com/api-reference/query/multi-query-items /enterspeed/enterspeedv1Api.yaml post /v1 The Query API is used to query items in an index, with this endpoint up to 5 queries can be requested in a single payload, each query defining the index to perform the lookup in. # Multi query items from Auto Indexing source groups Source: https://docs.enterspeed.com/api-reference/query/multi-query-items-from-auto-indexing-source-groups /enterspeed/enterspeedv1Api.yaml post /v1/source Note: This endpoint is currently in preview The Query API is used to query items in an index, this endpoint lets you query items ingested to Auto Indexing source groups with up to 5 queries requested in a single payload, each query defining the source groups alias and source entity type to perform the lookup in. # Query items Source: https://docs.enterspeed.com/api-reference/query/query-items /enterspeed/enterspeedv1Api.yaml post /v1/{indexAlias} The Query API is used to query items in an index. # Query items from Auto Indexing source groups Source: https://docs.enterspeed.com/api-reference/query/query-items-from-auto-indexing-source-groups /enterspeed/enterspeedv1Api.yaml post /v1/sourceGroupAlias/{sourceGroupAlias}/type/{sourceEntityType} Note: This endpoint is currently in preview The Query API is used to query items in an index, this endpoint lets you query items ingested to Auto Indexing source groups. # Get Routes Source: https://docs.enterspeed.com/api-reference/routes/get-routes /enterspeed/enterspeedv1Api.yaml get /routes/v1 Get all the routes for a specific environment. # Get Routes v2 Source: https://docs.enterspeed.com/api-reference/routes/get-routes-v2 /enterspeed/enterspeedv1Api.yaml get /routes/v2 Get all the routes for a specific environment with continuation token. # Enterspeed API Source: https://docs.enterspeed.com/enterspeed/api/overview The Enterspeed API are based on REST principles. ## OpenAPI specification Download the OpenAPI specification for the Enterspeed API. ## API keys The API uses two types of API keys. One for reading data and one for writing data. * **Reading** - The environment client API key is used for reading data from the Delivery API, Query API and Routes API. The key is sent in the request header as `X-Api-Key`. * **Writing** - The data source API key is used for writing data to the Ingest API. The key is sent in the request header as `X-Api-Key`. # Indexes Source: https://docs.enterspeed.com/enterspeed/best-practices/indexes ### Only index the fields you need Basicly, the more fields you index, the more space the index will take and the longer it takes to index all the items. It also impacts the performance when you query an index with a lot of fields. In general, this means that you should index as few fields as possible. As a rule of thumb you should not index more than 50 fields. You can use the schema [`alias` query feature](/api-reference/query/query-items) to return views for presentation data as part of your query result so you only index the fields you need for filtering and sorting. ### Prefer `keyword` over `text` (unless you need the power of the `text` type) From the name of the types, it's easy to think that every time you need to index a string, you should use the `text` type. However we also have the `keyword` type that also works on strings and it's important to know the difference. #### text When using the `text` type, the string value is analyzed when an item is inserted into the index. This means it takes a bit longer to index and query the field, but it brings a lot of power as well. Analyzed means that the string value is split into tokens and stored in the index. This makes it possible to do fuzzy search using the `search` property in the [Query API](/api-reference/query/query-items). #### keyword When using the `keyword` type, the string value is not analyzed but indexed as is. This makes it faster and suitable when you just need to filter, sort, build facets or use the field for display on the frontend. # Ingest integrations Source: https://docs.enterspeed.com/enterspeed/best-practices/ingest-integrations ### One source group per system Before you can ingest data (source entities) into Enterspeed, you need to create a source group to store the data. In general, you want to create a source group for every source system that you're ingesting data from. So basically, that's one source group for your CMS data, one source group for you PIM data, one source group for you ERP data, and so on. Within each source group you can then have multiple sources, typically one per environment, or two per environment if you have both published and preview data which is often used in CMS's. Names for source groups and sources could be something like: * CMS * \[PROD] Preview * \[PROD] Published * \[DEV] Preview * \[DEV] Published * PIM * \[PROD] Data * \[DEV] Data ### Only ingest your source entities when fully updated When you ingest source entities, it's important that you don't do multiple "partial" ingests, such as ingesting a single source entity every time a single field is updated in the source system.\ This could be if your source system auto saves and sends a save event everytime an editor changes a single field, instead of when the editor is done updating all necessary fields and clicks save. Doing multiple ingests for a single source entity, one for every single property change, will create multiple unnecessary view generations. And that can result in larger job queues and delay the final view generation. Instead, make sure to only ingest source entities one time once they're fully updated. ### Exclude unused properties that change often In general, you can ingest all your properties for a source entity type to Enterspeed - and then only map the properties you actually need in the schemas. Enterspeed will even detect if you're ingesting a source entity with no changes since the last ingested version of the source entity, and will not start processing a new view. However, if some of your properties change often and you don't use these properties, you shouldn't ingest them. If you do it'll generate new views on every ingest even though the views aren't changing. This could be if you have a stock count property that change every time a customer buys a product - but you either aren't using that stock count or perhaps only use it to return a boolean such as "true" if the stock count is larger than zero or "false" if it's zero. If that's the case, Enterspeed will regenerate the view every time the stock count changes from 100 to 99 to 98 to 97 and so on with the same result. In this case you should ingest the boolean value instead of the stock count. You can use the Management App's log function and filter by Ingest API to see exactly how many ingest requests you're doing. Create source Enterspeed ### Don't delete all source entities on every import run When running import jobs, you should not start by deleting all source entities and then reingesting them again. First off, it means that you'll delete all your views and they'll stay missing until all source entities has been reingested and all views has been processed again. Secondly, Enterspeed has a feature to only reprocess views if the source entities have changed. So by deleting all source entities and reingesting them, this feature of course doesn't work - and that can cause an extensive amount of extra jobs and view generations. If you don't get delete events from your backend system so you can delete source entities right away, you should keep a list of previous ingested source entities, so you can compare which source entities you need to delete in the next run. ### Don't duplicate information on multiple source entity types When you ingest data to Enterspeed, you should avoid ducplicated information across source entity types. E.g. if you have a product source entity and a price source entity, the sales price should not be ingested as property on both of them. If you have duplicated data, you need to ingest both source entities every time the sales price changes and then Enterspeed needs to process extra views. Enterspeed has features like [references](/enterspeed/reference/js/full-schema/properties#reference) and [lookup](/enterspeed/reference/js/full-schema/properties#lookup) that lets you use data from other source entities in the schemas, so use those features instead. # Overview Source: https://docs.enterspeed.com/enterspeed/best-practices/overview # Best Practices On these pages we have collected some best practices to help you get the best result and performance out of Enterspeed. Please read the pages as most of the points are applicable to every Enterspeed project - and as always, you are more than welcome to reach out if you have any questions. Ingesting data from your own sources, like CMS, PIM, ERP and so on, into Enterspeed Creating and updating views and routes for your frontend application Optimizing indexes used by the Query API # Schemas Source: https://docs.enterspeed.com/enterspeed/best-practices/schemas ### Make your reprocess actions as specific as possible Reprocess actions can cause execessive processing, especially if you reprocess more than needed. This could result in a larger queue of jobs, and it will take longer for all your views to be updated. Because of that, it's important to make your reprocess actions as precise as possible, by using `originId` or a precise `filter` so you only target the schemas and source entities you actually need to reprocess. Read more about [reprocessing](/enterspeed/key-concepts/reprocessing.md). ### Exclude properties from lookup if you don't need them By default when you do a [lookup](/enterspeed/reference/js/full-schema/properties#lookup) in a schema, the entire source entities are loaded from storage, including the `properties` object. ```json title="Full source entity including all custom properties" theme={null} { "sourceId": "gid://Source/e3211d99-e496-4e11-af3d-328eb543619f", "id": "gid://Source/e3211d99-e496-4e11-af3d-328eb543619f/Entity/1098-en-us", "type": "home", "originId": "1098-en-us", "originParentId": null, "url": "https://fairy-tales.com/", "updatedAt": "2023-07-18T08:20:41.8162846Z", "redirects": [], "properties": { "title": "Fairy tales by H. C. Andersen", "text": "Read the wonderful fairy tales from the danish author and poet Hans Christian Andersen. Classic tales like \"The Emperor's New Clothes\", \"The Snow Queen\" and many more.", ... } } ``` But in some cases you don't need all the custom properties, only some of the base fields like `originId`, `originParentId`, or `url`. In these cases it's important to specify that in the `lookup`, as this will improve performance and lower the amount of time it takes to generate views. You do that by setting `excludeProperties` to `true` in the `lookupOptions` parameter. ```js title="lookup with excluded custom properties" theme={null} properties: async function (sourceEntity, context) { const fairyTales = await context .lookup("type eq 'fairyTale'", { excludeProperties: true }) .limit(3) .toPromise(); return { fairyTales: fairyTales .map((fairyTale) => ({ // fairyTale.properties is not available id: fairyTale.originId, url: fairyTale.url, })); } } ``` # 3 - Delivering data Source: https://docs.enterspeed.com/enterspeed/deliver Your data has been ingested into Enterspeed and transformed using our schema definitions, now it's time for the final step - delivering the data. First, you need to set up an **Environment client** in Enterspeed. Go to *Settings* --> *Environment settings* --> *Environment clients* and create one. This will generate an API key to use when getting data delivered. Environment Client ## How to get data delivered There are currently two ways of getting data delivered from Enterspeed: 1. Using our Delivery .NET SDK 2. Using our API ### Getting data delivered via our Delivery .NET SDK You can find more information about it here: [https://github.com/enterspeedhq/enterspeed-sdk-delivery-dotnet](https://github.com/enterspeedhq/enterspeed-sdk-delivery-dotnet) ### Getting data delivered via our API You can of course use our API directly to get your data delivered. [You can find the API documentation right here](/api-reference). Want to see an example? See how we [fetched Enterspeed-data in Next.js](/enterspeed/tutorials/umbraco-nextjs/4-fetching-data-in-nextjs) # Adding data sources Source: https://docs.enterspeed.com/enterspeed/getting-started/data-sources Data sources are where a connection is created to your data source. This can be a CMS, a PIM-system, or perhaps a development instance of your CMS. Data sources Data sources are always part of a source group. A source group can have multiple data sources attached and each data source have its own unique API key. The reason data sources are wrapped in source groups is to make it easier to reference in schemas. That way you only have to make a single reference, even though you may have multiple sources spread across environments (e.g. *Development*, *Staging*, *Production*). A data source is connected to one or more environments. You can have multiple data sources in the same source group, but these can't share environments. On each data source, you can see how many source entities have been ingested. A source entity is a single "instance" from your data source, e.g. a page, an article, a product, etc. You can delete all these entities by clicking on the three dots next to the data source and clicking *Delete all entities*. Deleting entities, deleting a source group or deleting a data source will permanently delete the data and can't be undone. ## Creating a data source Click the *Settings*-tab and then click the *Data sources*-tab in the sidemenu. Next, click the *Create group*-button in the right corner. Give your source group a name and select a type (*the type is only used to better help you differentiate your source groups*). Next, give your data source a name, select one or more environments and then click the *Add*-button. Once you have added your data sources, click the *Create*-button. Your source group containing your data source(s) is now created. Each of your data sources will have its own unique API key you can use to ingest data into Enterspeed. # Setting up domains Source: https://docs.enterspeed.com/enterspeed/getting-started/domains Before you can start using Enterspeed, you need to add at least one domain and at least one accompanying hostname. The domain is merely used as a way of grouping your hostnames, meaning you can call it whatever you wish. We of course recommend calling it something similar to your hostname, so you can keep track of it. A **domain** in Enterspeed is a collection of **hostnames**. You should always have at least one hostname per domain. If you for instance had a site called `Tacomania.com`, the domain could be `Tacomania.com` and the hostname(s) could be: * `tacomania.com` * `blog.tacomania.com` * `shop.tacomania.com` If you're just testing something out, you can add `root.tld` as your hostname - this will work as a wildcard hostname. We don't recommend using this in production. The domain(s) will be attached to your environment client, which is the "connection" between Enterspeed and your front-end. You will learn more about [environments](/enterspeed/getting-started/environments) and [environment clients](/enterspeed/getting-started/environment-clients) later on. By linking the domain(s) to your environment client(s), you are able to filter the data based on hostname and only fetch the data which matches the provided hostname(s). ## Why the need for domains? Domains are used as a way to ensure you always fetch the correct data. Say you have ingested data from a multisite setup into Enterspeed. Now you have a lot of source entities (*you will learn more about these in the [Data sources](/enterspeed/getting-started/data-sources) section*), which belong to separate "sub-sites". How do you fetch the correct data? You do this by using domains. Domains in Enterspeed are linked to Environment Clients. Each domain can have multiple hostnames under it. When fetching data from your Environment client, only data from this specific domain will be fetched. ## Adding domains and hostnames If you're using one of our Umbraco [integrations](/enterspeed/integrations), the domain and hostname will automatically be created based on what you have entered under *Culture and Hostnames* in Umbraco. Click the *Settings*-tab and then click the *Environment settings*-tab in the sidemenu. Scroll down to Domains and click the *Create*-button. The domain name is only used as visual help. Data will be filtered using hostnames. Give your domain a name and click *Create*. Click on the three dots (settings icon) next to the domain name and select *Edit hostnames*. Click on the *Create*-button in the top right corner. Enter your hostname, e.g. `tacomania.com`. Click *Create* and then click *Save*. Domains example # Using environment clients Source: https://docs.enterspeed.com/enterspeed/getting-started/environment-clients Environment clients are the "connection" between Enterspeed and your front-end and, as the name suggests, it is also tied up to one of your environments. You can only have one environment per environment client. Moreover, environment clients also have domains attached to them, which makes it possible to filter your data based on the host names attached to the domain name. However, unlike environments, you can have as many domains attached to your environment client as you wish, but you will need at least one. An environment client won't work without a domain attached to it. You can also choose to have a single environment client per domain if you have multiple sites, for instance for separation of concern. ## Creating an environment Click the *Settings*-tab and then click the *Environment settings*-tab in the sidemenu. Scroll down to Domains and click the *Create*-button. Give your Environment client a name and select an environment. Next, select the domains you wish to add to the environment client and then click *Save changes*. Environment clients Once the environment client has been created an API key will be available. If needed the key can be regenerated by clicking the three dots next to the environment client (*Settings*) and selecting *Regenerate API Key*. ## API Scopes API scopes allow you to control which Enterspeed APIs your environment client can access, following the principle of least privilege. This is particularly useful when integrating with AI agents or third-party services where you want to limit access to only the necessary endpoints. ### Default Scopes When creating a new environment client, it automatically receives these default scopes: * **Delivery API** - Access to content delivery endpoints * **Query API** - Access schema-transformed and auto indexed data * **Routes API** - Access to route management and execution ### Available Scopes | Scope | Description | Requirements | | -------------------------------- | ----------------------------------------------- | ------------------ | | **Delivery API** | Content delivery endpoints | None | | **Query API** | Access schema-transformed and auto indexed data | None | | **Routes API** | Routes API access | None | | **MCP Server (AI Agent Access)** | Enables MCP tool endpoints for AI agents | Requires Query API | The MCP Server scope is designed for AI agents and does not provide data access by itself. It must be combined with the Query API scope to function properly. ### Scope Presets For convenience, the Management App provides several preset configurations: * **Standard** - Delivery + Routes + Query (default for regular applications) * **AI Assistant** - Query + MCP Server — Full data access for AI agents * **Custom** - Manually select specific scopes ### Managing Scopes You can configure scopes when creating or updating an environment client through the Management App. Existing environment clients without explicit scopes will continue to work with the default scope configuration, ensuring backward compatibility. **Using the Query MCP Server.** If you are setting up an AI agent, pick the **AI Assistant** preset above and see the [Query MCP Server documentation](/enterspeed/mcp-server/query-mcp/overview) for hostnames, authentication, and step-by-step client setup. ## Index-Level Scopes (Advanced) Beyond controlling which APIs your environment client can access, you can also configure **index-level scopes** within the Query API to restrict access to specific data. This provides fine-grained control over exactly what content an AI agent or integration can access. ### Schema indices When your environment client has Query API access, you can optionally restrict it to specific schema indices: * **All indices (default)** - Access to all deployed index schemas * **Specific indices** - Access only to selected schemas (e.g., `blogpost`, `product`) * **Wildcard patterns** - Access to schemas matching patterns (e.g., `blog*` matches `blogpost`, `blogpage`, etc.) **Example use cases:** * AI agent for blog content: Restrict to `blogpost` and `blogpage` indices only * Product recommendation system: Restrict to `product` and `category` indices only * Content migration tool: Use `cms*` pattern for all CMS-related schemas ### Auto indices You can also optionally restrict Query API access to specific auto-indexed source group and entity type combinations: * **All sources (default)** - Access to all auto-indexed source entities * **Specific combinations** - Access only to selected source group + entity type pairs (e.g., `cms:page`, `shop:product`) * **Wildcard patterns** - Access to all entity types within a source group (e.g., `cms:*`) **Format:** Index scopes use the pattern `sourceGroup:entityType` **Example configurations:** * CMS content only: `cms:page`, `cms:article`, `cms:blogpost` * E-commerce data: `shop:product`, `shop:category`, `shop:inventory` * All CMS content: `cms:*` ### Index Scope Behaviour Index scopes are **optional** and only apply when the **Query API** component scope is enabled. If no index scopes are configured, the client has access to **all** data within its allowed component scopes. This two-layer approach provides maximum flexibility: 1. **Component scopes** control which APIs can be accessed 2. **Index scopes** control which specific data within those APIs can be accessed # Configuring environments Source: https://docs.enterspeed.com/enterspeed/getting-started/environments To use Enterspeed you need to have at least one environment. When creating a new tenant in Enterspeed, two environments are automatically created: `Development` and `Production`. Environments play a huge role inside Enterspeed and are used in all parts of the [ITD-process](/enterspeed/getting-started/intro). **Ingesting data**: When creating a [data source](/enterspeed/getting-started/data-sources), you connect this source to an environment. This is helpful so you don't mix your Production data with your Development data. **Transforming data**: When deploying a [schema](/enterspeed/transform/intro) you choose which environment to wish to deploy to. Once deployed, views will be generated based on the data defined in the schema. These views will be available for the environment client with the matching environment connected. **Delivering data**: When delivering data, an environment client is used to make the connection between Enterspeed and your front-end. An environment client is, as the name suggests, tied up on a specific environment. This makes it easy to test changes before deploying to the Production environment. ## Creating an environment Enterspeed will as mentioned automatically create two environments for you. You can view them under *Environment settings* in the *Settings* section. Here you can create new environments, edit the name of your environments or delete them. Beware of deleting environments, since this is an irreversible action that will remove all data attached to the environment. Deleting an environment will cause all data for the environment to be deleted. # Intro Source: https://docs.enterspeed.com/enterspeed/getting-started/intro ## The ITD-process To get started using Enterspeed, you need to go through a three-step process - the ITD-process: 1. **I**ngest data 2. **T**ransform data 3. **D**eliver data The Enterspeed process ### Ingesting data In this step, we ingest the data from your current data source(s) into Enterspeed. You can do this by using one of our [integrations](/enterspeed/integrations) or by using our [API](/api). ***[Go to the Ingest data section.](/enterspeed/ingest)*** ### Transforming data Once the data have been ingested into Enterspeed, you can start transforming it. You do this by using our Schema designer. Here you can combine data from multiple sources and select which data you want to be available to the front-end. Once the data is transformed it gets stored in a high-performance Redis database that can be stored across multiple geographical regions. ***[Go to the Transforming data section.](/enterspeed/transform/intro)*** ### Delivering data The data is now available to fetch via the Enterspeed Delivery API. Like working with any other APIs, it's extremely easy to integrate into your front-end project. ***[Go to the Delivering data section.](/enterspeed/deliver)*** ## Configuring Enterspeed However, before you can start the ITD-process, you need to do some configuration first. We need to set up: * Domains and hostnames * Environment and environment clients * Data sources You will learn about this in the next sections. # 1 - Ingesting data Source: https://docs.enterspeed.com/enterspeed/ingest The first step when working with Enterspeed is to get your current data sources into Enterspeed. Go to Data sources under Settings and click the Create group button. This will open a modal that allows you to create a new source group. A source group can have multiple data sources attached, which each have its own unique API key. Data sources ## How to ingest data There are several ways of doing this: * Using one of our integrations * Using our .NET SDK * Using our API * Using the Management App ### Ingesting data via an integration Using one of our [integrations](/enterspeed/integrations/overview) is the easiest way to get data ingested. This integration takes care of calling the Enterspeed Ingest API when changes occur in your backend system. ### Ingesting data via our .NET SDK Another way of ingesting data is by using our .NET SDK. You'll find more information about it here: [https://github.com/enterspeedhq/enterspeed-sdk-dotnet](https://github.com/enterspeedhq/enterspeed-sdk-dotnet) ### Ingesting data via our API Last but now least, you can of course use our API directly to ingest your data. [You can find the API documentation right here.](/api-reference/ingest) ### Ingesting data via the Management App You can also use the Enterspeed Management App to manually ingest data directly from the [Source entities page](https://app.enterspeed.com/source-entities). This is useful for testing or for one-time data imports. ## Viewing ingested data Once your data has been ingested into Enterspeed, it will be visible under Source Entities. You can click on the View button to see the data for the source entity. Viewing ingested data # Algolia Source: https://docs.enterspeed.com/enterspeed/integrations/algolia The Enterspeed Algolia integration uses [destinations](/enterspeed/reference/js/full-schema/actions#destination) to send data from views directly to a configured Algolia index. This means that you can decide on the schema level which views you want to send to Algolia. You will only have to set the destination field on the entity schema you want to send to Algolia. All schema references are automatically resolved so you don't have to set it on all referenced schemas. It's possible to configure multiple Algolia destinations if you need to push different types of data to different Algolia indexes. ## Configuration In order to setup the Algolia configuration you need the following: | Setting | Description | | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | Algolia Index Name | The name of the Algolia index you want to integrate to | | Algolia Application ID | The unique application identifier used to identify you when working with Algolia's API | | Algolia API Key | The API key needs `addObject` and `deleteObject` rights for the index you want to integrate to | | Enterspeed Environment Client API Key | The API key for an Enterspeed Environment client. This is used to fetch the view that will be inserted into Algolia | ## Options Table of available options, that you can optionally specify, if needed for your use case. | Setting | Description | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | objectId | By default Enterspeed uses view id as the value for object id in Algolia. You can override default object id by providing value for this option. | | indexName | Option to override what index must be used for current view. By default `IndexName` of Algolia configuration is used. | | partialUpdate | A boolean indicating if the update of the Algolia record should be a partial update. [See Algolia documentation](https://www.algolia.com/doc/api-reference/api-methods/partial-update-objects/).

Default value: `false` | | createIfNotExists | A boolean indicating if the partial update should create a record in Algolia if it does not already exists. Only used if `partialUpdate` is set to `true`.

Default value: `false` | | skipDelete | A boolean indicating if a deleted view should delete the record in Algolia. Typically set to `false` for `partialUpdate` if the schema is not working on the "master" object.

Default value: `false` | Algolia does not support partial deletes. This means that if you are using partial updates and you want to clear the attributes from the partial update, you should not delete the source entity that triggers the partial update in Enterspeed. Instead you should update the source entity and set the properties to default values, like null, an empty string, an empty array and so on. ## Example of usage ```js title="Schema with Algolia destination" theme={null} /** @type {Enterspeed.FullSchema} */ export default { triggers: function(context) { context.triggers('geodata', ['city']); }, actions: function (sourceEntity, context) { context.destination('algolia').options({ objectId: `city-${sourceEntity.originId}` partialUpdate: false }); }, properties: function ({url, properties: p}, context) { return { url: url, name: p.name } } } ``` ```json title="Schema with Algolia destination" theme={null} { "triggers": { "geodata": ["city"] }, "destinations": [ { "alias": "algolia", "options": { "objectId": "city-{originId}" } } ], "properties": { "url": "{url}", "name": "{p.name}" } } ``` ## Algolia specific properties ### \_geoloc `_geoloc` is a special property in Algolia used for doing geo-searching. [See Algolia documentation](https://www.algolia.com/doc/guides/managing-results/refine-results/geolocation/) As stated in the Algolia documentation, the `lat` and `lng` properties must be numeric values. This means that you will need to make sure that your Enterspeed schema is mapping these properties as numeric values and not as strings. ```js title="Schema with Algolia destination" theme={null} /** @type {Enterspeed.FullSchema} */ export default { triggers: function(context) { context.triggers('geodata', ['city']); }, actions: function (sourceEntity, context) { context.destination('algolia').options({ objectId: `city-${sourceEntity.originId}` }); }, properties: function ({url, properties: p}, context) { return { url: url, name: p.name, _geoloc: { lat: p.lat, lng: p.lng, } } } } ``` ```json title="Schema with Algolia destination" theme={null} { "triggers": { "geodata": ["city"] }, "destinations": [ { "alias": "algolia", "options": { "objectId": "city-{originId}" } } ], "properties": { "url": "{url}", "name": "{p.name}", "_geoloc": { "type": "object", "properties": { "lat": { "type": "number", "value": "40.639751", "precision": 6 }, "lng": { "type": "number", "value": "-73.778925", "precision": 6 } } } } } ``` # Clerk.io Source: https://docs.enterspeed.com/enterspeed/integrations/clerk The Enterspeed Clerk.io integration uses [destinations](/enterspeed/reference/js/full-schema/actions#destination) to send data from views directly to a configured Clerk.io store. This means that you can decide on the schema level which views you want to send to Clerk.io. You will only have to set the destination field on the entity schema you want to send to Clerk.io. All schema references are automatically resolved so you don't have to set it on all referenced schemas. It's possible to configure multiple Clerk.io destinations if you need to push different types of data to different Clerk.io stores. As of now the Clerk.io integration only support pages. Contact us if you want to work with other types in Clerk.io. ## Configuration In order to setup the Clerk.io configuration you need the following: | Setting | Description | | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | Clerk Public Key | The unique public key of the Clerk.io store you want to integrate to | | Clerk Private Key | A private key on the Clerk.io store you want to integrate to | | Enterspeed Environment Client API Key | The API key for an Enterspeed Environment client. This is used to fetch the view that will be inserted into Clerk.io | ## Options Table of available options, that you can optionally specify, if needed for your use case. | Setting | Description | | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | id | By default Enterspeed uses view id as the value for object id in Clerk. You can override default object id by providing value for this option. | | clerkEntityType | The type of entity you want to send to Clerk. Supported values: `page`. | ## Example of usage ```js title="Schema with Clerk destination" theme={null} /** @type {Enterspeed.FullSchema} */ export default { triggers: function(context) { context.triggers('cms', ['page']); }, actions: function (sourceEntity, context) { context.destination('clerk').options({ id: sourceEntity.originId, relewiseEntityType: 'page' }); }, properties: function ({url, properties: p}, context) { return { url: url, title: p.title, text: p.text } } } ``` ```json title="Schema with Clerk destination" theme={null} { "triggers": { "cms": ["page"] }, "destinations": [ { "alias": "clerk", "options": { "id": "{originId}", "relewiseEntityType": "page" } } ], "properties": { "url": "{url}", "title": "{p.title}", "text": "{p.text}" } } ``` ## Clerk.io specific properties Each type in Clerk.io (page, product, category, ...) has a set of required field which must be mapped in the Enterspeed schema. See required fields here: [https://docs.clerk.io/reference/page-resource](https://docs.clerk.io/reference/page-resource) `id` is set automatically or by the options id if you use that, so you don't need to map this property. # Commercetools Source: https://docs.enterspeed.com/enterspeed/integrations/commercetools A ready to use integration service for connecting Commercetools as an Enterspeed data-source by importing products, variants and categories. Included in the respository is a reference implementation based on Azure Functions, that may be deployed directly - or integrated into your own projects. The included default implementation includes products, variants, availability, pricing and attributes, along with categories and their custom fields. ## Installation With .NET CLI ```bash theme={null} dotnet add package Enterspeed.Source.Commercetools --version ``` Using the Package Manager ```bash theme={null} Install-Package Enterspeed.Source.Commercetools -Version ``` ## Configuration Check out the [Github repo](https://github.com/enterspeedhq/enterspeed-source-commercetools) for more information on how to configure the Commercetools integration. # Elastic App Search Source: https://docs.enterspeed.com/enterspeed/integrations/elastic-app-search The Enterspeed Elastic App Search integration uses [destinations](/enterspeed/reference/js/full-schema/actions#destination) to send data from views directly to a configured Elastic App Search engine. This means that you can decide on the schema level which views you want to send to Elastic App Search. You will only have to set the destination field on the entity schema you want to send to Elastic App Search. It's possible to configure multiple Elastic App Search destinations if you need to push different types of data to different Elastic App Search engines. ## Configuration In order to setup the Elastic App Search configuration you need the following: | Setting | Description | | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | Engine API Endpoint | The endpoint of Elastic App Search engine you want to integrate to | | Private API Key | The Private API key used for calling Engine API endpoint | | Enterspeed Environment Client API Key | The API key for an Enterspeed Environment client. This is used to fetch the view that will be inserted into Enterspeed Elastic App | ## Options Table of available options, that you can optionally specify, if needed for your use case. | Setting | Description | | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | documentId | By default Enterspeed uses view id as the value for document id in elastic app search. You can override default document id by providing value for this option. | ## Example of usage ```js title="Schema with elastic app search destination" theme={null} /** @type {Enterspeed.FullSchema} */ export default { triggers: function(context) { context.triggers('geodata', ['city']); }, actions: function (sourceEntity, context) { context.destination('elastic-app-search').options({ documentId: `city-${sourceEntity.originId}` }); }, properties: function ({url, properties: p}, context) { return { url: url, name: p.city, country: p.country, population: parseInt(p.population), location: `${p.lat},${p.lng}`, photo: p.photo, } } } ``` ```json title="Schema with elastic app search destination" theme={null} { "triggers": { "geodata": [ "city" ] }, "destinations": [ { "alias": "elastic-app-search", "options": { "documentId": "city-{originId}" } } ], "properties": { "url": "{url}", "name": "{p.city}", "country": "{p.country}", "photo": "{p.photo}", "location": "{p.lat},{p.lng}", "population": { "type": "number", "value": "{p.population}" } } } ``` ## Elastic App Search specific properties Useful resources about document limitations and other requirements: * [https://www.elastic.co/guide/en/app-search/current/api-reference.html](https://www.elastic.co/guide/en/app-search/current/api-reference.html) * [https://www.elastic.co/guide/en/app-search/current/documents.html#documents-create](https://www.elastic.co/guide/en/app-search/current/documents.html#documents-create) # Elasticsearch Source: https://docs.enterspeed.com/enterspeed/integrations/elasticsearch The Enterspeed Elasticsearch integration uses [destinations](/enterspeed/reference/js/full-schema/actions#destination) to send data from views directly to a configured Elasticsearch. This means that you can decide on the schema level which views you want to send to Elasticsearch cluster. You will only have to set the destination field on the entity schema you want to send to Elasticsearch. It's possible to configure multiple Elasticsearch destinations if you need to push different types of data to different Elasticsearch clusters or indexes. ## Configuration In order to setup the Elasticsearch configuration you need the following: | Setting | Description | | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | Elasticsearch endpoint | The endpoint of Elasticsearch cluster you want to integrate to | | API Key | The API key used for calling Elasticsearch `_bulk` endpoint for creating, updating, deleting documents | | Index name | Name of the default index to use when creating, updating, deleting documents in Elasticsearch. | | Enterspeed Environment Client API Key | The API key for an Enterspeed Environment client. This is used to fetch the view that will be inserted into Elasticsearch | ## Options Table of available options, that you can optionally specify, if needed for your use case. | Setting | Description | | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | documentId | By default Enterspeed uses view id as the value for document id in elasticsearch. You can override default document id by providing value for this option. | | indexName | Option to override what index must be used for current view. By default `IndexName` of Elasticsearch configuration is used. | ## Example of usage ```js title="Schema with elasticsearch destination" theme={null} /** @type {Enterspeed.FullSchema} */ export default { triggers: function(context) { context.triggers('geodata', ['city']); }, actions: function (sourceEntity, context) { context.destination('elasticsearch').options({ documentId: `city-${sourceEntity.originId}`, indexName: 'cities' }); }, properties: function ({url, properties: p}, context) { return { url: url, name: p.city, country: p.country, population: parseInt(p.population), location: `${p.lat},${p.lng}`, photo: p.photo, } } } ``` ```json title="Schema with elasticsearch destination" theme={null} { "triggers": { "geodata": [ "city" ] }, "destinations": [ { "alias": "elasticsearch", "options": { "documentId": "city-{originId}", "indexName": "cities" } } ], "properties": { "url": "{url}", "name": "{p.city}", "country": "{p.country}", "photo": "{p.photo}", "location": "{p.lat},{p.lng}", "population": { "type": "number", "value": "{p.population}" } } } ``` # Enterspeed Integrations Source: https://docs.enterspeed.com/enterspeed/integrations/overview Enterspeed has a growing list of integrations to various systems. The integrations come in two categories. **Source** integrations are used to push data from a source system into Enterspeed. **Destination** integrations are used to push processed data from Enterspeed to external systems. ## Integrations
## Integrations currently in beta Integrations we currently have in beta. Please reach out to us if you would like to use any of these integrations in production.
## Integrations on the roadmap We are currently looking into the following integrations. If you are missing an integration to a specific system or is interested in one of the integrations on the roadmap, please reach out to us as we would love to look into it.
# Relewise Source: https://docs.enterspeed.com/enterspeed/integrations/relewise The Enterspeed Relewise integration uses [destinations](/enterspeed/reference/js/full-schema/actions#destination) to send data from views directly to a configured Relewise account. This means that you can decide on schema level which views you want to send to Relewise. You will only have to set the destination field on the entity schema you want to send to Relewise. All schema references are automatically resolved so you don't have to set it on all referenced schemas. It's possible to configure multiple Relewise destinations if you need to push different types of data to different Relewise accounts. ## Configuration In order to setup the Relewise configuration you need the following: | Setting | Description | | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | Relewise Dataset Id | The unique Dataset Id for the Relewise account you want to integrate to | | Relewise Server URL | The Server URL for the Relewise account you want to integrate to | | Relewise API Key | A Relewise API key with update and administrative action permissions for the types you want to integrate (product, content, brand, ...) | | Enterspeed Environment Client API Key | The API key for an Enterspeed Environment client. This is used to fetch the view that will be inserted into Relewise | ## Options Table of available options, that you can optionally specify, if needed for your use case. | Setting | Description | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | | id | By default Enterspeed uses view id as the value for object id in Relewise. You can override default object id by providing value for this option. | | relewiseEntityType | The type of entity you want to send to Relewise. Supported values: `product`, `productCategory`, `content`, `contentCategory` and `brand`. | ## IntelliSense In order to send data to Relewise (products, content, ...) the structure of the object you are mapping in the `properties` function must match with the corresponding Relewise model. See [Relewise API in Swagger](https://docs.relewise.com/swagger/index.html). To help you bulding the right model, Enterspeed can provide you with IntelliSense. Simply just change the type in the top of the schema from `/** @type {Enterspeed.FullSchema} */` to one of the following: * `/** @type {Enterspeed.Destinations.Relewise.Schemas.Product} */` * `/** @type {Enterspeed.Destinations.Relewise.Schemas.Content} */` * `/** @type {Enterspeed.Destinations.Relewise.Schemas.Brand} */` * `/** @type {Enterspeed.Destinations.Relewise.Schemas.ProductCategory} */` * `/** @type {Enterspeed.Destinations.Relewise.Schemas.ContentCategory} */` ## Example of usage ```js title="Product schema with Relewise destination" theme={null} /** @type {Enterspeed.Destinations.Relewise.Schemas.Product} */ export default { triggers: function(context) { context.triggers('pim', ['product']); }, actions: function (sourceEntity, context) { context.destination('relewise').options({ id: sourceEntity.originId, relewiseEntityType: 'product' }); }, properties: function ({properties: p}, context) { return { product: { displayName: { values: [ { language: { value: "en-gb" }, text: p.productName } ] }, salesPrice: { values: [ { amount: p.salesPrice, currency: { value: "Euro" } } ] } }, variants: context.reference('variantSchema').children() } } } ``` ```json title="Product schema with Relewise destination" theme={null} We recommend using JavaScript schemas when working with Relewise destination as it provides IntelliSense to map the complex models. ``` ```js title="Content schema with Relewise destination" theme={null} /** @type {Enterspeed.Destinations.Relewise.Schemas.Content} */ export default { triggers: function(context) { context.triggers('cms', ['contentPage']); }, actions: function (sourceEntity, context) { context.destination('relewise').options({ id: sourceEntity.originId, relewiseEntityType: 'content' }); }, properties: function ({properties: p}, context) { return { displayName: { values: [ { language: { value: "en-gb" }, text: p.title } ] }, data: { contentData: { type: "String", value: p.text } } } } } ``` ```json title="Content schema with Relewise destination" theme={null} We recommend using JavaScript schemas when working with Relewise destination as it provides IntelliSense to map the complex models. ``` ## What's supported The integration supports the following types: `product` (with variants), `productCategory`, `content`, `contentCategory` and `brand` and the type needs to be defined in the `relewiseEntityType` property in the destination options together with a value for the id you want in Relewise. See more details on the Relewise documentation: [https://docs.relewise.com/docs/developer/implementation-steps.html#\_1-provide-entities](https://docs.relewise.com/docs/developer/implementation-steps.html#_1-provide-entities) All updates are done with `UpdateKind.ClearAndReplace` and all administrative actions are done with `UpdateKind.Disable` # Azure Service Bus Source: https://docs.enterspeed.com/enterspeed/integrations/servicebus The Enterspeed Azure Service Bus integration uses [destinations](/enterspeed/reference/js/full-schema/actions#destination) to send data from views directly to a configured Azure Service Bus queue or topic. This means that you can decide on schema level which views you want to send to the service bus. You will only have to set the destination field on the entity schema you want to send to the Azure Service Bus. All schema references are automatically resolved so you don't have to set it on all referenced schemas. It's possible to configure multiple Azure Service Bus destinations if you need to push different types of data to different service bus queues or topic. ## Configuration In order to setup the Azure Service Bus configuration you need the following: | Setting | Description | | ---------------- | ------------------------------------------------------- | | ConnectionString | The connection string to the Azure Service Bus | | QueueOrTopicName | The name of the queue or topic in the Azure Service Bus | ## Message The message will send the following data. ```json theme={null} { "id": "gid://Environment/2052b78d-6c34-4f11-bea5-296cf2d26968/Source/053b598b-c3d1-46fb-91e3-53115169cdb2/Entity/1234/View/product", // the Enterspeed view id "originId": "1234", // the origin id of the entity "type": "product", // the type of the entity "action": "Deploy", // can have the value of Deploy or Remove "url": "https://weu.delivery.enterspeed.com/v2?id=gid://Environment/40bb2d76-3b71-4121-b9b6-238cf4f325c4/Source/9e78f134-cf84-4ec5-9180-0b72b94949be/Entity/1099-en-us/View/home" // the absolute url for the delivery api to fetch the view } ``` ## Example of usage ```js title="Content schema with Azure Service Bus destination" theme={null} /** @type {Enterspeed.FullSchema} */ export default { triggers: function(context) { context.triggers('cms', ['content']); }, actions: function (sourceEntity, context) { context.destination('service-bus'); }, properties: function ({properties: p, url}, context) { return { product: { url: url, title: p.title, content: p.text } } } ``` ```json title="Content schema with Azure Service Bus destination" theme={null} { "triggers": { "cms": ["content"] }, "destinations": [ { "alias": "service-bus" } ], "properties": { "url": "{url}", "title": "{p.title}", "content": "{p.text}" } } ``` # Shopify Source: https://docs.enterspeed.com/enterspeed/integrations/shopify The Shopify Destination is currently in preview, reach out if you want to get started. The Enterspeed Shopify integration uses [destinations](/enterspeed/reference/js/full-schema/actions#destination) to send data from views directly to a configured Shopify shop. This means that you can decide on schema level which views you want to send to Shopify. You will only have to set the destination field on the entity schema you want to send to Shopify. All schema references are automatically resolved so you don't have to set it on all referenced schemas. It's possible to configure multiple Shopify destinations if you need to push data to different Shopify shops. The jobs are executed in bulk requests to avoid hitting API rate limits in Shopify. This means a new bulk job of up to 1.000 jobs is started every minute unless another bulk job on the same shop connection is still active. If another bulk job is active, the integration will wait another minute and try again. The model you map in the Enterspeed schema looks to some degree like Shopifys product model, however what you map is a unified model containing both product, variant and translation data for a product making it easy to map out the full product in one go. ## Requirements It's required to have a meta field called `enterspeed.customId` of type `ID` on the object types the destination is used for. This field must have the `Filter on the product list and in the Admin API` enabled and is used as an identifier when updating or deleting objects in Shopify through the destination. CustomId meta field ## Configuration In order to setup the Shopify configuration you need the following: | Setting | Description | | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Shopify store name | The unique store name in front of .myshopify.com. E.g. \[my store name].myshopify.com | | Shopify access token | A Developer app API access token with the following permissions: `write_products`, `read_products`, `write_publications`, `read_publications`, `write_locales`, `read_locales`, `write_translations`, `read_translations` | | Shopify API secret key | A Developer app API secret key | | Enterspeed Environment Client API Key | The API key for an Enterspeed Environment client. This is used to fetch the view that will be inserted into Shopify | ## Options Table of available options, that you can optionally specify, if needed for your use case. | Setting | Description | | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | customId | Set your own id of the product, e.g. a SKU number or internal id. | | shopifyEntityType | The type of entity you want to send to Shopify. Supported values: `product`. | | statusForNewProducts | The default status for newly created products. Supported values: `Active`, `Draft`, `Archived`, `Unlisted`. If the `status` property is set map directly on the product, this value takes priority over this setting. | | setOnlyHandleForNewProducts | If the value is `true` the handle value will not be updated when the product is updated, it will on ly be set when the product is created the first time. Default value is `false`. Supported values: `true`, `false`. | When products are deleted in Enterspeed the Shopify Destination will archive the products i Shopify and not delete them. ## IntelliSense In order to send data to Shopify the structure of the object you are mapping in the `properties` function must match with the corresponding Shopify model. To help you bulding the right model, Enterspeed can provide you with IntelliSense. Simply just change the type in the top of the schema from `/** @type {Enterspeed.FullSchema} */` to one of the following: * `/** @type {Enterspeed.Destinations.Shopify.Schemas.Product} */` ## Example of usage ```js title="Product schema with Shopify destination" theme={null} /** @type {Enterspeed.Destinations.Shopify.Schemas.Product} */ export default { triggers: function(context) { context.triggers('pim', ['product']); }, actions: function (sourceEntity, context) { context.destination('shopify').options({ customId: sourceEntity.originId, shopifyEntityType: 'Product' }); }, properties: function ({properties: p}, context) { return { handle: `${p.name}-${p.itemNumber}`, title: p.name, seoTitle: p.name, type: 'electronic', tags: ['tag1', 'tag2'], templateSuffix: p.template } } } ``` ```json title="Product schema with Shopify destination" theme={null} We recommend using JavaScript schemas when working with Shopify destination as it provides IntelliSense to map the complex models. ``` ## What's supported The integration curerntly only supports the following types: `product` (with variants, prices, translations and so on). The type needs to be defined in the `shopifyEntityType` property in the destination options. ### Meta field types Meta fields are created as unstructured fields without a definition - see [Shopify documentation](https://shopify.dev/docs/apps/build/custom-data#unstructured-metafields). You can change the meta fields to structured fields either before or after products are ingested by creating a definition. Note the definition type must match what ever type is mapped in the Enterspeed schema. Keep in mind that changeing the type in the Enterspeed schema will not change the meta field definition type and the import will fail. # Field Value Converter Source: https://docs.enterspeed.com/enterspeed/integrations/sitecore/enterspeed-value-converter # Enterspeed Field Value Converter A field value converter is a class that will convert the input value from Sitecore into an [IEnterspeedProperty](https://github.com/enterspeedhq/enterspeed-sdk-dotnet/tree/master/documentation/entities/properties). To implement your own converter you need to implement the IEnterspeedPropertyValueConverter interface ## IEnterspeedFieldValueConverter This interface contains two methods that need to be implemented ### IsConverter ```csharp theme={null} bool CanConvert(Field field); ``` This method is called when the EnterspeedPropertyService tries to find the proper converter for this property. An implementation of this method could look like this: ```csharp theme={null} public bool CanConvert(Field field) { return field != null && field.TypeKey.Equals("number", StringComparison.OrdinalIgnoreCase); } ``` ### Convert ```csharp theme={null} IEnterspeedProperty Convert(Item item, Field field, EnterspeedSiteInfo siteInfo, List fieldValueConverters, EnterspeedSitecoreConfiguration configuration); ``` This is the method that is converting the Sitecore field to an IEnterspeedProperty. An implementation of this method could look like this: ```csharp theme={null} public IEnterspeedProperty Convert(Item item, Field field, EnterspeedSiteInfo siteInfo, List fieldValueConverters, EnterspeedSitecoreConfiguration configuration) { if (string.IsNullOrEmpty(field.Value)) { return null; } var value = 0d; if (field.Value.Contains(".")) { double.TryParse(field.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out value); } else if (field.Value.Contains(",")) { double.TryParse(field.Value, NumberStyles.Any, new CultureInfo("da-DK"), out value); } return new NumberEnterspeedProperty(_fieldService.GetFieldName(field), value); } ``` ## Registering a converter Converters are registered in a service configurator. You can roll your own service configurator, and register your own converters in this. Example: ```csharp theme={null} public class ServicesConfigurator : IServicesConfigurator { public void Configure(IServiceCollection services) { services.AddSingleton(); } } ``` Note if you want to override a default converter, you would have to register your own configurator, after the default Enterspeed configurator. This can be done with a patch config file. Here is an example of the default configurator being set up in a config file. ```xml theme={null} ``` ## Default converters Enterspeed ships with default property value converters for all the built-in fields that Sitecore ships with out of the box. [Sitecore 8 default property value converters](https://github.com/enterspeedhq/enterspeed-source-sitecore-cms/tree/master/src/Enterspeed.Source.SitecoreCms.V8/Services/DataProperties/DefaultFieldConverters)\ [Sitecore 9 default property value converters](https://github.com/enterspeedhq/enterspeed-source-sitecore-cms/tree/master/src/Enterspeed.Source.SitecoreCms.V9/Services/DataProperties/DefaultFieldConverters) # Getting data Source: https://docs.enterspeed.com/enterspeed/integrations/sitecore/getting-data # Getting data from Sitecore to Enterspeed When you press publish on a piece of content in Sitecore, Sitecore handles the content as normally: validation, trigger events, etc. The integration into Enterspeed is simply just a couple of events that the integration listens to. ## When content is being published When a piece of content is being published in Sitecore, the integration reacts upon this and pushes it to Enterspeed, when the item has successfully been published. It has then been sent to the Enterspeed Ingest API for Enterspeed to process it and deliver it to the Delivery API. ## SitecoreContentEntity explained To make sure you understand the whole process from end to end, an important detail is how the SitecoreContentEntity is created, because these properties define the entity you work with, in Enterspeed. The properties within the properties object don't change when it gets ingested in Enterspeed, this means that when you are to create your Schemas to model your API, you can rely 1:1 on what you ingest and what you can query in Enterspeed. ## Seeding content to Enterspeed If you have an existing site, have installed Enterspeed later in the development process or just want to make sure all your content is in Enterspeed, you can seed or re-seed all the content from Sitecore into Enterspeed. This can simply be done by going to the root item of the Site that you would like to push to Enterspeed, and doing a republish with the language, subitems and relations checked. This way we ensure that all data sources on the pages are sent to Enterspeed as well. # Getting started Source: https://docs.enterspeed.com/enterspeed/integrations/sitecore/getting-started # Getting started with Sitecore & Enterspeed The easiest way to get started with Sitecore and Enterspeed is using the pre-built Sitecore integration. **GitHub: [Enterspeed Source Sitecore CMS](https://github.com/enterspeedhq/enterspeed-source-sitecore-cms)** This integration takes care of calling the Enterspeed Ingest API when changes occur in Sitecore. ## Installation **Prerequisite:** Sitecore 8 or 9 If your want to use Sitecore 10 with Enterspeed please reach out to us as we would love to look into it. The fastest way to get up and running is to install the Enterspeed Sitecore integration with NuGet. **NuGet:**\ [Enterspeed.Source.SitecoreCms.V8](https://www.nuget.org/packages/Enterspeed.Source.SitecoreCms.V8/)\ [Enterspeed.Source.SitecoreCms.V9](https://www.nuget.org/packages/Enterspeed.Source.SitecoreCms.V9/) You can either install it manually from the NuGet manager in Visual Studio or execute one of the Install-Package command: ```bash theme={null} Install-Package Enterspeed.Source.SitecoreCms.V8 ``` ```bash theme={null} Install-Package Enterspeed.Source.SitecoreCms.V9 ``` The NuGet package installs config files into this directory; verify that this folder contains config files. ```bash theme={null} ~\App_Config\Include\Enterspeed ``` ## Configuration Once installed, please navigate to `/sitecore/templates/System/Enterspeed` in the Master database, and publish the item, including all descendants. These templates must exist in the Web database as a prerequisite for Enterspeed configuration items. You will see that your Sitecore instance is loaded with a new item in `/Sitecore/system` called "Enterspeed Configuration". You will have to create a Site configuration, for each Enterspeed configuration you would like to create. In the Site configuration file, we have 7 fields * API Base Url (required) > This is the api url for Enterspeed. Unless you have gotten a specific Enterspeed endpoint to call, please use: [https://api.enterspeed.com](https://api.enterspeed.com) * API Key (required) > This is the Source API key. This API key can be found in the settings section of your tenant in [https://app.enterspeed.com/](https://app.enterspeed.com/) (Settings/Data sources) * Enabled Sites > In this field, you define the area of content covered by the site configuration. All items within this area are pushed to the source specified in this configuration. Selected items must share the same fullPath as the rootPath(s) configured in your site configuration within the Sitecore config files. * Media Base Url > Base url for media being pushed to Enterspeed. This would be typically be a url provided by your CDN for media and file hosting * Site Base Url (required) > Base Url for your site. * Publish Hook Url > You can call an external hook, when publish has finished. This could for example trigger a build in Netlify. * Enable Preview > With this checkbox you are defining where this configuration is for a preview site. ## First load after installing the Sitecore connector. A table called EnterspeedJobs must be created so that jobs can be processed asynchronously. This table is created in the master database, meaning the SQL user specified in the connection string must have the necessary permissions to create tables (e.g., db owner). Ensure that the master database user has temporary rights to create tables in your master database. ## Enterspeed Logs If something unexpected occurs or the need for investigation arises, the connector creates an Enterspeed log, which collects any exceptions encountered during data ingestion. This log can be found in your Sitecore logs folder. The format of the file is: `Enterspeed.log.{date}.{time}.txt` # Processed entities Source: https://docs.enterspeed.com/enterspeed/integrations/sitecore/processed-entities ### Content Content items that are being sent to Enterspeed, will have references to the renderings inserted on them, along with information of the fields of the given datasources inserted on these renderings. Each rendering reference sent to Enterspeed could have these properties: * `name` - the name of this rendering * `placeholder` - the Sitecore placeholder inserted on either the presentation details or the rendering itself * `parameters` - an array of key/values inserted on the rendering options * `datasource` - a reference to the inserted datasource item ### Dictionaries Published dictionary items will be pushed to Enterspeed and exist as datasources of type dictionaryEntry. ### Renderings Renderings are processed separately, as well, but only if the rendering is inserted on the presentation details of a content item that resides in an enabled site. This means that newly created renderings are not processed until they're inserted to be rendered on content for which you have enabled. ### Supported field types * Single-Line Text * Rich-Text * Checkbox * Date * File * Image * Integer * Multi-Line Text * Number * Checklist * Droplist * Grouped Droplink * Grouped Droplist * Multilist * Name Value List * Name Lookup Value List * Treelist * Droplink * Droptree * General Link ### Field names in Enterspeed Field names on your content are sanitized when sent to Enterspeed. See below example: * Content * Title * Text * CTA Link * Footer * Contact Link * Text The above sections and fields will be sanitized like this - see below: * `content_title` * `content_text` * `content_ctalink` * `footer_contactlink` * `footer_text` # Services Source: https://docs.enterspeed.com/enterspeed/integrations/sitecore/services ## EnterspeedPropertyService : IEnterspeedPropertyService This service is used for converting a Sitecore field to an IEnterspeedProperty. ### Methods ```csharp theme={null} IDictionary GetProperties (IPublishedContent content, string culture = null); IDictionary ConvertProperties (IEnumerable properties, string culture = null; ``` Both methods will find the correct registered Enterspeed Property Value Converter and convert the value to an [IEnterspeedProperty](https://github.com/enterspeedhq/enterspeed-sdk-dotnet/tree/master/documentation/entities/properties). # SitecoreContentEntity Source: https://docs.enterspeed.com/enterspeed/integrations/sitecore/sitecorecontententity The SitecoreContentEntity is the concrete Sitecore-specific implementation of the [IEnterspeedEntity](https://github.com/enterspeedhq/enterspeed-sdk-dotnet/tree/master/documentation/entities). ## Implementation details ### Abstract | Name | Type | Description | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | | Id | string | Unique identifier e.g. `e61ddc8e-90ad-4d31-bb00-024987a5f2d1` | | Type | string | ContentType alias | | Url | string | The current URL of the content, either relative or absolute | | Redirects | string\[] | Array of redirects for the item | | ParentId | string | Unique identifier of the parent e.g. `e61ddc8e-90ad-4d31-bb00-024987a5f2d1-en-us` | | Properties | [IEnterspeedProperty](https://github.com/enterspeedhq/enterspeed-sdk-dotnet/blob/master/documentation/entities/properties/README.md)> | Dictionary of property alias and value is the converted Enterspeed property | ### Example ```json theme={null} { "id": "e61ddc8e-90ad-4d31-bb00-024987a5f2d1", "type": "site", "parentId": "e61ddc8e-90ad-4d31-bb00-024987a5f2d1-en-us", "url": "https://example.com/about-us", "redirects": ["/about"], "properties": { "includeInNavigation": { "name": "includeInNavigation", "type": "boolean", "value": false }, "title": { "name": "title", "type": "string", "value": "Home" }, "metaData": {} } } ``` ### Meta data To process Sitecore specific properties we have added a `metaData` object that contains: | Name | Type | Description | | ------------------ | --------------------------------------- | ------------------------------------------------------------------------ | | name | string | Name of the item | | displayName | string | DisplayName of the item | | sitecoreId | string | Id of the Sitecore Item | | language | string | e.g. `en-us` | | sortOrder | number | What order the item is sorted in | | level | number | What level in the tree the item is in | | createDate | string | Date for when the item has been created | | updateDate | string | Date for when the node has last been updated | | updatedBy | string | Name of the user that has updated the item | | fullPath | string\[] | Ancestors id's | | languages | string\[] | Langauge versions available | | isAccessRestricted | boolean | Value determining if the item is restricted for anonymous users/visitors | | accessRestrictions | List of users and, and if they can read | | ```json theme={null} { "properties": { "metaData": { "name": "Name of the item", "displayName": "displayName of the item", "sitecoreId": "{0138AB78-5146-4A0C-82F5-DD2FB786C381}", "language": "en", "sortOrder": -463, "level": 4, "createDate": "2022-05-02T12:33:40", "updateDate": "2022-05-09T13:03:58", "updatedBy": "sitecore\\admin", "fullPath": [ "984169440b16415e8a04d19ca522caed-en", "a5fb76a88b9341c19d9f7ec0f04c654b-en", "4228f830dbc64303ba22fbb5faae4af6-en", "92a5b6dec6474ee4bce685475d10a650-en", "0138ab7851464a0c82f5dd2fb786c381-en" ], "languages": ["sv-SE", "en"], "isAccessRestricted": false, "accessRestrictions": [] } } } ``` # Typesense Source: https://docs.enterspeed.com/enterspeed/integrations/typesense The Enterspeed Typesense integration uses [destinations](/enterspeed/reference/js/full-schema/actions#destination) to send data from views directly to a configured Typesense cluster. This means that you can decide on schema level which views you want to send to Typesense. You will only have to set the destination field on the entity schema you want to send to Typesenses. All schema references are automatically resolved so you don't have to set it on all referenced schemas. It's possible to configure multiple Typesense destinations if you need to push different types of data to different Typesense clusters. ## Configuration In order to setup the Typesense configuration you need the following: | Setting | Description | | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | Typesense Server URL | The Server URL for the Typesense cluster you want to integrate to | | Typesense API Key | A Typesense Admin API key | | Enterspeed Environment Client API Key | The API key for an Enterspeed Environment client. This is used to fetch the view that will be inserted into Typesense | ## Schema options On Enterspeed schema level you have the following options: | Option | Required | Description | | ----------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | collection | Required | The Typesense collection the data should be send to | | id | Optional | The value that should be used as id in for the Typesense document. If omitted, the Enterspeed view id will be used | | dirtyValues | Optional | Defines what Typesense should do when the type of a particular field being indexed does not match the Typesense schema.

Valid values are: `coerce_or_reject` (default), `coerce_or_drop`, `drop`, `reject`

Refer to the [Typesense documentation](https://typesense.org/docs/latest/api/documents.html#dealing-with-dirty-data) for more information | ## Example of usage ```js title="Content schema with Typesense destination" theme={null} /** @type {Enterspeed.FullSchema} */ export default { triggers: function(context) { context.triggers('cms', ['content']); }, actions: function (sourceEntity, context) { context.destination('typesense').options({ id: sourceEntity.originId, collection: 'content' dirtyValues: 'coerce_or_reject' }); }, properties: function ({properties: p, url}, context) { return { product: { url: url, title: p.title, content: p.text } } } ``` ```json title="Content schema with Typesense destination" theme={null} { "triggers": { "cms": ["content"] }, "destinations": [ { "alias": "typesense", "options": { "id": "{originId}", "collection": "content", "dirtyValues": "coerce_or_reject" } } ], "properties": { "url": "{url}", "title": "{p.title}", "content": "{p.text}" } } ``` # Umbraco & Cloudinary Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco-cloudinary This is a add-on to the [Umbraco source integration](./umbraco/getting-started). This package will automatically upload the Umbraco media to Cloudinary and ingest the Cloudinary url to Enterspeed instead of the Umbraco url. **GitHub: [Enterspeed Source Umbraco CMS Cloudinary](https://github.com/enterspeedhq/enterspeed-source-umbraco-cms-cloudinary)** ## Installation The fastest way to get up and running is to install the Enterspeed Umbraco Cloudinary integration with NuGet. [NuGet: Enterspeed.Source.UmbracoCms.Cloudinary](https://www.nuget.org/packages/Enterspeed.Source.UmbracoCms.Cloudinary) ### Version support | Enterspeed.Source.UmbracoCms.Cloudinary | Enterspeed.Source.UmbracoCms | | --------------------------------------- | ---------------------------- | | V1 | V2.1.0 ≤ x \< 4.0.0 | | V2 | V4.0.0 ≤ x \< 4.3.0 | | V3 | V4.3.0 ≤ x \< 5.0.0 | | V4 | V5.0.0 > x | ## Configuration The only configuration specific to this add-on is the Cloudinary environment credentials. These can be configured in the `appsettings.json` file. ```json theme={null} "Enterspeed": { ... "Cloudinary": { "CloudName": "", // Required "ApiKey": "", // Required "ApiSecret": "", // Required "AssetFolder": "" // Optional } } ``` # Additional properties Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/additional-properties If you need to add additional properties to a content or media node before it's ingested into Enterspeed, you can use `EnterspeedPropertyDataMappers` or `EnterspeedPropertyMetaDataMappers`. You can also create a [Custom Property Service](/enterspeed/integrations/umbraco/enterspeed-propertys-service#custom-property-service) and override some of the `MapAdditionalProperties` methods, however mappers are more flexible if you are doing conditional mapping, like only adding additional properties for specific content types. ## Mappers With mappers you can create small effective classes to map additional properties for types that matches a filter, e.g. all nodes of a specific type. You can create mappers to add properties to the properties object or the meta-data object. Note that we in this example are using `StringEnterspeedProperty`. You can choose between multiple property types. (Array, boolean, number, object and so on.) ### Example ```csharp theme={null} // Data mapper example public class SlugDataMapper : IEnterspeedPropertyDataMapper { public bool IsMapper(IPublishedContent content) => content.ContentType.Alias == "contentPage"; public void MapAdditionalData(IDictionary data, IPublishedContent content, string culture) { // some logic data["slug"] = new StringEnterspeedProperty("slug", "mySlugValue"); } } ``` ```csharp theme={null} // Meta-data mapper example public class SlugMetaDataMapper : IEnterspeedPropertyMetaDataMapper { public bool IsMapper(IPublishedContent content) => content.ContentType.Alias == "contentPage"; public void MapAdditionalData(IDictionary metaData, IPublishedContent content, string culture) { // some logic metaData["slug"] = new StringEnterspeedProperty("slug", "mySlugValue"); } } ``` ### Registering your new property service The mappers are registered in Umbraco via an [IComposer](https://docs.umbraco.com/umbraco-cms/reference/using-ioc). **Umbraco 9+** ```csharp theme={null} public class MyCustomComposer : IComposer { public void Compose(IUmbracoBuilder builder) { builder.EnterspeedPropertyMetaDataMappers() .Append(); } } ``` **Umbraco 8** ```csharp theme={null} [RuntimeLevel(MinLevel = RuntimeLevel.Run)] public class MyCustomComposer : IUserComposer { public void Compose(Composition composition) { composition.EnterspeedPropertyMetaDataMappers() .Append(); } } ``` You should now be able to see the data you have mapped, in the data for your source entities in Enterspeed. ```json theme={null} "metaData": { "name": "Tattoo", "culture": "en-us", "sortOrder": 0, "level": 3, "createDate": "2022-09-05T15.48.36", "updateDate": "2022-09-05T15.48.37", "nodePath": [ "1097-en-us", "1098-en-us", "1099-en-us" ], "slug": "mySlugValue" 👈👈👈 } ``` # Background tasks Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/background-tasks Enterspeed is running two background tasks in Umbraco ## Umbraco 8 ### HandleEnterspeedJobsHostedService Task This task is handling all jobs that are "Pending". This will usually be when all content has been Seeded. This task will run every 60 seconds and will handle jobs in batches of 50 until there are no more Pending jobs. **Note** that there will only be one task running at a time, which means that if a task is already handling pending jobs, a new task will not be created. ### InvalidateEnterspeedJobsHostedService Task This task will change the state of jobs that has been "Processing" for more than 1 hour, to "Failed". This is done to clean up the Jobs queue. This task will run every 10 minutes. ## Umbraco 9+ ### HandleEnterspeedJobs Task This task is handling all jobs that are "Pending". This will usually be when all content has been Seeded. This task will run every 60 seconds and will handle jobs in batches of 50 until there are no more Pending jobs. **Note** that there will only be one task running at a time, which means that if a task is already handling pending jobs, a new task will not be created. ### InvalidateEnterspeedJobs Task This task will change the state of jobs that has been "Processing" for more than 1 hour, to "Failed". This is done to clean up the Jobs queue. This task will run every 10 minutes. # Content dashboard Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/content-dashboard The content dashboard is for the editor to get an overview of the Enterspeed integration, ie. seeding content and the data integrity: has something failed while being sent to Enterspeed Ingest and what failed? The Content dashboard lives in the Content section in the Umbraco backoffice. Umbraco v10 Content dashboard ## Failed jobs This list is to get an overview of what, if any, has failed while being sent to the Enterspeed Ingest API. If an entity fails to be sent, a row will be added and can be unfolded to see error details. Umbraco v10 Failed jobs ## Seed Seeding allows the editor to send all published content to Enterspeed for processing. Seed is an asynchronous action and can take a while to process. Umbraco v10 Seed Content # Culture and hostnames Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/culture-and-hostnames ## Domains for nodes that don't vary by culture Nodes based on content types that don't allow *vary by culture* will by default use the default language in Umbraco when trying to resolve the domain ([you can customize the logic needed](#customize-the-culture-logic)). **Example**\ Your Umbraco installation has *en-US* as the default language. The node you are ingesting or one of its ancestors has the following setup for Culture and hostname. | Domain | Culture | | -------------------------------------------------- | ------- | | [https://enterspeed.com/](https://enterspeed.com/) | en-US | | [https://enterspeed.dk](https://enterspeed.dk) | da-DK | If the node you are ingesting is based on a content type that does not allow *vary by culture* it will use the default language and because of that use [https://enterspeed.com/](https://enterspeed.com/) as the domain when ingested into Enterspeed. ## Multiple domains per culture for the same site For the moment Enterspeed can only handle one domain per culture in Culture and hostnames for each site node. If you have a multi-site Umbraco installation, with multiple sites nodes, you can have as many domains with the same culture as you want. This limitation is for scenarios where one site node has multiple domains with the same culture. ### Non-working Culture and Hostnames setup for the same site This setup would not work as expected: | Domain | Culture | | -------------------------------------------------- | ------- | | [https://enterspeed.com/](https://enterspeed.com/) | en-US | | [https://enterspeed.dk](https://enterspeed.dk) | en-US | It is expected that both URLs would serve the same content from Umbraco. In reality, only one of them would work in Enterspeed. This is because Umbraco defaults to one of the domains when no current URI is available, and Enterspeed must have a single URL. As the integration just uses Umbracos built-in IPublishedContent.Url() method. This method chooses the best suitable URL by culture. ### Working Culture and Hostnames setup This setup would work as expected: | Domain | Culture | | -------------------------------------------------- | ------- | | [https://enterspeed.com/](https://enterspeed.com/) | en-US | | [https://enterspeed.dk](https://enterspeed.dk) | da-DK | This setup would give English content the [https://enterspeed.com](https://enterspeed.com) **domain** and the danish content the [https://enterspeed.dk](https://enterspeed.dk). If you do require to have the same content and same language on multiple domains, you have to implement some logic on your frontend that handles that. ### Domains in Enterspeed It is possible to add more domains in Enterspeed if you wish your content to be available on multiple domains. ## Changing Culture and hostnames When you change a hostname in Culture and hostnames, you will manually have to re-seed the content into Enterspeed. This is currently a manual step, so please bare with us while we figure out the best way to automate this. ## Customize the culture logic If you want to customize the culture logic, eg. if you use another culture then the default culture for a specific site or node that does not vary by culture you can implement your own version of `UmbracoCultureProvider` either by implementing the `IUmbracoCultureProvider` interface or by extending the `UmbracoCultureProvider` class and override the methods you want to customize. ```csharp theme={null} public class CustomUmbracoCultureProvider : IUmbracoCultureProvider { public IEnumerable GetCulturesForCultureVariant(IContent content) { // My custom logic } public IEnumerable GetCulturesForCultureVariant(IPublishedContent content) { // My custom logic } public string GetCultureForNonCultureVariant(IContent content) { // My custom logic } public string GetCultureForNonCultureVariant(IPublishedContent content) { // My custom logic } } ``` ### Registration See examples of how to register your custom implementations [here](/enterspeed/integrations/umbraco/service-registration#custom-service-registrations). # Database Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/database ## Tables ### EnterspeedJobs table This table contains the jobs that need to be executed, in order to sync with Enterspeed. | Name | Type | Description | | --------- | -------- | --------------------------------------- | | Id | integer | Unique identifier, auto increments | | ContentId | integer | Id of the Umbraco node | | Culture | string | Culture of the Umbraco node | | JobType | integer | 0 = Publish, 1 = Delete | | JobState | integer | 0 = Pending, 1 = Processing, 2 = Failed | | Exception | string | Exception message if the job failed | | CreatedAt | datetime | Job creation date | | UpdatedAt | datetime | Job updated at | # Guards Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/enterspeed-guards # Enterspeed guards Guards are split by type - content or dictionary item, they serve the purpose of ensuring that data that is about to be ingested into Enterspeed, is successfully validated by predefined or your extended guard rules. ## Enterspeed content handling guard Our package already includes a single guard - `ContentCultureUrlRequiredGuard`, that ensures that if content for publishing to Enterspeed has culture, it must also have a URL for that specific culture available. To extend guards with your own one, you need to implement the `IEnterspeedContentHandlingGuard` interface. ### IEnterspeedContentHandlingGuard ```csharp theme={null} public interface IEnterspeedContentHandlingGuard { /// /// Validates if content can be ingested. /// /// Content for ingest. /// Culture of content. /// True or false, if is valid for ingest or not. bool CanIngest(IPublishedContent content, string culture); } ``` ### Registering a content handling guard Guards are registered in Umbraco via the [Composing](https://our.umbraco.com/documentation/implementation/composing/) functionality. **Umbraco 9+** ```csharp theme={null} public class MyCustomerGuardsComposer : IComposer { public void Compose(IUmbracoBuilder builder) { builder.EnterspeedContentHandlingGuards() .Append(); } } ``` **Umbraco 8** ```csharp theme={null} [RuntimeLevel(MinLevel = RuntimeLevel.Run)] public class MyCustomerGuardsComposer : IUserComposer { public void Compose(Composition composition) { composition.EnterspeedContentHandlingGuards() .Append(); } } ``` ## Enterspeed dictionary item handling guard To extend dictionary item guards with your own one, you need to implement the `IEnterspeedDictionaryItemHandlingGuard` interface. ### IEnterspeedDictionaryItemHandlingGuard ```csharp theme={null} public interface IEnterspeedDictionaryItemHandlingGuard { /// /// Validates if dictionary item can be ingested. /// /// Dictionary item for ingest. /// Culture of dictionary item. /// True or false, if is valid for ingest or not. bool CanIngest(IDictionaryItem dictionaryItem, string culture); } ``` ### Registering a dictionary item handling guard Guards are registered in Umbraco via the [Composing](https://our.umbraco.com/documentation/implementation/composing/) functionality. **Umbraco 9+** ```csharp theme={null} public class MyCustomerGuardsComposer : IComposer { public void Compose(IUmbracoBuilder builder) { builder.EnterspeedDictionaryItemHandlingGuards() .Append(); } } ``` **Umbraco 8** ```csharp theme={null} [RuntimeLevel(MinLevel = RuntimeLevel.Run)] public class MyCustomerGuardsComposer : IUserComposer { public void Compose(Composition composition) { composition.EnterspeedDictionaryItemHandlingGuards() .Append(); } } ``` # Enterspeed Property Service Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/enterspeed-propertys-service The Enterspeed property service has the role of mapping node data into usable objects for the data sources. Our package already includes a default Enterspeed property service, that maps all properties including some meta-data that is present on all data sources. You can extend the meta-data, to include relevant data for your solution, on all data sources. Examples could be site settings or domain name data. ## Custom Property Service To extend the properties or meta-data on sources, you would need to create your custom property service. This is easily done by inheriting the default property service. In this example, all default behavior of the `EnterspeedPropertyService` is preserved but grants us the option to extend meta-data. As you can see, the `MapAdditionalProperties` and `MapAdditionalMetaData` can be overridden. Here you only need to add your logic and include your own property + data. You can also override `MapAdditionalMediaProperties` and `MapAdditionalMediaMetaData` to extend media source entities. Note that we in this example are using `StringEnterspeedProperty`. You can choose between multiple property types. (Array, boolean, number, object and so on.) If you want different logic based on content type or other conditions, you should use [Mappers](/enterspeed/integrations/umbraco/additional-properties) instead. ### Example ```csharp theme={null} public class CustomPropertyService : EnterspeedPropertyService { public CustomPropertyService(EnterspeedPropertyValueConverterCollection converterCollection, IServiceProvider serviceProvider) : base(converterCollection, serviceProvider) { } protected override void MapAdditionalProperties(Dictionary data, IPublishedContent content, string culture) { data.Add("mySpecialKey", new StringEnterspeedProperty("my value fetched from my business logic")); } protected override void MapAdditionalMediaProperties(Dictionary data, IPublishedContent content, string culture) { data.Add("mySpecialKey", new StringEnterspeedProperty("my value fetched from my business logic")); } protected override void MapAdditionalMetaData(Dictionary metaData, IPublishedContent content, string culture) { metaData.Add("mySpecialKey", new StringEnterspeedProperty("my value fetched from my business logic")); } protected override void MapAdditionalMediaMetaData(Dictionary metaData, IPublishedContent content, string culture) { metaData.Add("mySpecialKey", new StringEnterspeedProperty("my value fetched from my business logic")); } } ``` ### Registering your new property service The property service is registered in Umbraco via an [IComposer](https://our.umbraco.com/documentation/implementation/composing/). The `AddUnique` extension method replaces the normal property service and implements your own. **Umbraco 9+** ```csharp theme={null} public class MyCustomComposer : IComposer { public void Compose(IUmbracoBuilder builder) { builder.Services.AddUnique(ServiceLifetime.Transient); } } ``` **Umbraco 8** ```csharp theme={null} [RuntimeLevel(MinLevel = RuntimeLevel.Run)] public class MyCustomComposer : IUserComposer { public void Compose(Composition composition) { composition.RegisterUnique(); } } ``` You should now be able to see the data you have mapped, in the meta-data object of your data sources in Enterspeed. ```json theme={null} "metaData": { "name": "Tattoo", "culture": "en-us", "sortOrder": 0, "level": 3, "createDate": "2022-09-05T15.48.36", "updateDate": "2022-09-05T15.48.37", "nodePath": [ "1097-en-us", "1098-en-us", "1099-en-us" ], "mySpecialKey": "my value fetched from my business logic" 👈👈👈 } ``` # Value Converter Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/enterspeed-value-converter # Enterspeed Value Converter A property value converter is a class that will convert the input value from Umbraco into an [IEnterspeedProperty](https://github.com/enterspeedhq/enterspeed-sdk-dotnet/tree/master/documentation/entities/properties). To implement your own converter you need to implement the IEnterspeedPropertyValueConverter interface ## IEnterspeedPropertyValueConverter This interface contains two methods that need to be implemented ### IsConverter ```csharp theme={null} bool IsConverter(IPublishedPropertyType propertyType); ``` This method is called when the EnterspeedPropertyService tries to find the proper converter for this property. An implementation of this method could look like this: ```csharp theme={null} public bool IsConverter(IPublishedPropertyType propertyType) { return propertyType.EditorAlias.Equals("Umbraco.TextBox"); } ``` ### Convert ```csharp theme={null} IEnterspeedProperty Convert(IPublishedProperty property, string culture); ``` This is the method that is converting the Umbraco property to an IEnterspeedProperty. An implementation of this method could look like this: ```csharp theme={null} public IEnterspeedProperty Convert(IPublishedProperty property, string culture) { var value = property.GetValue(culture); return new StringEnterspeedProperty(property.Alias, value); } ``` ## Registering a converter Converters are registered in Umbraco via an [IComposer](https://docs.umbraco.com/umbraco-cms/reference/using-ioc). **Umbraco 9+** ```csharp theme={null} public class MyCustomerPropertyValueConverterComposer : IComposer { public void Compose(IUmbracoBuilder builder) { builder.EnterspeedPropertyValueConverters() .Append(); } } ``` **Umbraco 8** ```csharp theme={null} [RuntimeLevel(MinLevel = RuntimeLevel.Run)] public class MyCustomerPropertyValueConverterComposer : IUserComposer { composition.EnterspeedPropertyValueConverters() .Append(); } ``` **Umbraco 7** ```csharp theme={null} public class RegisterCustomPropertyValueConverters : ApplicationEventHandler { protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) { EnterspeedContext.Current.EnterspeedPropertyValueConverters .Append(); } } ``` Note that the EnterspeedPropertyService will find the converters in the order that they are registered, which means that if you want to replace a default converter with your own, you need to insert your converter like this: **Umbraco 9+** ```csharp theme={null} [ComposeAfter(typeof(EnterspeedComposer))] public class MyCustomerPropertyValueConverterComposer : IComposer { public void Compose(IUmbracoBuilder builder) { builder.EnterspeedPropertyValueConverters() .InsertBefore(); } } ``` **Umbraco 8** ```csharp theme={null} [RuntimeLevel(MinLevel = RuntimeLevel.Run)] public class MyCustomerPropertyValueConverterComposer : IUserComposer { composition.EnterspeedPropertyValueConverters() .InsertBefore(); } ``` **Umbraco 7** ```csharp theme={null} public class RegisterCustomPropertyValueConverters : ApplicationEventHandler { protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) { EnterspeedContext.Current.EnterspeedPropertyValueConverters .InsertBefore(); } } ``` ## Default converters Enterspeed ships with [default property value converters](https://github.com/enterspeedhq/enterspeed-source-umbraco-cms/blob/master/documentation/enterspeed-value-converters/property-value-converters/defaults/README.md) for all the built-in property editors that Umbraco ships with out of the box. # Getting data Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/getting-data # Getting data from Umbraco to Enterspeed When you press Save and publish on a piece of content in Umbraco, Umbraco handles the content as normally: validation, trigger events, etc. The integration into Enterspeed is simply just a couple of events that the integration listens to. ## When content is being published When a piece of content is published in Umbraco, the integration reacts to this and adds a job for the process in the database. Each job is unique and will only be processed once by the integration. You can read more about the jobs in the reference section. Directly after the job is added to the database it will be processed by the integration and sent to the Enterspeed Ingest API for Enterspeed to process it and deliver it to the Delivery API. ## UmbracoContentEntity explained To make sure you understand the whole process from end to end, an important detail is how the UmbracoContentEntity is created, because these properties define the entity you work with in Enterspeed. The properties within the properties object don't change when it gets ingested in Enterspeed, this means that when you are to create your Schemas to model your API, you can rely 1:1 on what you ingest and what you can query in Enterspeed. ## Seeding content to Enterspeed If you have an existing site, have installed Enterspeed later in the development process or just want to make sure all your content is in Enterspeed, you can seed or re-seed all the content from Umbraco into Enterspeed. This can simply be done by going to the Enterspeed Content dashboard and then going to Seed and pressing the button. In theory, the same happens when you press Save and publish. The seeding pulls all the content from the published cache and inserts a new EnterspeedJob in the database for each piece of content. Umbraco v10 Seed Content Every minute the database is checked for pending entities and if it has any it will be sent to the Enterspeed Ingest API, equivalent to the process from steps 3 to 7 in Publishing step by step. You can read more about the jobs in the reference section. # Getting started Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/getting-started # Getting started with Umbraco & Enterspeed The easiest way to get started with Umbraco and Enterspeed is by using the pre-built Umbraco integration. **GitHub: [Enterspeed Source Umbraco CMS](https://github.com/enterspeedhq/enterspeed-source-umbraco-cms)** This integration takes care of calling the Enterspeed Ingest API when changes occur in Umbraco. For a full overview of what Umbraco entities are sent to Enterspeed, please see Umbraco entities. ## Installation The fastest way to get up and running is to install the Enterspeed Umbraco integration with NuGet. **NuGet:**\ [Umbraco 7](https://www.nuget.org/packages/Enterspeed.Source.UmbracoCms.v7/)\ [Umbraco 8](https://www.nuget.org/packages/Enterspeed.Source.UmbracoCms.v8/)\ [Umbraco 9+](https://www.nuget.org/packages/Enterspeed.Source.UmbracoCms/) **Using Umbraco 9+**
*Nuget package (Enterspeed.Source.UmbracoCms)
* ***Version 5.x:*** Use this if your Umbraco version is 14 or higher.
***Version 4.x:*** Use this if your Umbraco version is 13 or lower. **Using Umbraco Cloud?**
If you have used the Umbraco Cloud UaaS.cmd tool to set up your solution, you need to manually update the referenced dlls after installing the Enterspeed NuGet package. Specifically, Microsoft.Bcl.AsyncInterfaces.dll needs to be updated. From Visual Studio navigate to the \[Namespace].Web\bin folder, and right-click on Microsoft.Bcl.AsyncInterfaces.dll, and select "Update Reference".
When the installation above has been completed two new dashboards have been added to your Umbraco solution. * **Content:** To seed and check errors when data is ingested * **Settings:** To configure Enterspeed in Umbraco ## Configuration Before Umbraco starts sending data to Enterspeed you will need to add a little piece of configuration. Luckily this can easily be done within Umbraco itself or via [appsettings.json](#app-settings-umbraco-9) (Umbraco 9 and up) or [Web.config](#webconfig-umbraco-7-or-8). (Umbraco 7 and 8) ### Source API key and Ingest endpoint Firstly go to Settings and then select the Enterspeed Settings dashboard in your Umbraco backoffice. You should see something like this: Umbraco v10 Enterspeed Settings #### Enterspeed endpoint The Enterspeed endpoint is the ingest endpoint, that Umbraco will use to send content to Enterspeed. Unless you have gotten a specific Enterspeed endpoint to call, please use: [https://api.enterspeed.com](https://api.enterspeed.com/) #### Media domain The Media domain is to tell the Enterspeed integration where you have your media placed, e.g. if you have a CDN. If you leave it empty, it will just use your current Umbraco installation domain. ### API key Before you can insert an API key, you must have created a Source within the [Enterspeed Management](https://app.enterspeed.com/settings/data-sources). The API key looks something like this: source-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. When you have gotten it, insert it in the API key input field. ### Preview API key The optional API key serves the purpose of ingesting draft and unpublished content to the secondary (preview) source. Can be leveraged for your content editors to preview content, similarly to the in-built 'Save & Preview' functionality in Umbraco. Before you can insert a Preview API key, you must have created a Source within the [Enterspeed Management](https://app.enterspeed.com/settings/data-sources). Enterspeeds connector will automatically push data to relevant primary or secondary sources based on actions performed in Umbraco backoffice, such as - Unpublish, trash, save, publish, etc. ### Testing connection When you have inserted the Enterspeed endpoint and the API key(-s) click on Test connection and make sure that you get a successful response. When you do go ahead and Save the configuration. ## App settings (Umbraco 9+) For appsettings.json or environment-specific settings file, please use the following appSettings example: ```json theme={null} { ... "Enterspeed": { "Endpoint": "https://api.enterspeed.com", // required (deprecated - replaced by `BaseUrl` in 4.4.0 and 5.3.0 but still works as fallback) "BaseUrl": "", // optional (default https://api.enterspeed.com) "Apikey": "", // required "MediaDomain": "", // optional "PreviewApikey": "", // optional "EnabledFailedJobsProcessing" : boolean, // optional (default false) "EnableMasterContent": boolean, // optional (default false) "RemoveTrailingSlash": boolean, // optional (default false) "RootDictionariesDisabled": "", // optional (default false) "RunJobsOnAllServerRoles": "", // optional (default false) } ... } ``` There are additional settings that can only be configured in the app settings file. These settings are applicable only for Umbraco 9 and later versions. ### Enable Failed Jobs Processing This feature automatically reprocesses recent failed jobs. It will retry up to five times. ### Master Content Allows ingestion of a master variant whenever a language variant is updated. Similarly, the master variant will be deleted when the last language variant is removed. Master variants have the type of the node with the postfix -master, enabling schemas to be created specifically for the master variants. ### Remove Trailing Slash Removes trailing slashes from all URLs. ### Root Dictionaries Disabled Root dictionaries are enabled by default. This functionality ensures that a root source entity is created for the dictionaries in Enterspeed, allowing the use of handles in Enterspeed. ## Run Jobs in All Server Roles Enables job processing for all Umbraco server roles. In most cases, this is not recommended, as multiple servers can pick up the same jobs from the shared database, resulting in multiple ingests of the same jobs. ## Web.config (Umbraco 7 or 8) For Web.config, please use the following appSettings example: ```xml theme={null} ``` ## Processed Umbraco entities Here is an overview of what is processed and send to Enterspeed and what is not. ### Processed entities * Published content * Draft content (unpublished content/saved content) * Media * Dictionary ### Not processed entities * Members * Users # Grid Editor Value Converters Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/grid-editor-value-converters # Enterspeed Grid Editor Value Converters A grid editor value converter is a class that will convert the input value from an Umbraco grid editor into an [IEnterspeedProperty](https://github.com/enterspeedhq/enterspeed-sdk-dotnet/tree/master/documentation/entities/properties). To implement your own converter you need to implement the IEnterspeedGridEditorValueConverter interface If no converter is registered for the grid editor, the DefaultGridLayoutPropertyValueConverter will try to convert it into an IEnterspeedProprety automatically, by looking at the types. ## IEnterspeedGridEditorValueConverter This interface contains two methods that need to be implemented ### IsConverter ```csharp theme={null} bool IsConverter(string alias); ``` This method is called when the EnterspeedGridEditorService tries to find the correct converter for this grid editor. An implementation of this method could look like this: ```csharp theme={null} public bool IsConverter(string alias) { return alias.InvariantEquals("rte"); } ``` ### Convert ```csharp theme={null} IEnterspeedProperty Convert(GridControl editor, string culture) ``` This is the method that is converting the Umbraco grid editor to an IEnterspeedProperty. An implementation of this method could look like this: ```csharp theme={null} public IEnterspeedProperty Convert(GridControl editor, string culture) { return new StringEnterspeedProperty(editor.Value.ToString()); } ``` ## Registering a converter Converters are registered in Umbraco via an [IComposer](https://our.umbraco.com/documentation/implementation/composing/). **Umbraco 9+** ```csharp theme={null} public class MyCustomerGridEditorValueConverterComposer : IComposer { public void Compose(IUmbracoBuilder builder) { builder.EnterspeedGridEditorValueConverters() .Append(); } } ``` **Umbraco 8** ```csharp theme={null} [RuntimeLevel(MinLevel = RuntimeLevel.Run)] public class MyCustomerGridEditorValueConverterComposer : IUserComposer { composition.EnterspeedGridEditorValueConverters() .Append(); } ``` Note that the EnterspeedGridEditorService will find the converters in the order that they are registered, which means that, if you want to replace a default converter with your own, you need to insert your converter like this: **Umbraco 9+** ```csharp theme={null} public class MyCustomerPropertyValueConverterComposer : IComposer { public void Compose(IUmbracoBuilder builder) { builder.EnterspeedGridEditorValueConverters() .InsertBefore(); } } ``` **Umbraco 8** ```csharp theme={null} [RuntimeLevel(MinLevel = RuntimeLevel.Run)] public class MyCustomerPropertyValueConverterComposer : IUserComposer { composition.EnterspeedGridEditorValueConverters() .InsertBefore(); } ``` ## Default converters Enterspeed ships with [default grid editor value converters](https://github.com/enterspeedhq/enterspeed-source-umbraco-cms/tree/master/documentation/enterspeed-value-converters/grid-editor-value-converters/defaults) for some of the built-in grid editors that Umbraco ships with out of the box. # Jobs Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/jobs A job is an instruction that needs to be executed in order to synchronize data with Enterspeed. A job contains information about what content from Umbraco, that needs to be handled, and how it should be handled (ie. Publish or Delete). Jobs are stored in a custom table. ## Job lifecycle When a content node in Umbraco is being published, unpublished, deleted or moved a new job will be added to the EnterspeedJobs table with the state of "Pending". Immediately after the jobs have been created, they will be handled by the EnterspeedJobHandler, which is changing the state of the jobs to "Processing". A job will then either be deleted if it could be handled without any errors, or set to "Failed" with the exception(s) that was thrown. ## Seeding jobs In the Enterspeed Content dashboard located under Content in Umbraco, you have the possibility to Seed all your content. When the button is clicked, all content will be queued up for publishing to Enterspeed. This means that for each published content node (and variant) in Umbraco, there will be created a publishing job. This will only create publishing jobs and not delete jobs. ## Old processing jobs If a job has been in the state of "Processing" for more than 1 hour, the state of that job will automatically change to "Failed". This is done to clean up jobs that, for some reason timed out while processing. This is done by the InvalidateEnterspeedJobsHostedService background task. ## IEnterspeedJobHandler The `IEnterspeedJobHandler` is responsible for deciding if it can support and process the requested job. The common process consists of fetching data from Umbraco, converting it to an `IEnterspeedEntity` and sending it to the Enterspeed Ingest API. For each job that is being handled all previously failed jobs, for the same content node will be fetched and deleted, so we only have a maximum of 1 failed job per content node with the recent exception. If a previously failed job is handled with success, the failed job will also be deleted, since it's no longer failing. To implement your own job handler you need to implement the `IEnterspeedJobHandler` interface. ### CanHandle ```csharp theme={null} bool CanHandle(EnterspeedJob job); ``` This method is called when the Enterspeed jobs handling service tries to find a proper handler for this job. An implementation of this method could look like this: ```csharp theme={null} public bool CanHandle(EnterspeedJob job) { return // Check if 'Main source/Publish' is configured _enterspeedConnectionProvider.GetConnection(ConnectionType.Publish) != null // Check if current job is for 'Content node' && job.EntityType == EnterspeedJobEntityType.Content // Check if content changes were published, rather than saved as draft && job.ContentState == EnterspeedContentState.Publish // Check if we want to Ingest, instead of deleting content && job.JobType == EnterspeedJobType.Publish; } ``` ### Handle ```csharp theme={null} void Handle(EnterspeedJob job); ``` This is the method that is responsible for processing job - lookup relevant Umbraco data, executing validation, mapping, and ingestion. An implementation of this method could look like this: ```csharp theme={null} public void Handle(EnterspeedJob job) { using (var context = _umbracoContextFactory.EnsureUmbracoContext()) { var content = GetContent(job, context); if (!CanIngest(content, job)) { return; } var umbracoData = CreateUmbracoContentEntity(content, job); Ingest(umbracoData, job); } } ``` ## Registering a job handler Job handlers are registered in Umbraco via an [IComposer](https://our.umbraco.com/documentation/implementation/composing/). Example: ```csharp theme={null} public class MyCustomJobHandlersComposer : IComposer { public void Compose(IUmbracoBuilder builder) { builder.EnterspeedJobHandlers() .Append(); } } ``` Note that the Enterspeed jobs handling service will find the handlers in the order that they are registered, which means that if you want to replace some of the default handlers with your own, you need to insert your handler like this: ```csharp theme={null} public class MyCustomJobHandlersComposer : IComposer { public void Compose(IUmbracoBuilder builder) { builder.EnterspeedJobHandlers() .InsertBefore(); } } ``` ## Default job handlers | Name | Action | Triggered by | | ------------------------------------------------ | --------------- | --------------------------------- | | EnterspeedContentPublishJobHandler | Ingest - save | Published content | | EnterspeedContentDeleteJobHandler | Ingest - delete | Trashed/unpublished content | | EnterspeedDictionaryItemPublishJobHandler | Ingest - save | Saved dictionary item | | EnterspeedDictionaryItemDeleteJobHandler | Ingest - delete | Deleted dictionary item | | EnterspeedPreviewContentPublishJobHandler | Ingest - save | Saved draft content | | EnterspeedPreviewContentDeleteJobHandler | Ingest - delete | Trashed/unpublished draft content | | EnterspeedPreviewDictionaryItemPublishJobHandler | Ingest - save | Saved dictionary item | | EnterspeedPreviewDictionaryItemDeleteJobHandler | Ingest - delete | Deleted dictionary item | [Source code of Job handling related classes (Umbraco 8)](https://github.com/enterspeedhq/enterspeed-source-umbraco-cms/tree/master/src/Enterspeed.Source.UmbracoCms.V8/Handlers)\ [Source code of Job handling related classes (Umbraco 9+)](https://github.com/enterspeedhq/enterspeed-source-umbraco-cms/tree/master/src/Enterspeed.Source.UmbracoCms/Handlers) ## In case, of more customization If specifying new job handlers is not enough, or you want to change the flow of how job handlers are assigned and jobs are handled: * [`IEnterspeedJobsHandler Umbraco 8`](https://github.com/enterspeedhq/enterspeed-source-umbraco-cms/blob/master/src/Enterspeed.Source.UmbracoCms.V8/Handlers/EnterspeedJobsHandler.cs) * [`IEnterspeedJobsHandler Umbraco 9+`](https://github.com/enterspeedhq/enterspeed-source-umbraco-cms/blob/master/src/Enterspeed.Source.UmbracoCms/Handlers/EnterspeedJobsHandler.cs) # Publishing Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/publishing # Publishing step by step When the editor publishes a node the cache refreshed event is triggered and the Enterspeed Umbraco integration listens to this event, to send the updated content to Enterspeed. When this event is triggered these steps are executed synchronously: 1. A new EnterspeedJob is added to a database for processing. 2. The newly added job is requested to be processed immediately. 3. The published content from Umbraco is fetched from the cache. 4. Each property on the IPublishedContent is converted to an IEnterspeedProperty with an IEnterspeedPropertyValueConverter. 5. A UmbracoContentEntity is created. 6. The content entity is sent to the Enterspeed Ingest API. 7. If the response is successful nothing happens, if it is not: 1. The EnterspeedJob is re-inserted in the database as failed. 2. The failed jobs can be viewed on the content dashboard. If you wish you can check out the sequence diagram for the process here: ```bash theme={null} @startuml title Umbraco - publishing hide footbox actor "Editor" as EDITOR boundary "Umbraco" as UMBRACO participant "Cache refreshed" as CACHE_REFRESHED database "Jobs" as JOBS participant "Job service" as JOBS_SERVICE participant "Enterspeed Property Service " as PROPERTY_SERVICE participant "IEnterspeedPropertyValueConverter\nimplementation" as PROPERTY_VALUE_CONVERTER boundary "Enterspeed Ingest" as INGEST EDITOR -> UMBRACO: Publish content UMBRACO -> CACHE_REFRESHED: Content has been updated CACHE_REFRESHED -> JOBS: Insert EnterspeedJobs to process CACHE_REFRESHED -> JOBS_SERVICE: Process jobs synchronously JOBS_SERVICE -> JOBS: Fetch jobs for process JOBS -> JOBS_SERVICE: return EnterspeedJob[] JOBS_SERVICE -> PROPERTY_SERVICE: Get properties loop Foreach property in properties PROPERTY_SERVICE -> PROPERTY_VALUE_CONVERTER: Create IEnterspeedProperty object PROPERTY_VALUE_CONVERTER --> PROPERTY_SERVICE: return IEnterspeedProperty end PROPERTY_SERVICE -> JOBS_SERVICE: return Dictionary JOBS_SERVICE -> JOBS_SERVICE: Create IEnterspeedEntity JOBS_SERVICE -> INGEST: Send IEnterspeedEntity to Enterspeed INGEST -> JOBS_SERVICE: Response alt Response is not successful JOBS_SERVICE -> JOBS: Insert failed job end ``` Umbraco publishing # Redirects Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/redirects ## Umbraco Redirects If you use the build-in Umbraco Redirect URL Management the redirects will be ingested into Enterspeed out of the box. Umbraco Content Structure ## Custom Redirects If you are using a 3. party redirect plugin in your Umbraco installation you can implement your own version of the `UmbracoRedirectsService` either by implementing the `IUmbracoRedirectsService` interface or by extending the `UmbracoRedirectsService` class and overriding the \`GetRedirects\`\` method. ### Implementing interface ```csharp theme={null} public class CustomUmbracoRedirectsService : IUmbracoRedirectsService { private readonly IRedirectUrlService _redirectUrlService; private readonly IUmbracoUrlService _umbracoUrlService; public CustomUmbracoRedirectsService(IRedirectUrlService redirectUrlService, IUmbracoUrlService umbracoUrlService) { _redirectUrlService = redirectUrlService; _umbracoUrlService = umbracoUrlService; } public virtual string[] GetRedirects(Guid contentKey, string culture) { // My custom logic } } ``` ### Overriding ```csharp theme={null} public class CustomUmbracoRedirectsService : UmbracoRedirectsService { public CustomUmbracoRedirectsService(IRedirectUrlService redirectUrlService, IUmbracoUrlService umbracoUrlService) : base(redirectUrlService, umbracoUrlService) { } public override string[] GetRedirects(Guid contentKey, string culture) { var umbracoRedirects = base.GetRedirects(contentKey, culture); // logic for getting custom redirects return umbracoRedirects.Concat(customRedirects).ToArray(); } } ``` ### Registration See examples of how to register your custom implementations [here](/enterspeed/integrations/umbraco/service-registration#custom-service-registrations). # Service registration Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/service-registration In a default Umbraco installation all Enterspeed services will automatically be registered because of the call to `.AddComposers()` in the `ConfigureServices` method of the `Startup` class. ```csharp theme={null} public class Startup { ... public void ConfigureServices(IServiceCollection services) { services.AddUmbraco(_env, _config) .AddBackOffice() .AddWebsite() .AddDeliveryApi() .AddComposers() .Build(); } ... } ``` ## Manual registration If you, for what ever reason, have removed the `.AddComposers()` call and manually have registered the Umbraco services, you can register Enterspeed using the `.AddEnterspeed()` method. ```csharp theme={null} public class Startup { ... public void ConfigureServices(IServiceCollection services) { services.AddUmbraco(_env, _config) .AddBackOffice() .AddWebsite() .AddDeliveryApi() .AddEnterspeed() .Build(); } ... } ``` ## Custom service registrations Sometimes you want to overwrite existing functionality with your own implementation. You can do that by implementing existing interfaces or overwrite existing classes and then register your new custom types. You do that by creating an Umbraco composer class - see example below. You can also read more about [dependency injection in the Umbraco documentation](https://docs.umbraco.com/umbraco-cms/reference/using-ioc) ```csharp theme={null} [ComposeAfter(typeof(EnterspeedComposer))] public class CustomComposer : IComposer { public void Compose(IUmbracoBuilder builder) { builder.Services.AddUnique(ServiceLifetime.Transient); } } ``` # Services Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/services ## EnterspeedPropertyService : IEnterspeedPropertyService This service is used for converting a Umbraco property to an IEnterspeedProperty. ### Methods ```csharp theme={null} IDictionary GetProperties (IPublishedContent content, string culture = null); IDictionary ConvertProperties (IEnumerable properties, string culture = null; ``` Both methods will find the correct registered Enterspeed Property Value Converter and convert the value to an [IEnterspeedProperty](https://github.com/enterspeedhq/enterspeed-sdk-dotnet/tree/master/documentation/entities/properties). ## EnterspeedGridEditorService : IEnterspeedGridEditorService This service is used for converting a Umbraco grid editor value to an IEnterspeedProperty. ### Methods ```csharp theme={null} IEnterspeedProperty ConvertGridEditor(GridControl control, string culture = null) ``` This will find the correct registered Enterspeed Grid Editor Value Converter and convert the value to an [IEnterspeedProperty](https://github.com/enterspeedhq/enterspeed-sdk-dotnet/tree/master/documentation/entities/properties) # Troubleshooting Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/troubleshooting A section of some of the common issues that might occour. ### Ingest skipped if audit/notification pipeline is exisited by other extensions If another pipeline is changing the audit event or breaking the notification flow, this could cause the Enterspeed Umbraco source package to skip the ingest, as we listen for specific notifications (PublishVariant, Publish, Move) An example of this below ```csharp theme={null} public class ContentEventsNotificationNotificationHandler : INotificationAsyncHandler, { public async Task HandleAsync(ContentPublishingNotification notification, CancellationToken cancellationToken) { // Prematurely exiting return; } } ``` ### Does updating a domainon a root node in Umbraco trigger a re-ingest of the entire sub-tree? No. Changing a domain on a root node will not trigger a re-seed of the node and its descendants. If you want to trigger a re-seed in Umbraco after changing the domain, the best option is to use the "Publish with descendants"-feature in Umbraco. After updating the domain, click the node you changed the domain on and select "Publish with descendants". This will send the updates node and its descendants to Enterspeed. # Umbraco Cloud Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/umbraco-cloud # Getting started with Umbraco Cloud If you are building a new site or you are new to Umbraco Cloud, you should refer to [https://our.umbraco.com/documentation/Umbraco-Cloud/Getting-started/](https://our.umbraco.com/documentation/Umbraco-Cloud/Getting-started/). # Installation Described in [Getting started](/enterspeed/integrations/umbraco/getting-started#installation) # UmbracoContentEntity Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/umbraco-content-entity The UmbracoContentEntity is the concrete Umbraco specific implementation of the [IEnterspeedEntity](https://github.com/enterspeedhq/enterspeed-sdk-dotnet/tree/master/documentation/entities). ## Implementation details ### Abstract | Name | Type | Description | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | | Id | string | Unique identifier ie. "1078-en-us" | | Type | string | ContentType alias | | Url | string | The current URL of the content, either relative or absolute | | Redirects | string\[] | Array of redirects for the node | | ParentId | string | Unique identifier of the parent ie. "1078-en-us" | | Properties | [IEnterspeedProperty](https://github.com/enterspeedhq/enterspeed-sdk-dotnet/blob/master/documentation/entities/properties/README.md)> | Dictionary of property alias and value is the converted Enterspeed property | ### Example ```json theme={null} { "id": "1055-en-us", "type": "site", "parentId": "1054-en-us", "url": "https://example.com/about-us", "redirects": ["/about"], "properties": { "includeInNavigation": { "name": "includeInNavigation", "type": "boolean", "value": false }, "title": { "name": "title", "type": "string", "value": "Home" }, "metaData": {} } } ``` ### Meta data To process Umbraco-specific properties we have added a `metaData` object that contains: | Name | Type | Description | | ---------- | --------- | -------------------------------------------- | | culture | string | ie. en-us | | nodeName | string | Name of the node | | createDate | string | Date for when the node has been created | | updateDate | string | Date for when the node has last been updated | | nodePath | string\[] | Path to ancestor nodes in the tree | | sortOrder | number | What order the nodes are sorted in | | level | number | What level in the tree the node has | ```json theme={null} { "properties": { "metaData": { "culture": { "name": "culture", "type": "string", "value": "en-US" }, "nodeName": { "name": "nodeName", "type": "string", "value": "This is the name of a node" }, "createDate": { "name": "createDate", "type": "string", "value": "09-12-2020T10:49:01:00" }, "updateDate": { "name": "updateDate", "type": "string", "value": "10-12-2020T10:49:01:00" }, "nodePath": { "name": "nodePath", "type": "array", "items": [ { "name": null, "type": "number", "value": 1061, "precision": 0 }, { "name": null, "type": "number", "value": 1062, "precision": 0 }, { "name": null, "type": "number", "value": 1063, "precision": 0 } ] }, "sortOrder": { "name": "sortOrder", "type": "number", "value": 1, "precision": 0 }, "level": { "name": "level", "type": "number", "value": 1, "precision": 0 } } } } ``` # Tagging in Umbraco Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/umbraco-tagging The following article describes an example of tagging implementation for content in Umbraco, and how to filter source entities by tag in Enterspeed. As an example, we are going to build a basic news portal about sports, named "Sports Central". ## Content preparation in Umbraco In order to build the site's content structure and filter it in Enterspeed, we have to do some groundwork as - creating document types, data types and adding some articles as content. Umbraco Content Structure ## Document types ### Article Used for article content nodes themselves, with the following properties: | Property | Data type | Description | | ----------- | ----------- | -------------------------------------- | | title | Textstring | Title for the article | | subheader | Textstring | Subheader for the article | | publishedAt | DatePicker | Published display date for the article | | tags | Tags Picker | Related tags for the article | ### Period Group Is a document type without any properties, in the content tree, it will be used a grouping folder for articles per month, e.g. 09/2021. ### Articles Is a document type for grouping all 'Period group' content nodes. ### Settings Document type contains various settings for the site and different data types. ### Tags Used as container node for listing all possible tags underneath it in the content tree. ### Tag Tag, that can be news article can be associated with. ### Articles By Tag Is an *Element Type* document type, describing a block for displaying articles by a specific tag. Will be used on the front page of the site. | Property | Data type | Description | | -------- | ----------------- | ---------------------------------------------------------------- | | title | Textstring | Title used for the block, f.x. - *'Latest from football world'.* | | tag | Single Tag Picker | A tag that you want to include articles for. | | top | Numeric | Define how many articles you want to see. | ### Site Document type representing site instance. | Property | Data type | Description | | -------- | -------------------- | -------------------------------------------------------------------------------------------------- | | blocks | Frontpage Block List | Our front page will consist of different blocks as - articles by tag, and potentially many others. | ## Data types ### Single Tag Picker Is a data type extending Umbraco default 'Content Picker', where we specify 'Tags' content node as a start node. ### Frontpage Block List Is a data type extending Umbraco default 'Block List', where we add a single available block - 'Articles By Tag', that we have created previously as part of Document types. ## Adding blocks to frontpage On the site node, we are going to add some blocks for displaying articles by different tags. Umbraco blocks Each of them configured with their own criteria for display of articles: Umbraco blocks Content Umbraco blocks Content basketball ## Defining schemas in Enterspeed ### Creating a Site schema Site schema will work with source entities of type - 'site' since those are the ones where we have defined our blocks (in Umbraco) to describe what blocks we want to display. This schema doesn't contain much of the logic, its responsibility is to iterate over blocks defined on the source entity and pass each of the blocks in partial view, with matching name. ```json theme={null} { "route": { "url": "{url}" }, "triggers": { "umbraco": ["site"] }, "properties": { "blocks": { "type": "array", "input": "{p.blocks}", "items": { "type": "partial", "input": "{item}", "alias": "Block-{item.contentType}" } } } } ``` The only tricky part to remember is that now we have defined an alias match for partial schema view, where `'item.contentType'` value is an alias of our current block Element document type in Umbraco - `'articlesByTag'`. ## Creating a partial schema - Articles By Tag Block We are halfway through. The previous schema iterated through blocks, this schema will handle a specific type of block - `Block-articlesByTag`. As result, we want to display the following properties - `alias` and `title` of this block and collection of `articles` matching criteria defined on this block along with basic information about them - `title`, `subheader`, `publishedAt` date and a list of `tags` associated. ```json theme={null} { "alias": "Block-articlesByTag", "properties": { "alias": "{item.contentType}", "title": "{item.content.title}", "articles": { "type": "array", "input": { "$lookup": { "filter": "type eq 'article' and properties.tags/any(t: t.id eq '{item.content.tag}')", "top": "{item.content.top}", "orderBy": { "property": "p.publishedAt", "sort": "desc" } } }, "items": { "type": "object", "properties": { "title": "{item.p.title ?? item.p.metaData.name}", "subheader": "{item.p.subheader}", "publishedAt": "{item.p.publishedAt}", "tags": { "type": "array", "input": "{item.p.tags}", "var": "tag", "items": "{tag.name}" } } } } } } ``` Notice how we are using `$lookup` in combination with `filter`, `top`, and `orderBy`. In simple words, we want to lookup all source entities that match our filter, meaning where the type of source entity is an article and it has a current block selected tag associated with it. On our front page, we want to show the latest published news first. That is what we define in our sorting criteria for all filters found in source entities. Lastly, regarding lookup, we defined a limit on how many articles will be displayed for the current block. ## Outcome After updating and deploying previously mentioned schemas and publishing all content data to Enterspeed, the outcome from the Delivery API would look like this: ```json theme={null} { "meta": { "status": 200, "redirect": null }, "route": { "blocks": [ { "alias": "articlesByTag", "title": "Hey Vancouver", "articles": [ { "title": "Canucks camp notebook: Sporting new look, Boeser striving for consistency", "subheader": "Dan Murphy and Iain MacIntyre discuss the latest news surrounding Elias Pettersson and Quinn Hughes, as well as what's happening with Travis Hamonic", "publishedAt": "09/28/2021 00:00:00", "tags": ["Vancouver Canucks", "Ice hockey"] }, { "title": "Canucks Sign Forward Jason Dickinson", "subheader": "...to a three-year contract", "publishedAt": "09/02/2021 00:00:00", "tags": ["Ice hockey", "Vancouver Canucks"] }, { "title": "NHL expects full capacity in all cities except Vancouver and Montreal", "subheader": "", "publishedAt": "08/30/2021 00:00:00", "tags": ["Vancouver Canucks", "Ice hockey"] } ] }, { "alias": "articlesByTag", "title": "What is happening in #basketball?", "articles": [ { "title": "Position breakdown: Maximizing Porzingis a priority among big men", "subheader": "When you have one of the tallest players in the NBA, one with a rare skill set of inside and outside capabilities, it makes sense to maximize his assets.", "publishedAt": "09/27/2021 00:00:00", "tags": ["Basketball"] }, { "title": "Will Kidd get the best out of Doncic?", "subheader": "The savvy former guard looks to bring his Hall of Fame expertise to Dallas.", "publishedAt": "09/14/2021 00:00:00", "tags": ["Basketball"] } ] } ] }, "views": {} } ``` # Updating package Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/updating-package We're trying our best to make sure that your experience is as smooth as possible, but sometimes things might not work as expected. After the package upgrade, it is recommended to do a hard refresh and clear the browser's cache, to ensure that the latest plugin resources are loaded. # Webhooks Source: https://docs.enterspeed.com/enterspeed/integrations/webhooks Webhooks in Enterspeed are HTTP callbacks that will send all generated views by schemas with [destinations](/enterspeed/reference/js/full-schema/actions#destination) to a URL configured in the Webhook. This means that you can decide on the schema level which views you want to send to the webhook. You will only have to set the destination field on the entity schema you want to send to the webhook. All schema references are automatically resolved so you don't have to set it on all referenced schemas. It's possible to configure multiple Webhook destinations if you need to push different types of data to different URL endpoints. ## Configuration In order to setup the Webhook configuration you need the following: | Setting | Description | | ---------- | ----------------------------------------------------------------------------------------- | | Name | A name to identify the webhook | | URL | The url to call from the webhook | | Access Key | An API key the client can use to identify that the call is in fact coming from Enterspeed | ## Request The request to the client-configured URL will be made with the following configuration. | Setting | Value | Description | | ----------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Retry count | 3 | How many times the webhook will try to send the request | | Timeout | 10 | The request timeout in seconds | | HTTP method | POST | The requests is made as a POST | | Headers | `X-Api-Key`

`X-Enterspeed-Webhook-Name`

`X-Enterspeed-System` | The following headers will be send with the request.

X-Api-Key will hold a key so that the client can validate that the request is actually coming from Enterspeed

X-Enterspeed-Webhook-Name will hold the name of the webhook

X-Enterspeed-System will hold the version number of the webhook | ### Payload The request will send the following data. ```json theme={null} { "Id": "gid://Environment/2052b78d-6c34-4f11-bea5-296cf2d26968/Source/053b598b-c3d1-46fb-91e3-53115169cdb2/Entity/1234/View/product", // the Enterspeed view id "OriginId": "1234", // the origin id of the entity "Type": "product", // mapping schema alias "Action": "Deploy", // can have the value of Deploy or Remove "Url": "https://weu.delivery.enterspeed.com/v2?id=gid://Environment/40bb2d76-3b71-4121-b9b6-238cf4f325c4/Source/9e78f134-cf84-4ec5-9180-0b72b94949be/Entity/1099-en-us/View/home" // the absolute url for the delivery api to fetch the view } ``` # Domains & hostnames Source: https://docs.enterspeed.com/enterspeed/key-concepts/domains-hostnames Domains and hostnames are used to route your delivery API requests to ensure the correct data is returned. This is especially important if you have a multiple-site setup. ## Environment clients Environment clients are always linked to exactly one environment and provide an API key to be used in the delivery API request. ## Domains Domains are site-specific and not Environment client specific. This means that an Environment client can have multiple domains as an Environment client can point to an environment with multiple sites. This could be the case if you, e.g. have a single CMS installation containing multiple sites. Environment clients and domains Domains are site-specific and should only contain hostnames related to one specific site. ## Hostnames A domain can have multiple hostnames if the same site is available on multiple hostnames. Multiple hostnames pr domain Don't use the same domain for hostnames associated with different sites, as domains are site specific. Using the same domain for hostnames associated with different sites can result in data from the wrong site being returned from the delivery API if two source entities have the same relative path. ## Relative URL's If you only have one site, you could skip the hostnames and use the relative URL's instead. This will, behind the scene, create a hostname with the value of `root.tld`, making it possible to use relative URL's when requesting the delivery API. Relative URL's # Index schemas Source: https://docs.enterspeed.com/enterspeed/key-concepts/index-schemas This page gives you an introduction to the concept of indexes. Go to [schema references](/reference/js/index-schema/intro) to find the API documentation for index schemas. An index schema, as the name implies, creates an index, and more precise a search index, for you to query using the [Query API](/api-reference/query). An index schemas has two responsibilities: 1. Defining the index, including the name and the fields for the index 2. Mapping source entities that goes into the index as items ## Use cases An Enterspeed search index has multiple obvious use cases. ### Lists With an index you can create large lists of data that you can filter, sort and paginate using the Query API in you frontend application, integration or whereever you are using the Query API. Full and partial schemas are not build for larger lists of data from multiple source entities as they generate static views and require reprocessing everytime one of the items in the list is updated. On top of that they don't support dynamic lists and pagination. ### Fetching data dynamic With an index you can fetch a single or multiple items based on multiple dynamic parameters as opposed to full schema where you can create static handles to fecth a view. Fetching a view by handle is still more efficient and performant than querying an index. So only use search indexes when needed. ## Configure an index schema When using an index schema, you need to define the types for each fields, which impact the way the fields can be queried. In the below examples, we are creating a product index and querying it from the Query API. ## Examples Example of an index schema for products ```js title="Index schema example" theme={null} /** @type {Enterspeed.IndexSchema} */ export default { triggers: function(context) { context.triggers('pim', ['product']) }, index: { fields: { sku: { type: "keyword" }, title: { type: "text" }, price: { type: "float" }, category: { type: "keyword" } } }, properties: function (sourceEntity) { return { sku: sourceEntity.properties.sku, title: sourceEntity.properties.title, price: sourceEntity.properties.price.salesPrice, category: sourceEntity.properties.category.name } } } ``` Query API request example for the product index. ```json title="Qeury API request for the product index" theme={null} { "filters": { "and": [ { "field": "category", "operator": "qeuals", "value": "Caps" }, { "field": "price", "operator": "lessThan", "value": 200 } ] }, "sort": [ { "field": "price", "order": "asc" } ], "pagination": { "page": 0, "pageSize": 20 } } ``` See more about the [Query API](/api-reference/query). # Overview Source: https://docs.enterspeed.com/enterspeed/key-concepts/overview To understand the concept of Enterspeed, you would have to look at it as a 3 step process. 1. Ingest data 2. Transform data 3. Delivering data ## Ingesting data To get started ingesting data we need to do a few things first. ### Environment settings The first thing to do is ensure that we have an environment available. Enterspeed will automatically create two environments for you. You can view them under Environment settings in the [settings section](https://app.enterspeed.com/settings/environment-settings) You can create new environments, edit the name of your environments or delete them. Beware of deleting environments, since this is an irreversible action that will remove all data attached to the environment. ### Data source settings Then we also need to prepare Enterspeed, so your tenant can receive data. This is done by creating a data source. Data sources are where a connection is created to your CMS, PIM-system, or perhaps a development instance of your CMS. Go to [data sources](https://app.enterspeed.com/settings/data-sources) and create a data source group. (e.g. Demo CMS), and your data sources. You should now have an API key available for you. The data source API key is a unique key used for both authentication when pushing data and also as an id for the data source that you will be pushing source entities to. Read more about data sources and how to manage them [here](/docs/getting-started/data-sources.md). ### Preparing your system You will need a way to ingest data into Enterspeed from your source system. We have multiple options to get started with pushing source entities to Enterspeed. 1. Through our API. Find the [API documentation here](/api-reference/ingest). 2. Premade [connectors](https://docs.enterspeed.com/integrations) so you can get started immediately. 3. Our .NET SDK. [https://github.com/enterspeedhq/enterspeed-sdk-dotnet](https://github.com/enterspeedhq/enterspeed-sdk-dotnet) ```json title="Example of data sent to the ingest API" theme={null} { "type": "product", "url": "https://enterspeed.com/product-enterspeed-tshirt-old-xs/", "originParentId": "123", "redirects": ["https://enterspeed.com/product-enterspeed-tshirt-old/"], "properties": { "name": "Official Enterspeed T-shirt", "price": 199.99, "inStock": true, "features": [ { "name": "color", "value": "blue" }, { "name": "size", "value": "M" } ], "information": { "short": "Nice t-shirt", "long": "Nice t-shirt in cotton" } } } ``` ### Source Entities When the above steps have been applied successfully you are ready to push data to Enterspeed. Data in Enterspeed is called Source Entities. Source Entities conform to a specific format. The important thing to know about source entities is that these are not representing the final output of your Enterspeed routes, but should be seen as the data that is available for you to work with and [transform](#transforming-data) to your needs through your schemas. ```json title="Source entity example" theme={null} { "id": "1044-en-us", "type": "frontPage", "url": "https://www.example.com/", "properties": { "title": "Welcome", "description": "description value" } } ``` ## Transforming data ### Schemas The data now exists as source entities in Enterspeed and can be formed and modeled easily with data mapping in Enterspeed schemas. Read more about [schemas](/enterspeed/key-concepts/schemas). ```js title="Schema" theme={null} /** @type {Enterspeed.FullSchema} */ export default { triggers: function(context) { context.triggers('umbraco', ['frontPage']) }, properties: function (sourceEntity, context) { return { headline: sourceEntity.properties.title, description: sourceEntity.properties.description, } } } ``` ### Routing Routing is set up in Schemas and is a part of setting up schemas and API's. We currently offer two ways of setting up routing: * [Url routing](/enterspeed/key-concepts/schemas) * [Handles](/enterspeed/key-concepts/schemas) ### Partial Schemas Partial schemas are a bit different from the typical schema. A partial schema is a reusable schema that is used across multiple schemas. A typical use case is when you want a specific data structure and type of data across many schemas. You can read more about partials [here](/enterspeed/key-concepts/partial-schemas) and how it is used. ### Actions Actions are used when a new view has been generated from a schema. It defines which specific actions to take following the newly generated view. Currently, Enterspeed supports triggering the `process` of another schema. Imagine the following. You have a *product* and *category* source entity type. When you ingest a *product*, the list of products should be updated in the generated category view and include the changes. This is a typical use-case scenario for actions. Read more about [Actions](/enterspeed/reference/js/full-schema/actions) ### References Referencing another schema. The [Reference](/reference/json/property-types#reference) property type is a bit different than the [Partial](/reference/json/property-types#partial) property type. The [Reference](/reference/json/property-types#reference) property allows referencing other views created from either this Source Entity or from another source entity and the Partial property can only use data from this Source Entity. A benefit of a reference field is that the referenced view is resolved when requested by the Delivery API. There is no need the update the requested view if a referenced view has been updated. Read more about the Reference property [here](/enterspeed/key-concepts/referencing-schemas) with a more in-depth explanation and examples. ### Views A view should be considered as the output. Data is mapped in a Schema from source entities. When a source entity is created, updated or deleted, all schemas that are set up for the type of this source entity will create, update or delete the view accordingly. In short, the view is the response when calling the Delivery API. ## Delivering data Your data has now been ingested as source entities and transformed using schemas. An example of calling the delivery API can be found [here](/enterspeed/tutorials/umbraco-nextjs/fetching-data-in-nextjs). ```js title="JavaScript example of calling the delivery API" theme={null} const call = async (query, preview) => { const url = `https://delivery.enterspeed.com/v1?${query}`; const response = await fetch(new Request(url), { headers: { "Content-Type": "application/json", "X-Api-Key": preview ? process.env.ENTERSPEED_PREVIEW_ENVIRONMENT_API_KEY : process.env.ENTERSPEED_PRODUCTION_ENVIRONMENT_API_KEY, }, }); return response.json(); }; export const getByHandle = async (handle, preview) => { const response = await call(`handle=${handle}`, preview); return response.views[handle]; }; export const getByUrl = async (url, preview) => { const response = await call(`url=${url}`, preview); return response.route; }; ``` ### Environment clients You need to set up an environment client. Navigate to Environment clients under Settings -> Environment settings and click the Create button. Give your environment client a name (e.g. My My Client Application) and select one of the environments that you have created. Afterward, click on the Create button. A Delivery API key has now been created. Copy and save it for use in your client application. Next to your environment client, select edit domains. Select the Domain and press save changes. You should now be ready to call the Enterspeed Delivery API. Go to the [docs](/api-reference/delivery) for requirements regarding this. We also have an example [here](/enterspeed/tutorials/umbraco-nextjs/4-fetching-data-in-nextjs) ### Final notes We would suggest going to the [tutorials section](/enterspeed/tutorials/overview) for examples and some in-depth samples of getting started. # Partial schemas Source: https://docs.enterspeed.com/enterspeed/key-concepts/partial-schemas This page gives you an introduction to the concept of partial schemas. Go to [schema references](/reference/js/partial-schema/intro) to find the API documentation for partial schemas. Partial schemas are a bit different from the typical schema. A partial schema is a reusable schema that is used across multiple schemas. A typical use case is when you want a specific data structure and type of data across many schemas. The difference between using a partial schema versus referencing another schema is that a referenced schema is stored as a separate view. Using a partial schema with your schema gives the output of a single view when processed. Partial schemas are typically used when you want a reusable schema for mapping data that is part of the same entity. Eg. metadata (title, description, ...) is the same across different entity types but the data lives on the entity itself. Don't use partial schemas for mapping data from another entity that's being referenced, as your views can be outdated if the referenced entity is updated. In this case, you should use [reference schemas](/enterspeed/key-concepts/referencing-schemas). ## Configure a partial schema When using a partial schema, you are giving it a data object. This data object is typically the value of a specific part of the data source you are working on. In the below example, we are using a partial schema called `seo`, and passing it the value of `p.seo` (a complex JSON object with SEO properties and data) ## Examples Example of a schema that is referencing a partial schema ```js title="Schema" theme={null} /** @type {Enterspeed.FullSchema} */ export default { properties: function (sourceEntity, context) { return { headline: sourceEntity.properties.title, seo: context.partial('seo', sourceEntity.properties.seo) } } } ``` Example of the partial schema being used by the schema. ```js title="Schema" theme={null} /** @type {Enterspeed.PartialSchema} */ export default { properties: function (input, context) { return { metaTitle: input.seoTitle, metaDescription: input.seoDescription } } } ``` The data source used in this example. ```json title="Data source" theme={null} { "sourceId": "gid://Source/bfb8fd65-35d7-48d1-94bc-df0da13469d2", "id": "gid://Source/bfb8fd65-35d7-48d1-94bc-df0da13469d2/Entity/1103", "type": "product", "originId": "1103", "originParentId": "1098", "url": "http://localhost:57152/products/bowling-ball/", "redirects": [], "properties": { "title": "Bowling Ball", "seo": { "seoTitle": "Bowling Ball", "seoDescription": "A bowling ball is a hard spherical ball used to knock down bowling pins in the sport of bowling. Balls used in ten-pin bowling and American nine-pin bowling traditionally have holes for two fingers and the thumb." } } } ``` More examples can be found here [here](/enterspeed/tutorials/umbraco-nextjs/3-designing-your-apis#example-schemas--partial-schemas) # Redirects Source: https://docs.enterspeed.com/enterspeed/key-concepts/redirects When changing the URL of a page in a CMS or moving a product to another category, you often want to have that old URL redirected to the new URL. In Enterspeed you can ingest incoming redirects on your source entities. You do that by using the `X-Enterspeed-Redirects` header in your ingest request or as part of the object in the root property called `redirects` as described in the [API documentation](/api-reference/ingest/save-entity). Once ingested you can see the redirects on the source entity in the Enterspeed app. ```json title='Source entity with redirect' theme={null} { "sourceId": "gid://Source/bce0dd5f-f371-4d5d-b95f-3ed427ec312d", "id": "gid://Source/bce0dd5f-f371-4d5d-b95f-3ed427ec312d/Entity/1118-en-us", "type": "contentPage", "originId": "1118-en-us", "originParentId": "1056-en-us", "url": "http://mydomain.com/new-url", "redirects": ["http://mydomain.com/old-url"], "properties": {} } ``` By default, if a schema has a single URL mapped as route, these redirects will automatically be applied to the view as implicit redirects. If you are using JavaScript schemas you also have the option clear the implicit redirects and create your own explicit redirects. Implicit redirects are cleared if you map more than one URL, as Enterspeed don't know which one of the URLs the implicit redirects should point to, or if you set explicit redirects in your schema. Using explicit redirect gives you the option to dynamically build the redirects in your schema and especially if you have multiple URLs for a view you want to specify which of the URLs the different redirects should point to. ```js title="Example of explicit redirects" theme={null} routes: function(sourceEntity, context) { context .url('http://mydomain.com/new-url') .redirects(['http://mydomain.com/old-url']); context .url('http://mydomain.com/another-url'); } ``` [See redirect API documentation](/enterspeed/reference/js/full-schema/routes#url) No matter if you use implicit or explicit redirects the delivery response will be the same if you request a view on a redirect URL. From the delivery response you will get a redirect response instead of a 404 error. ```json title='A request to the old URL returns a redirect response' theme={null} { "meta": { "status": 301, "redirect": "http://mydomain.com/new-url", "missingViewReferences": [] }, "views": null } ``` # Referencing schemas Source: https://docs.enterspeed.com/enterspeed/key-concepts/referencing-schemas The reference property allows referencing other views created from either its Source Entity or from another Source Entity. Read more about reference property for JSON schemas [here](/reference/json/property-types#reference) or for JS schemas [here](/reference/js/full-schema/properties#reference). Reference schemas are typically used when you are mapping data from another entity. E.g. a page has a reference to another page entity or media entity. The reason why you would use reference schemas when mapping data from two different entities is to make sure your views are fully updated if one of the entities is updated. Let's say you have a page schema that triggers on the page entity type. This means that every time the page is ingested the schema is processed and the view is updated. Inside the schema, you are mapping the url and name of an image entity that the page is referencing, either directly in the schema or by using a partial schema. Now, what happens if the image entity is ingested again with a new url or name? Nothing, because our page schema only triggers when a page is ingested. This means that your page view with the image data is outdated, which is not good. That's why you use reference schemas when mapping data from other entities. References are resolved on delivery request time. This means that if you have a page schema being processed every time a page is ingested with a schema reference to the image and an image schema being processed every time an image is ingested, then when you make a delivery request to the page, the views are merged together and you view data is always up to date. Keep in mind, that you can only avoid reprocessing the referencing schema (the page schema) if you build the reference using `byOriginId` or `byOriginIds`. If you build the refernce using the `filter` function, you will still need to reprocess the referencing schema. You can read more about why that is and the reprocessing in general [here](/enterspeed/key-concepts/reprocessing) ## Use cases Here is a list of examples of how to use the reference property and how it's used to reference schemas. [Site settings](/enterspeed/reference/schema-example-library/site-settings) [Breadcrumb navigation](/enterspeed/reference/schema-example-library/breadcrumb-navigation) [Latest news](/enterspeed/reference/schema-example-library/latest-news) # Reprocessing Source: https://docs.enterspeed.com/enterspeed/key-concepts/reprocessing This page gives you an introduction to the concept of reprocessing. Go to [schema references](/reference/js/full-schema/actions#reprocess) to find the API documentation for reprocessing. When Enterspeed preprocesses views - which happens either when source entities are ingested or when a schema is deployed -, we also need to reprocess views whenever dependencies are updated or deleted. Let's say you have a `product` source entity ingested from a PIM system and you want to create a schema to map the product and to enrich the product with some related `contentBlock`s from a CMS system. The schema could look somewhat like this. ```js title="Schema with dependencies" theme={null} /** @type {Enterspeed.FullSchema} */ export default { triggers: function(context) { context.triggers('pim', ['product']); }, properties: async function (sourceEntity, context) { const relatedContent = async context .lookup(`type eq 'contentBlock' and properties.sku eq '${sourceEntity.properties.sku}'`) .SourceGroup('cms') .toPromise(); return { sku: sourceEntity.properties.sku, name: sourceEntity.properties.name, relatedContent: relatedContent.map((c) => { return { title: c.properties.title, content: c.properties.content } }) }; } } ``` For this schema, the `product` is the trigger and the `contentBlock`s, which we get from the `lookup`, are the dependencies. The trigger on the `product` source entity makes sure that the schema is triggered every time the `product` is updated, and our view will then be updated too. But what if the related `contentBlock`s are updated, added, or removed? Well, schemas don't automatically trigger when dependencies change, so in this case the `product` schema will not be triggered, and our view will not be updated. We need to handle that in the schemas ourselves. Before looking into how we can update views when dependencies are changed, let's first take a deeper look at the dependencies and the different types of dependencies we have in Enterspeed. ## Dependencies In Enterspeed we have two types of data available in a schema. The first one is source entity that triggers the schema - and this one is passed as a parameter to the different functions in the schema like the [routes function](/reference/js/full-schema/routes), and the [properties function](/reference/js/full-schema/properties). The second one is dependencies, and dependencies can then be broken further down in two types, [lookups](/reference/js/full-schema/properties#lookup) and [references](/reference/js/full-schema/properties#reference). ### Lookups When you do a lookup in a schema, the result of the lookup is a list of source entities. As you get the raw source entities with all the data right in the schema, it gives you full flexibility of how you want to map out your data, but that flexibility also comes with a downside. Since you get the raw source entities right in your schema, it means that the data you map out is embedded directly into the view you create and because of that we always need to reprocess the view if one of the dependencies (one of the source entities from the lookup) is updated. ```js title="lookups return a list of source entities" theme={null} properties: async function (sourceEntity, context) { const relatedContent = async context .lookup(`type eq 'contentBlock' and properties.sku eq '${sourceEntity.properties.sku}'`) .SourceGroup('cms') .toPromise(); return { sku: sourceEntity.properties.sku, name: sourceEntity.properties.name, relatedContent: relatedContent.map((c) => { return { title: c.properties.title, content: c.properties.content } }) }; } ``` When working with dependencies from a `lookup` call, you always need to reprocess the referencing schema when the dependencies are updated. ### References As the name implies, references don't bring the dependent data directly into the schema, but just a reference to views. With references you can create reusable schemas and reference the views created by that schema across multiple other schemas. As the referenced views are not directly embedded into the referencing view it also means that it's not always neccessary to reprocess the referencing view. To understand exactly how references works, let's take a look at a view containing references. ```json title="View with two references" theme={null} { "sku": "p-1234", "name": "Nike running shoe", "relatedContent": [ { "view": null, "id": "gid://Environment/8b1c6be0-535d-4d6c-b690-a7e3590a0643/Source/e3211d99-e496-4e11-af3d-328eb543619g/Entity/1106-en-us/View/contentBlock", "$type": "ViewReference" }, { "view": null, "id": "gid://Environment/8b1c6be0-535d-4d6c-b690-a7e3590a0643/Source/e3211d99-e496-4e11-af3d-328eb543619g/Entity/1107-en-us/View/contentBlock", "$type": "ViewReference" } ] } ``` Looking at the view we can see that references are not resolved yet; it's still just references to specific view ids. First when you request the view from the Delivery API, the Delivery API will make sure to resolve the references and return the content of the referenced views instead of the internal references with the view ids as you see above. #### References by filter needs reprocess Now this is all good. It means that if you update one of the referenced views, you don't need to reprocess the referencing view. But what happens if you ingest a new `contentBlock` source entity or deletes a source entity that matches the `sku` value from the `product`? Then the list of references is not updated, because even though the data of the references isn't stored in the view, it *does* store the result of which views to reference from the `filter` function. Here's an example of the same schema as above, but with references instead of lookup. ```js title="schema with references using filter" theme={null} properties: function (sourceEntity, context) { return { sku: sourceEntity.properties.sku, name: sourceEntity.properties.name, relatedContent: context.reference('contentBlock') .filter(`type eq 'contentBlock' and properties.sku eq '${sourceEntity.properties.sku}'`) .SourceGroup('cms') }; } ``` Whenever you create references based on the `filter` function, you still need to reprocess the referencing view if you want the list of references to be updated when new dependencies are ingested or existing once are deleted, just like with the `lookup` function. In other words, when the referenced source entities holds to reference key, in this case the `contentBlock`s has the reference to the product via the `sku` property, you need to search using the `filter` function and you need to reprocess the referencing view. When working with referenced dependencies from a `filter` call, you always need to reprocess the referencing schema when the dependencies are updated. #### References by origin id(s) doesn't needs reprocess Let's look at another example where the relationship is turned around and referencing source entity holds the information about the relationship. ```js title="schema with references using origin ids" theme={null} properties: function (sourceEntity, context) { return { sku: sourceEntity.properties.sku, name: sourceEntity.properties.name, relatedContent: context.reference('contentBlock') .byOriginIds(sourceEntity.properties.relatedContentIds) .SourceGroup('cms') }; } ``` In this case the triggering source entity (the `product`) now has the knowledge about the relationship and knows the ids of the `contentBlocks`. This means that we can create the references by using the `byOriginIds` function and that whenever the `product` is updated with new relationships it will simply trigger the referencing schema so we don't need any reprocessing. When working with referenced dependencies from a `byOriginId` or `byOriginIds` call, you never need to reprocess the referencing schema when the dependencies are updated. ## Reprocess actions We've talked about dependencies, the different types of dependencies, when you need to reprocess, and when you *don't* need to reprocess. So, we just need the last part - How do we reprocess other schemas when a dependency changes? To do that we use the [action](/reference/js/full-schema/actions) and the [reprocess](/reference/js/full-schema/actions#reprocess) functions. Here's an example of a `contentBlock` schema that will reprocess the `product` schema for the specific `sku` value whenever the `contentBlock` is updated. ```js title="Schema with reprocess action" theme={null} /** @type {Enterspeed.FullSchema} */ export default { triggers: function(context) { context.triggers('cms', ['contentBlock']); }, action: function(sourceEntity, context) { context.reprocess('product') .byOriginId(sourceEntity.properties.sku) .sourceGroup('pim'); } properties: function (sourceEntity, context) { return { title: sourceEntity.properties.title, content: sourceEntity.properties.content }; } } ``` Reprocess actions can cause execessive processing, especially if you reprocess more than needed. This could result in a larger queue of jobs, and it will take longer for all your views to be updated.\ Because of that, it's important to make your reprocess actions as precise as possible, by using `originId` or a precise `filter` so you only target the schemas and source entities you actually need to reprocess. # Routing Source: https://docs.enterspeed.com/enterspeed/key-concepts/routing This page gives you an introduction to the concept of routing. Go to [schema references](/reference/js/full-schema/routes) to find the API documentation for routing. You can do routing by either Url or Handle. ## URL If you want your schema to be routable by an URL, you can specify the `url` as an expression. If we take a look at this example we can see that we have a Url property available in the Data Source ```json title="Source entity example" theme={null} { "id": "1044-en-us", "type": "frontPage", "url": "/frontPage", "properties": { "title": "Welcome", "description": "description value" } } ``` In the below example of the schema for this Data Source, we can see that the route is mapped to the url property on the Data Source. This means that you can get the data from this source by making a request to `https://delivery.enterspeed.com/v1?url=/frontpage` ```js theme={null} /** @type {Enterspeed.FullSchema} */ export default { triggers: function(context) { context.triggers('umbraco', ['frontPage']) }, routes: function(sourceEntity, context) { context.url(sourceEntity.url) }, properties: function (sourceEntity, context) { return sourceEntity.properties } } ``` **What does "No environment client configured to support domain name for source entity url /relative-url/" mean?** If you are using relative URLs (*e.g. /about-us/*) and are trying to test the schema by making a CURL request, you might have seen this error message. The reason for this is that the URL doesn't have a domain that matches an environment client. Environment clients need to be able to match the URL in the source entity with the hostname provided for the environment client. The best way to solve this is to use absolute URLs in your schema (*e.g. [https://my-domain.com/about-us/](https://my-domain.com/about-us/)*). If you however want to use relative URLs, it can be done by adding a domain to your environment client with the hostname `root.tld`. ## Handle Handle differentiates a bit from URL routing. A handle can be whatever you would like. In this example, a navigation structure is returned. The schema returns an array of navigation items and utilizes the lookup and [reference](/enterspeed/key-concepts/referencing-schemas) fields. This handle would be called like this: `https://delivery.enterspeed.com/v1?handle=mainNavigation` ```js theme={null} /** @type {Enterspeed.FullSchema} */ export default { triggers: function(context) { context.triggers('umbraco', ['navigationGroup']) }, routes: function(sourceEntity, context) { context.handle('mainNavigation') }, properties: function (sourceEntity, context) { return { children: context.reference('navigationItem') .children() .orderBy({ propertyName: 'properties.metaData.sortOrder', direction: 'desc' }) } } } ``` # Schemas Source: https://docs.enterspeed.com/enterspeed/key-concepts/schemas The schemas define the API endpoint and data structure for your data. This is where decoupling is happening since you are specifying the new structure of your [source entities](/enterspeed/key-concepts/source-entities) through schema mappings, and generating [views](/enterspeed/key-concepts/views) based on these mappings. ### How? A schema acts as a middleman, grabbing the data from the source entities and generating the view with a data structure that is based on the schema definitions. [Views](/enterspeed/key-concepts/views) are the result of the above, and what we receive as a response when calling the Enterspeed API. Schema is key in setting up and defining API's, property mapping, [routes](/enterspeed/key-concepts/routing), [actions](/enterspeed/reference/js/full-schema/actions) and more. ## Important topics [Schema docs](/enterspeed/reference/js/full-schema/intro) [Referencing schemas](/enterspeed/key-concepts/referencing-schemas) [Actions](/enterspeed/reference/js/full-schema/actions) [Routing](/enterspeed/key-concepts/routing) [Partial schemas](/enterspeed/key-concepts/partial-schemas) [Reprocessing](/enterspeed/key-concepts/reprocessing) ## Tutorials [Schema snippets](/enterspeed/reference/snippets) [Designing a schema](/enterspeed/transform/designing-a-schema) [Multilevel navigation example](/enterspeed/tutorials/multilevel-navigation/1-getting-started) [Umbraco & Next.js example](/enterspeed/tutorials/umbraco-nextjs/3-designing-your-apis) # Source Entities Source: https://docs.enterspeed.com/enterspeed/key-concepts/source-entities When pushing to Enterspeed from your source system, data exists in Enterspeed as Source Entities. Source Entities are not a representation of the final output. They should be seen as the data that is available for you to work with. The important thing to remember is that we are not going to see the output of the Source Entities, since you are specifying the output of your Source Entities through schema mappings. These schema mappings generate views of data, based on the mappings that you have done in the schema. Read more about [schemas](/enterspeed/key-concepts/schemas) and how to get started working with mapping data from your Source Entities. # Source Groups Source: https://docs.enterspeed.com/enterspeed/key-concepts/source-groups Source groups contains the sources you ingest your source entities into. Typically you have a source per environment inside a source group. ## Modes Source groups works in two different modes. Modes are selected on source group creation and can't be changed afterwards. Source group modes ### Schema Transformation (default) Schema Transformation is the default and recommended mode for most scenarios. It enables you to transform and combine source entities into views or index items using defined schemas. Once processed, views can be accessed via the [Delivery API](/api-reference/delivery), while index items are available for querying through [Query API](/api-reference/query). The schema transformation is a powerfull feature, allowing you to tailor and structure data precisely to meet consumer needs. However, because the transformation process takes time, this mode is best suited for non-real-time data. ### Auto Indexing Auto Indexing mode are currently in preview. On Auto indexing mode there is no transformation process. Instead, ingested source entities goes directly into an index, which can be queried using the [Query API](/api-reference/query/query-items-from-auto-indexing-source-groups). Since no transformation is involved, the Auto Indexing mode is suited for scenarios such as handling large volumes of small, custom price objects that don't require transformation, or frequently changing data like stock numbers. For each source entity type you want to query, you add a type mapping. You name it after the source entity type it indexes — for example `price` — and define the fields and their data types in JSON, as in the example below. ```json theme={null} { "fields": { // the fields that are added to the index "sku": { "type": "keyword", "metadata": { "description": "The product key" } }, "price": { "type": "float", "metadata": { "description": "The sales price of the product" } }, "validFrom": { "type": "date", "metadata": { "description": "A from date where the price is active" } }, "validTo": { "type": "date", "metadata": { "description": "A to date where the price is active" } } }, "metadata": { "description": "The price of a product for a specific time interval" } } ``` The type name isn't part of this JSON — you set it on the mapping itself. Add one mapping per source entity type you want to query, and add more whenever you need them. Only top-level properties holding a single value, or a list of single values, can be indexed. Nested objects, and lists of objects, are never indexed and can't be added to a mapping. Properties that aren't defined in the type's fields aren't added to the index, so you can't query them. Your source entities are stored in full, so adding an indexable property to the mapping later populates it for the entities you have already ingested — you don't need to re-ingest them. You can add, change, and remove type mappings after the source group is created. See [Managing type mappings](#managing-type-mappings). ## Types Setting the right types for the properties in your index is important. The types defines the intend of the fields and prevents data of other types from going into the index. The types also helps with effeciently index, search, and analyze of the data added to the index. ### Text | Type | Description | | ------- | -------------------------------------------------------------------------------------------------- | | keyword | The `Keyword` type is for string values used in filtering and sorting | | text | The `text` type will analyze string values, improving results when querying using full text search | ### Numeric | Type | Description | | ------- | ------------------------------- | | integer | A 32 bit signed integer | | long | A 64 bit signed integer | | float | A 32 bit signed floating number | | double | A 64 bit signed floating number | ### Boolean | Type | Description | | ------- | ------------------------ | | boolean | A `true` / `false` value | ### Date | Type | Description | | ---- | ------------------------------------------------------------------------------------------------------------------------------------------ | | date | Support values like a Javascript Date object, a string like `2024-11-21` or `2024-11-21T23:00:00Z` or number of milliseconds since eclipse | ### List | Type | Description | | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | keyword\[] | List of the `Keyword` type for string values used in filtering and sorting | | text\[] | List of the `text` type for analyzing string values, improving results when querying using full text search | | integer\[] | List of 32 bit signed integers | | long\[] | List of 64 bit signed integers | | float\[] | List of 32 bit signed floating numbers | | double\[] | List of 64 bit signed floating numbers | | boolean\[] | List of `true` / `false` values | | date\[] | List of dates with supported values like a Javascript Date object, strings like `2024-11-21T23:00:00Z` or numbers of milliseconds since eclipse | ## Managing type mappings You can add, change, and remove type mappings on an Auto Indexing source group at any time. Each type gets its own index, so a change to one type never affects the others. Type mappings on a source group ### Adding a type Add a mapping for a type that isn't indexed yet, and Enterspeed creates the index and fills it from the source entities you have already ingested. You don't need to re-ingest anything. Indexing runs in the background, so it can take a moment before every entity is queryable. ### Changing a type What happens when you save depends on what you changed. | Change | What happens | | ------------------------------------ | -------------------------------------------------------------------- | | Descriptions and other metadata only | Written to the index straight away. Nothing is re-indexed. | | Adding or removing a field | The index is rebuilt in the background. | | Changing a field's type | The index is rebuilt in the background, if the change is compatible. | Editing a type mapping Rebuilds are zero-downtime. Enterspeed builds a new version of the index alongside the live one and switches over once it has caught up, so your queries keep returning complete results throughout. Because a rebuild re-indexes from the source entities Enterspeed already stores, adding a field also populates it for your existing entities — including properties that weren't in the index before. ### Compatible field type changes A rebuild re-indexes your existing values under the new type, so a type change is only allowed when every value can survive it. Adding and removing fields is always allowed. | From | To | | ------------------------------------------------------- | ---------------------------------------------------- | | `integer` | `long`, `double` | | `long` | `double` | | `float` | `double` | | `keyword` | `text` | | `integer`, `long`, `float`, `double`, `boolean`, `date` | `text`, `keyword` | | any type | a list of that type, or of any type it can change to | Converting a number or date to `text` or `keyword` keeps the value but loses range and sort behaviour, so filter on it as a string afterwards. These changes are rejected, because existing values would be lost: * Narrowing a number, such as `double` to `integer` * `text` to `keyword`, since a `keyword` value is capped at 32,766 bytes and longer text would be rejected outright * A list to a single value, such as `keyword[]` to `keyword` * Switching between unrelated types, such as `date` to `integer` ### Replacing a type Sometimes you need a change that the rules above reject, but your data supports it anyway. Say you mapped a price field as `text` and it should have been `double` — that change is rejected because a `text` field *can* hold values that aren't numbers, not because yours does. For cases like this you can replace the type instead. Enterspeed tells you which fields are the problem and offers this per type, so you can replace one type and leave the rest of your changes to go through normally. An incompatible type change Replacing rebuilds the index from your stored source entities, skipping the compatibility check. Every entity whose values fit the new type is kept — so if all your `text` values really are numbers, they all survive the change to `double`. Any entity holding a value that doesn't fit is left out of the index entirely, not just that one field, so you can no longer query it. The entity itself is kept and is queryable again once re-ingested with a matching value — but this is exactly what the compatibility check normally protects you from, so make sure you know your data before replacing a type. ### Removing a type Removing a mapping deletes the index for that type, so you can no longer query it. Your source entities are kept. Entities of a removed type — or of any type on a source with no mappings configured yet — are marked **Not configured** in the source entities list, so you can see at a glance which ones aren't indexed. Add the mapping back later and Enterspeed fills the index again from the entities it already has. # Views Source: https://docs.enterspeed.com/enterspeed/key-concepts/views A view is the output of the data that is mapped in a [Schema](/enterspeed/key-concepts/schemas), where the source of the data is a [Source Entity](/enterspeed/key-concepts/source-entities). Views are reprocessed when a change in the Source Entity has happened. It is possible to test the output of your schema mappings in your tenant from the administration. Navigate [here](https://app.enterspeed.com/views) for an overview of your generated views. # Documentation MCP Source: https://docs.enterspeed.com/enterspeed/mcp-server/documentation-mcp/overview The Enterspeed Documentation MCP Server lets you search and retrieve content from the [docs.enterspeed.com](https://docs.enterspeed.com) site and even report back issues or suggestions to the content. It turns the Enterspeed documentation into a set of [Model Context Protocol](https://modelcontextprotocol.io/) tools that AI clients (Claude, Cursor, custom agents, Azure AI Foundry, etc.) can discover and call. You do not need to host anything yourself. ## Hostname | Environment | URL | Transport | | -------------- | --------------------------------- | --------- | | **Production** | `https://docs.enterspeed.com/mcp` | HTTP | The documentation is public, so this server needs no authentication — there is no key to issue and nothing to scope. That is also why it is the one Enterspeed MCP server that works with browser-based clients. ## Installation Simply click the arrow next to the Copy page button on the top right of this page and select **Connect to VS Code** and click the install button in VS Code. Install MCP server on VS Code Or you can install it yourself in the VS Code configuration. VS Code's GitHub Copilot reads MCP servers from an `mcp.json` file. For a single workspace, put the config at `.vscode/mcp.json`; for every workspace, use the **MCP: Open User Configuration** command from the command palette. ```json theme={null} { "servers": { "enterspeedDocs": { "url": "https://docs.enterspeed.com/mcp", "type": "http" } } } ``` Reload the window (**Developer: Reload Window** from the command palette) and open the Copilot chat pane. The Enterspeed documentation tools appear in the tool picker once Copilot connects. ```bash theme={null} claude mcp add enterspeedDocs \ --scope user \ --transport http \ https://docs.enterspeed.com/mcp ``` The user scope (`--scope user`) installs the MCP for the current user across all projects. Use `--scope project` to install it for the current project only but for all users (written to a checked-in `.mcp.json`). The CLI stores the server configuration under `~/.claude.json`. Edit that file if you need to tweak anything afterwards. Open your Claude Desktop config file: * **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` * **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` Add the `mcpServers` entry: ```json theme={null} { "mcpServers": { "enterspeedDocs": { "command": "npx", "args": [ "-y", "mcp-remote", "https://docs.enterspeed.com/mcp" ] } } } ``` Fully quit and relaunch Claude Desktop. The Enterspeed doc tools now appear in the tool picker. ### Other MCP clients The three clients above are the ones we test against regularly, but the MCP server is client-agnostic. Any MCP-capable client follows the same pattern — point it at `https://docs.enterspeed.com/mcp` with `http` transport. Or simply just ask your favorite AI agent how to connect to the Enterspeed Documentation MCP Server. It should be able to generate the configuration for you. # Overview Source: https://docs.enterspeed.com/enterspeed/mcp-server/management-mcp/overview Connect an AI client to the Enterspeed Management API — read your tenant configuration, author schemas, and create environments, sources, domains, and clients. The Enterspeed Management MCP Server turns the Enterspeed Management API into a set of [Model Context Protocol](https://modelcontextprotocol.io/) tools that AI clients (Claude, Cursor, VS Code, custom agents) can discover and call. It is the configuration counterpart to the [Query MCP](/enterspeed/mcp-server/query-mcp/overview) server: where Query MCP reads your *data*, Management MCP reads and changes your *setup*. You do not need to host anything yourself — Enterspeed runs the server for you. Typical uses: * Ask an agent to explain how a tenant is wired together — sources, environments, domains, clients, deployments. * Have an agent write a mapping schema, dry-run it against a real source entity, and save a new version. * Investigate a view or route that is not producing what you expect. * Query your tenant logs and metrics in natural language. ## Hostname | Environment | URL | Transport | | -------------- | ---------------------------------------- | ------------------- | | **Production** | `https://mcp.management.enterspeed.com/` | MCP Streamable HTTP | A health check is available without authentication: ```bash theme={null} curl -s https://mcp.management.enterspeed.com/health # "ok" ``` The server speaks **MCP Streamable HTTP** on the root URL. Responses are delivered as a server-sent event stream on that same URL, so clients that label their transport *SSE* generally work when pointed at the root. There is no separate `/sse` endpoint — always configure the root URL. ## How authentication works The Management MCP server is a **passthrough proxy**. It holds no credentials of its own. Your MCP client sends a Management API key on every request, the server forwards it unchanged, and the Management API makes every authorisation decision. ``` AI client ──x-api-key──► mcp.management.enterspeed.com ──x-api-key──► the Management API │ validates the key and its permissions ``` Clients authenticate with a single HTTP header on every MCP request: ``` x-api-key: management- ``` The key is a **Management API key** issued from the Enterspeed Management App. It is *not* an environment client key — that is what [Query MCP](/enterspeed/mcp-server/query-mcp/overview) uses. There is no query-string fallback on this server. The key must be sent as an `x-api-key` header, because a key in a URL ends up in access logs along the whole request path. If your client cannot set headers, see [Known limitations](#known-limitations). ### Use a read-only key for read-only work Management clients have an access level — **read-write** or **read-only**. Because the server passes your key straight through, **the key decides what an agent can do** — the server never widens it. We recommend the smallest key that does the job: | What you want the agent to do | Recommended access level | | ----------------------------------------------------------------- | ------------------------ | | Explain, inspect, investigate, dry-run, validate | **Read-only** | | Author schemas, create environments, sources, domains, or clients | **Read-write** | A read-only key reaches every read tool — including both dry-run tools and schema validation, which are read-only despite sounding like they change something. If an agent tries a write tool with a read-only key, the Management API rejects the call and the agent gets a clear permission error. That is the intended outcome, not a fault. **Management clients created in the Management App are read-write.** The app's create form takes a name only, and the API defaults to read-write when no access level is supplied. To issue a read-only key today, create the management client through the Management API and set the access level explicitly — see [Creating a Management API key](#creating-a-management-api-key) below. ## What the server can and cannot do The server exposes **35 tools: 22 reads and 13 additive writes**. * **Reads** cover tenant overview, sources and source entities, mapping schemas, deployments, views and routes, indexes and index documents, domains and hostnames, environment clients, logs, and metrics. * **Writes** are **additive only** — create and update. They cover schema authoring (create a schema, save a version, deploy), environments, source groups and sources, domains, and environment clients. **There are no delete tools, by design.** This is not a policy the server can be talked out of — it has no way to express a destructive call. Every request the server can make is a `GET`, or a `POST`/`PUT` that appears on a fixed allow-list checked when the server starts. No tool can delete a schema, environment, source, domain, client, or view. The practical consequence: an agent connected to this server can create clutter you may need to tidy up manually, but it cannot destroy your configuration or your content. See [Tools](/enterspeed/mcp-server/management-mcp/tools) for the full inventory with each tool marked read or write. ## Guardrails Beyond the key's own permissions, the hosted server applies three limits you should know about. On the hosted server, `deploy_mapping_schema` only deploys to environments **named** `dev`, `development`, `test`, or `staging`. A deploy targeting any other environment — including anything named `prod`, `production`, or `live` — is refused with a `deploy_not_permitted` error naming the environments that are allowed. This is a restriction of the hosted server, not of your key. Renaming an environment to get around it does not work either: `update_environment` refuses a rename that would move an environment onto the allowed list. Production deploys stay a deliberate human action in the Management App. The server strips the `accessKey` field out of every response before your client sees it, at every level of nesting. This covers environment client keys and source ingest keys. It applies even to `create_environment_client`, which mints a key: the tool creates the client, and the key value is not in the response. The agent is told a key was created and that it must be collected from the app — so it reports the right next step instead of inventing a value. **Fetch the key in the Management App** when you need it. The field is removed rather than masked, so no tool can observe a key at any point. Requests are counted per API key over a rolling one-minute window. Exceeding the limit returns: ``` HTTP 429 Too Many Requests Retry-After: Too many requests. Retry after seconds. ``` Well-behaved MCP clients back off and retry. The limit applies to the whole MCP endpoint, and `/health` is exempt. For comparison, [Query MCP](/enterspeed/mcp-server/query-mcp/overview) allows 120 requests per minute per key. ## Security model If you are reviewing this server before pointing an agent at your tenant, this is the short version: * **No server-held credentials.** The server has no Enterspeed key of its own and no way to configure one. It can only act with a key you send it. * **Passthrough authorisation.** Every permission decision is made by the Management API against your key, not by the MCP server. * **No destructive verbs.** The server cannot issue a `DELETE` or a `PATCH` at all. * **Secrets are not readable.** Access keys are removed from responses at the HTTP boundary, so no tool can observe one. * **Production deploys are out of reach** on the hosted server. * **Least privilege is yours to set.** Use a read-only key unless the agent genuinely needs to write. ## Creating a Management API key 1. Sign in to the **Enterspeed Management App**. 2. Go to **Settings → Management clients**. 3. Click to create a management client and give it a descriptive name, such as `claude-schema-authoring`. 4. Copy the key — it starts with `management-`. Store it in your secret manager immediately. The key this creates is **read-write**. Use it when the agent genuinely needs to author schemas or create configuration. To issue a read-only key, create the management client through the Management API and set the access level explicitly: ```bash theme={null} curl -X POST https://management.enterspeed.com/api/v1/tenant/management-clients/ \ -H "X-Api-Key: management-your-existing-key" \ -H "Content-Type: application/json" \ -d '{ "name": "claude-read-only", "accessLevel": "ReadOnly" }' ``` The response contains the new key. Store it in your secret manager immediately. Omitting `accessLevel` creates a **read-write** client, which is why the Management App's create form produces read-write keys today. Give each agent its own key with a name you will recognise later. That way you can revoke one integration without disturbing the others. ## Connecting a client The pattern is the same for every MCP client: point it at the root URL with **HTTP** transport and set an `x-api-key` header. ```bash theme={null} claude mcp add enterspeed-management \ --scope user \ --transport http \ https://mcp.management.enterspeed.com/ \ -H "x-api-key: management-your-key" ``` The user scope (`--scope user`) installs the server for the current user across all projects. Use `--scope project` to install it for the current project only but for all users — that writes a checked-in `.mcp.json`, so use an environment variable for the key rather than pasting it. Then start a session and ask about your tenant: ```bash theme={null} claude > Give me an overview of my Enterspeed tenant. ``` VS Code's GitHub Copilot reads MCP servers from an `mcp.json` file. For a single workspace, put the config at `.vscode/mcp.json`; for every workspace, use the **MCP: Open User Configuration** command from the command palette. ```json theme={null} { "servers": { "enterspeedManagement": { "type": "http", "url": "https://mcp.management.enterspeed.com/", "headers": { "x-api-key": "management-your-key" } } } } ``` Reload the window (**Developer: Reload Window** from the command palette) and open the Copilot chat pane. The Enterspeed management tools appear in the tool picker once Copilot connects. Never commit `.vscode/mcp.json` with a real key. Use `${input:enterspeed-management-key}` with a matching [`inputs` entry](https://code.visualstudio.com/docs/copilot/reference/mcp-configuration) so VS Code prompts for the key on first use, or put the config in your user profile instead of the workspace. Open your Claude Desktop config file: * **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` * **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` Add the `mcpServers` entry: ```json theme={null} { "mcpServers": { "enterspeedManagement": { "type": "http", "url": "https://mcp.management.enterspeed.com/", "headers": { "x-api-key": "management-your-key" } } } } ``` Fully quit and relaunch Claude Desktop. The Enterspeed management tools now appear in the tool picker. Older Claude Desktop versions cannot open a remote MCP server directly from this file. If the server does not appear, use the `mcp-remote` bridge instead: ```json theme={null} { "mcpServers": { "enterspeedManagement": { "command": "npx", "args": [ "-y", "mcp-remote", "https://mcp.management.enterspeed.com/", "--header", "x-api-key:management-your-key" ] } } } ``` Any MCP-capable client follows the same pattern — the root URL, `http` transport, and an `x-api-key` header. Known-good examples: * **Cursor** — `.cursor/mcp.json`, same schema as VS Code. * **Windsurf** — settings → Cascade → MCP Servers, using the URL + header form. * **Zed** — settings under `"context_servers"`. Or simply ask your favorite AI agent how to connect to an MCP server that needs an `x-api-key` header. It should be able to generate the configuration for you. ## Known limitations **Custom connectors on claude.ai in the browser are not supported yet.** Browser-based custom connectors cannot send a custom HTTP header, and this server requires `x-api-key`. Use Claude Code, Claude Desktop, VS Code, Cursor, or your own agent instead. We are working on OAuth support to remove this restriction. Until it ships, a header-capable client is required. ## Sample prompts Use these to smoke-test a fresh connection. **Orientation** > Give me an overview of my Enterspeed tenant — how many sources, environments, and domains do I have, and how are they connected? **Schema investigation** > List my mapping schemas and show me the current version of the one that produces my product pages. What triggers it? **Authoring loop (needs a write-capable key)** > Write an index schema that indexes my `article` entities with title, publish date, and author. Dry-run it against a real article first, then save it as a new version. **Troubleshooting** > The route for `/products/red-shoe` is not returning what I expect. Inspect the route and the view behind it and tell me what is wrong. **Logs** > Show me any processing errors in the last 24 hours, grouped by schema. ## Troubleshooting | Symptom | Likely cause | Fix | | ------------------------------------------------------------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `401 Unauthorized` on every tool call | Wrong key type — an environment client key instead of a Management API key | Use a key starting with `management-`. Environment client keys belong to [Query MCP](/enterspeed/mcp-server/query-mcp/overview). | | Reads work, writes return a permission error | The key is read-only | Expected. Use a write-capable key only if the agent genuinely needs to create or update configuration. | | `deploy_not_permitted` when deploying a schema | The target environment is not one the hosted server may deploy to | Deploy to `dev`, `development`, `test`, or `staging`, or deploy to production from the Management App. | | `create_environment_client` succeeded but there is no key in the response | Working as intended — access keys are never returned through MCP | Fetch the key in the Management App. | | `429` with a `Retry-After` header | More than 60 requests in a minute on this key | Let the client back off, or split work across keys. | | Client cannot connect at all | Configured with a `/sse` path, or a proxy that buffers streamed responses | Use the root URL. Streamed responses must pass through unbuffered. | ## Next steps * [Tools](/enterspeed/mcp-server/management-mcp/tools) — the full tool inventory, read and write. * [Query MCP](/enterspeed/mcp-server/query-mcp/overview) — query your Enterspeed data instead of your configuration. # Tools Source: https://docs.enterspeed.com/enterspeed/mcp-server/management-mcp/tools The full Management MCP tool inventory — 22 read tools and 13 additive write tools, with no delete tools by design. The Management MCP server exposes **35 tools: 22 reads and 13 additive writes**. This page lists all of them so you can see exactly what an agent connected to your tenant is able to do. The tool set is fixed — it is the same for every tenant and every key. What differs is what your key is *allowed* to call: see [Authentication](/enterspeed/mcp-server/management-mcp/overview#how-authentication-works). **There are no delete tools, by design.** The server has no way to express a destructive request — every call it can make is a read, or a create/update on a fixed allow-list checked when the server starts. No tool can delete a schema, environment, source, domain, client, or view. ## Reading your tenant ### Orientation Start here when you want an agent to understand how a tenant is put together. | Tool | Type | What it does | | ------------------------------- | ---- | ------------------------------------------------------------------------------------- | | `get_tenant_overview` | Read | A single summary of the tenant — sources, environments, domains, and how they relate. | | `list_sources` | Read | Lists the tenant's sources and source groups. | | `list_environment_clients` | Read | Lists environment clients and their scopes. Access keys are never included. | | `inspect_domains_and_hostnames` | Read | Domains and the hostnames mapped to them. | ### Source entities | Tool | Type | What it does | | ---------------------- | ---- | -------------------------------------------------------------------- | | `list_source_entities` | Read | Lists ingested source entities, filterable by source group and type. | | `get_source_entity` | Read | Fetches one source entity in full, as ingested. | ### Mapping schemas | Tool | Type | What it does | | ------------------------ | ---- | ------------------------------------------------------------------------------- | | `list_mapping_schemas` | Read | Lists the tenant's mapping schemas. | | `get_mapping_schema` | Read | Fetches a schema, including a specific version. | | `search_mapping_schemas` | Read | Searches across schema content — useful for "which schema sets this property?". | ### Views, routes, and indexes | Tool | Type | What it does | | -------------------- | ---- | ------------------------------------------------------------------------------------------- | | `list_views` | Read | Lists generated views. | | `get_view` | Read | Fetches a single view's output. | | `inspect_route` | Read | Resolves a route and shows the view behind it. The first stop for "why is this URL wrong?". | | `inspect_indexes` | Read | Lists indexes and their field definitions. | | `get_index_document` | Read | Fetches a single indexed document. | ### Deployments, logs, and metrics | Tool | Type | What it does | | ----------------------------- | ---- | ------------------------------------------------------------ | | `get_environment_deployments` | Read | Deployment history for an environment. | | `query_tenant_logs` | Read | Queries tenant logs — errors, warnings, processing activity. | | `aggregate_tenant_logs` | Read | Aggregates logs, for example grouping errors by schema. | | `get_tenant_metrics` | Read | Tenant-level metrics. | ## Authoring schemas This group is the reason most people connect the server. The four read tools let an agent design and prove a schema before anything is written. | Tool | Type | What it does | | --------------------------------------- | --------- | ------------------------------------------------------------------- | | `get_schema_type_definitions` | Read | The type definitions an agent needs to write a valid schema. | | `dry_run_mapping_schema` | Read | Runs a schema without saving it and returns the output. | | `dry_run_mapping_schema_against_entity` | Read | Same, against one of your real source entities. | | `validate_mapping_schema_version` | Read | Validates a schema version without saving it. | | `create_mapping_schema` | **Write** | Creates a new mapping schema. | | `save_mapping_schema_version` | **Write** | Saves a new version of an existing schema. | | `deploy_mapping_schema` | **Write** | Deploys a schema version to an environment. Restricted — see below. | The dry-run and validate tools are reads, despite sounding like they change something. A read-only key can design, test, and validate a schema end to end — it just cannot save it. That makes a read-only key a good default even for authoring work, until you are happy with the result. `deploy_mapping_schema` on the hosted server only deploys to environments **named** `dev`, `development`, `test`, or `staging`. Anything else — including environments named `prod`, `production`, or `live` — is refused with a `deploy_not_permitted` error. Production deploys stay a deliberate action in the Management App. ## Creating and updating configuration All writes are **additive**: they create something new or update something that exists. None of them remove anything. | Tool | Type | What it does | | --------------------------- | --------- | ------------------------------------------------------------------------------------------------------------- | | `create_environment` | **Write** | Creates an environment. | | `update_environment` | **Write** | Updates an environment. Will not rename an environment onto the deploy-allowed list. | | `create_source_group` | **Write** | Creates a source group. | | `update_source_group` | **Write** | Updates a source group. | | `create_source` | **Write** | Creates a source. | | `update_source` | **Write** | Updates a source. | | `create_domain` | **Write** | Creates a domain. | | `update_domain` | **Write** | Updates a domain, including its hostnames. | | `create_environment_client` | **Write** | Creates an environment client. The generated access key is **not** returned — fetch it in the Management App. | | `update_environment_client` | **Write** | Updates an environment client's scopes. | **On an environment client, an empty index scope means *unrestricted*, not *no access*.** A partial update that dropped the field would therefore widen the client rather than narrow it. `update_environment_client` avoids this by reading the current client and merging your changes onto it, so an agent cannot silently remove an index restriction by omitting it — but it is worth knowing when you review what an agent changed. ## What is deliberately not exposed Some Management API capabilities are intentionally absent from the tool set: * **Anything that deletes.** No schema, environment, source, source group, domain, client, view, or index can be removed through MCP. * **Key rotation.** No tool can regenerate an access key. Combined with the redaction applied to every response, that means an agent can neither read an existing key nor replace one. * **Bulk and production deploy paths.** Deploying broadly, or to production, stays in the Management App. For the update tools, the permission is the *verb*, not the address. Several of the allowed updates share an address with a delete operation in the underlying API — the server can reach the update and has no way to reach the delete. ## Next steps * [Overview](/enterspeed/mcp-server/management-mcp/overview) — hostname, authentication, guardrails, and client setup. * [Query MCP](/enterspeed/mcp-server/query-mcp/overview) — query your Enterspeed data instead of your configuration. # Overview Source: https://docs.enterspeed.com/enterspeed/mcp-server/overview The Enterspeed MCP servers let AI clients query your data, manage your configuration, and search the documentation. Enterspeed hosts three [Model Context Protocol](https://modelcontextprotocol.io/) servers. Point an AI client at one and it discovers a set of tools it can call on your behalf. You do not need to host anything yourself. Ask questions about your data in Enterspeed using the MCP server for the Query API. Inspect and configure your tenant — schemas, environments, sources, domains, and clients. Search and retrieve documentation content or report back issues or suggestions. ## Which one do you need? | If you want to… | Use | | ------------------------------------------------------------------ | ---------------------------------------------------------------------- | | Query content, indexes, and source entities in an environment | [Query MCP](/enterspeed/mcp-server/query-mcp/overview) | | Understand or change how a tenant is configured, or author schemas | [Management MCP](/enterspeed/mcp-server/management-mcp/overview) | | Ask questions about Enterspeed itself | [Documentation MCP](/enterspeed/mcp-server/documentation-mcp/overview) | Query MCP and Management MCP are complementary — many people connect both, so an agent can read a schema, change it, and then query the result. ## How authentication works Both tenant-facing servers work the same way: they are **passthrough proxies that hold no credentials of their own**. Your client sends a key on every request, the server forwards it unchanged, and the underlying Enterspeed API makes every authorisation decision. That means the key you issue is the whole boundary. For Query MCP, a key scoped to one environment and one index prefix gives an agent exactly that much and no more. For Management MCP, the key's access level decides whether an agent can change anything at all. **Custom connectors on claude.ai in the browser are not supported yet.** Browser-based custom connectors cannot send a custom HTTP header, and both tenant-facing servers authenticate with one. Use Claude Code, Claude Desktop, VS Code, Cursor, or your own agent instead. We are working on OAuth support to remove this restriction. Until it ships, a header-capable client is required. The Documentation MCP server needs no authentication and is unaffected. ## Transport Both tenant-facing servers speak **MCP Streamable HTTP** on the root URL. Responses are delivered as a server-sent event stream on that same URL, so clients that label their transport *SSE* generally work when pointed at the root. There is no separate `/sse` endpoint — always configure the root URL, and use the `http` transport type where your client offers a choice. # Connecting a client Source: https://docs.enterspeed.com/enterspeed/mcp-server/query-mcp/connecting-a-client Wire VS Code, Claude Code, Claude Desktop, or the Anthropic Messages API to the Enterspeed Query MCP Server. The Enterspeed Query MCP Server speaks plain MCP Streamable HTTP, so any MCP-capable client can use it. This page covers the four most common integrations; the pattern (MCP URL + `x-api-key` header) is identical for every other MCP client — see [Other MCP clients](#other-mcp-clients) at the bottom. Prerequisites are the same in every case: * A scoped environment client key (`Query API` + `MCP Server` at minimum). See the [Overview](/enterspeed/mcp-server/query-mcp/overview#creating-a-scoped-key). * The production endpoint `https://mcp.query.enterspeed.com/`. Configure the **root URL** with the `http` transport type. There is no separate `/sse` endpoint — the server delivers its responses as a server-sent event stream on the root URL itself. ## Pick your client VS Code's GitHub Copilot reads MCP servers from an `mcp.json` file. For a single workspace, put the config at `.vscode/mcp.json`; for every workspace, use the **MCP: Open User Configuration** command from the command palette. ```json theme={null} { "servers": { "enterspeedQuery": { "type": "http", "url": "https://mcp.query.enterspeed.com/", "headers": { "x-api-key": "environment-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" } } } } ``` Reload the window (**Developer: Reload Window** from the command palette) and open the Copilot chat pane. The Enterspeed tools appear in the tool picker once Copilot connects. Never commit `.vscode/mcp.json` with a real key. Use `${input:enterspeed-key}` with a matching [`inputs` entry](https://code.visualstudio.com/docs/copilot/reference/mcp-configuration) so VS Code prompts for the key on first use, or put the config in your user profile instead of the workspace. ```bash theme={null} claude mcp add enterspeed \ --scope user \ --transport http \ https://mcp.query.enterspeed.com/ \ -H "x-api-key: environment-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" ``` The user scope (`--scope user`) installs the server for the current user across all projects. Use `--scope project` to install it for the current project only but for all users — that writes a checked-in `.mcp.json`, so use an environment variable for the key rather than pasting it. Then start a session and ask Claude to list your indices: ```bash theme={null} claude > List the Enterspeed indices I have access to. ``` The CLI stores the server configuration under `~/.claude.json`. Edit that file if you need to tweak the header or URL afterwards. Open your Claude Desktop config file: * **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` * **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` Add the `mcpServers` entry: ```json theme={null} { "mcpServers": { "enterspeedQuery": { "type": "http", "url": "https://mcp.query.enterspeed.com/", "headers": { "x-api-key": "environment-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" } } } } ``` Fully quit and relaunch Claude Desktop. The Enterspeed tools now appear in the tool picker. Older Claude Desktop versions cannot open a remote MCP server directly from this file. If the server does not appear at all, use the `mcp-remote` bridge instead: ```json theme={null} { "mcpServers": { "enterspeedQuery": { "command": "npx", "args": [ "-y", "mcp-remote", "https://mcp.query.enterspeed.com/", "--header", "x-api-key:environment-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" ] } } } ``` If Claude Desktop connects but shows *"no tools discovered"*, the key is almost certainly missing the `MCP Server` scope. Create a new key using the **AI Assistant** preset. This is the production path: a C# service opens an MCP session to the Enterspeed server, discovers the available tools, and runs a tool-use loop with Claude. The `x-api-key` header is set once on the MCP transport. **Why not the inline `mcp_servers` feature?** Anthropic's inline remote-MCP connector forwards an `Authorization: Bearer ` header to the upstream MCP server and does not let you override the header name. The Enterspeed MCP server reads `x-api-key`. Driving the tool-use loop yourself (as below) works today and gives you full control over retries, logging, and cost. ### Project setup The loop uses two NuGet packages: [`Anthropic.SDK`](https://www.nuget.org/packages/Anthropic.SDK) for the Messages API and [`ModelContextProtocol`](https://www.nuget.org/packages/ModelContextProtocol) for the MCP session. ```bash theme={null} dotnet new console -n EnterspeedClaudeClient cd EnterspeedClaudeClient dotnet add package Anthropic.SDK dotnet add package ModelContextProtocol dotnet user-secrets init dotnet user-secrets set "ANTHROPIC_API_KEY" "sk-ant-..." dotnet user-secrets set "ENTERSPEED_MCP_KEY" "environment-xxxxxxxx-..." ``` ### Program.cs ```csharp theme={null} using Anthropic.SDK; using Anthropic.SDK.Messaging; using Microsoft.Extensions.Configuration; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol.Transport; using System.Text.Json; var config = new ConfigurationBuilder() .AddUserSecrets() .AddEnvironmentVariables() .Build(); var anthropicKey = config["ANTHROPIC_API_KEY"]!; var enterspeedKey = config["ENTERSPEED_MCP_KEY"]!; // 1. Open an MCP session against the Enterspeed server var transport = new SseClientTransport(new SseClientTransportOptions { Endpoint = new Uri("https://mcp.query.enterspeed.com/"), AdditionalHeaders = new Dictionary { ["x-api-key"] = enterspeedKey } }); await using var mcp = await McpClientFactory.CreateAsync(transport); // 2. Discover the tools this key is allowed to use var mcpTools = await mcp.ListToolsAsync(); // 3. Hand the tool schemas to Claude as Messages API tools var claude = new AnthropicClient(new APIAuthentication(anthropicKey)); var tools = mcpTools .Select(t => new Tool { Name = t.Name, Description = t.Description ?? string.Empty, InputSchema = t.InputSchema }) .ToList(); var messages = new List { new(RoleType.User, "Find the three most recent published blog posts and summarise " + "them in one sentence each.") }; // 4. Tool-use loop — repeat until Claude produces a final answer while (true) { var response = await claude.Messages.GetClaudeMessageAsync(new MessageParameters { Model = AnthropicModels.Claude4Sonnet, MaxTokens = 4096, Messages = messages, Tools = tools }); // Record Claude's turn messages.Add(new Message(response)); if (response.StopReason != "tool_use") { Console.WriteLine(response.Message); break; } // Execute every tool_use block via the MCP client and feed results back foreach (var block in response.Content.OfType()) { var args = block.Input.Deserialize>() ?? new(); var mcpResult = await mcp.CallToolAsync(block.Name, args); messages.Add(new Message { Role = RoleType.User, Content = new List { new ToolResultContent { ToolUseId = block.Id, Content = mcpResult.Content.FirstOrDefault()?.Text ?? string.Empty } } }); } } ``` Run it: ```bash theme={null} dotnet run ``` Expected output includes a short summary of three blog posts, and the loop's intermediate turns show Claude calling `query_blog` (or the equivalent per-index tool for whatever index the key is scoped to). **SDK property names.** The exact property names on `ToolUseContent`, `ToolResultContent`, and the `Message` / `Tool` shapes evolve with the Anthropic.SDK package. If a symbol above does not resolve, check the current release notes for the corresponding type name — the orchestration pattern (list tools once, loop until `StopReason != "tool_use"`) stays the same. ### Keeping costs under control * **Cache the tool list.** Call `ListToolsAsync()` once per session, not per request. The MCP server also caches per API key for 5 minutes, so repeat calls are cheap even if you do list more often. * **Narrow the tool set with an Index Scope.** Fewer indices means fewer `query_*` tools surfaced to Claude, which means fewer input tokens. * **Use prompt caching on the tool list.** When you pass `tools` to the Messages API, mark the list with `cache_control: { type: "ephemeral" }` via `CacheControl`-style helpers in Anthropic.SDK — see the package README for the current property name. ## Other MCP clients The four clients above are the ones we test against regularly, but the MCP server is client-agnostic. Any MCP-capable client follows the same pattern — point it at `https://mcp.query.enterspeed.com/` with `http` transport and set an `x-api-key` header. Some known-good examples: * **Cursor** — `.cursor/mcp.json` with the same `servers` schema as VS Code. * **Windsurf** — settings → Cascade → MCP Servers, using the URL + header form. * **Continue** — `~/.continue/config.json` under `experimental.modelContextProtocolServers`. * **Zed** — settings under `"context_servers"`. If your client does not support custom headers, pass the key as an `?apiKey=` query-string parameter on the MCP URL instead. Avoid that in production — the key ends up in request logs along the whole path. **Custom connectors on claude.ai in the browser are not supported yet.** Browser-based custom connectors cannot send a custom HTTP header, and the query-string fallback is not a safe substitute for a shared URL. Use one of the clients above instead. We are working on OAuth support to remove this restriction. Until it ships, a header-capable client is required. ## Troubleshooting | Symptom | Likely cause | Fix | | ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | Client responds *"I don't have any tools"* | MCP server entry missing, or the URL is wrong | Confirm the config was reloaded by the client; hit `/health` on the MCP endpoint. | | Every tool call returns *"forbidden"* | `x-api-key` forwarded correctly but scope missing | Add `Query API` to the environment client. | | `mcp add` / config reload fails with *"failed to establish connection"* | Local proxy, VPN, or corporate firewall stripping SSE headers | Bypass the proxy (`NO_PROXY=mcp.query.enterspeed.com`) or try a different network. | | Tools list is empty after reload | Client caches tool lists per session | Fully quit and relaunch the client (on macOS, Cmd-Q in Claude Desktop; **Developer: Reload Window** in VS Code). | | `401` on every request | Wrong key type — Management API token instead of environment-client key | Create an **environment client** key, not a Management API token. | ## Next steps * [Overview](/enterspeed/mcp-server/query-mcp/overview) — sample prompts and scope reference. * [Connecting an agent](/enterspeed/mcp-server/query-mcp/connecting-an-agent) — Azure AI Foundry and custom-agent wiring. # Connecting an agent Source: https://docs.enterspeed.com/enterspeed/mcp-server/query-mcp/connecting-an-agent Wire a custom C# agent or Azure AI Foundry to the Enterspeed Query MCP Server. This guide covers two common agent setups: 1. **Custom C# agent** — you own the LLM orchestration and want MCP tools to be part of a larger workflow. Recommended when you need latency, auth, or retry control. 2. **Azure AI Foundry** — you want the Foundry Responses API or Agent service to handle orchestration, with the MCP server plugged in as a tool provider. Before you start, you need: * A scoped environment client key (`Query API` + `MCP Server` at minimum). See the [Overview](/enterspeed/mcp-server/query-mcp/overview#creating-a-scoped-key). * The production endpoint `https://mcp.query.enterspeed.com/` — the root URL, with `http` transport. There is no separate `/sse` endpoint. ## Part 1 — C# agent (MCP client SDK) Agents talk to the Enterspeed MCP server over **MCP Streamable HTTP**. In C#, the most direct client is the official [`ModelContextProtocol`](https://www.nuget.org/packages/ModelContextProtocol) NuGet package — it opens the connection, negotiates the protocol, lists tools, and lets you call them. Authentication is a single `x-api-key` header on the transport. The transport class is named `SseClientTransport` for historical reasons. Pointed at the root URL, as below, it speaks Streamable HTTP — you do not need a separate `/sse` endpoint, and there isn't one. ### Prerequisites ```bash theme={null} dotnet new console -n EnterspeedMcpAgent cd EnterspeedMcpAgent dotnet add package ModelContextProtocol ``` Store secrets with `dotnet user-secrets` (never commit them): ```bash theme={null} dotnet user-secrets init dotnet user-secrets set "ENTERSPEED_MCP_KEY" "environment-xxxxxxxx-..." ``` ### Program.cs ```csharp theme={null} using ModelContextProtocol.Client; using ModelContextProtocol.Protocol.Transport; var enterspeedKey = Environment.GetEnvironmentVariable("ENTERSPEED_MCP_KEY") ?? throw new InvalidOperationException("ENTERSPEED_MCP_KEY not set"); var transport = new SseClientTransport(new SseClientTransportOptions { Endpoint = new Uri("https://mcp.query.enterspeed.com/"), // The MCP server reads `x-api-key` on every request and forwards it to // the Enterspeed Query API, which validates scope and index restrictions. AdditionalHeaders = new Dictionary { ["x-api-key"] = enterspeedKey } }); await using var mcpClient = await McpClientFactory.CreateAsync(transport); // Discover what this key can do var tools = await mcpClient.ListToolsAsync(); foreach (var tool in tools) { Console.WriteLine($"- {tool.Name}: {tool.Description}"); } // Call a tool — here the static get_indices tool var result = await mcpClient.CallToolAsync( "get_indices", new Dictionary()); Console.WriteLine(result.Content[0]); ``` Run it: ```bash theme={null} dotnet run ``` The output starts with the tools available to your key (static tools, the dynamic `enterspeed_query` tool, and one `query_` tool per index you have access to) and then prints the result of the first tool call. ### Driving it from an LLM (tool-use loop) To turn this into an LLM-driven agent, hand the `tools` list to the model of your choice. The pattern is identical for every major LLM provider: 1. Call `ListToolsAsync()` once per session and cache the result. 2. Send the tool schemas to the LLM alongside the user prompt. 3. When the LLM emits a tool call, invoke `mcpClient.CallToolAsync(name, args)` and feed the response back in the next turn. 4. Repeat until the LLM produces a final answer. For Claude specifically, see [Connecting a client](/enterspeed/mcp-server/query-mcp/connecting-a-client) for a complete Messages-API example that wires this loop. ### Alternative — the Anthropic Remote MCP connector If you prefer to let Claude open the MCP connection itself via the inline `mcp_servers` feature of the Messages API, you can — but note that Anthropic's spec currently only supports an `Authorization: Bearer ` header on the upstream MCP server; it does not let you set a custom header name. The Enterspeed MCP server reads `x-api-key`, so the inline connector does not work with a raw scoped key today. Workarounds: * Pass the key as an `?apiKey=` query-string parameter on the MCP URL (supported by the server, but the key ends up in request logs — acceptable for prototyping only). * Use the explicit [`ModelContextProtocol`](https://www.nuget.org/packages/ModelContextProtocol) client above and drive the tool-use loop yourself. ### Alternative — Semantic Kernel plugin If you use Microsoft.SemanticKernel, turn every MCP tool into a `KernelFunction`: ```csharp theme={null} using Microsoft.SemanticKernel; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol.Transport; var transport = new SseClientTransport(new SseClientTransportOptions { Endpoint = new Uri("https://mcp.query.enterspeed.com/"), AdditionalHeaders = new Dictionary { ["x-api-key"] = enterspeedKey } }); await using var mcp = await McpClientFactory.CreateAsync(transport); var kernel = Kernel.CreateBuilder() .AddOpenAIChatCompletion("gpt-4o", azureOpenAiKey) .Build(); var tools = await mcp.ListToolsAsync(); kernel.Plugins.AddFromFunctions( "enterspeedQuery", tools.Select(t => t.AsKernelFunction())); var answer = await kernel.InvokePromptAsync( "List the Enterspeed indices. Then describe the first one."); Console.WriteLine(answer.GetValue()); ``` ## Part 2 — Azure AI Foundry (step-by-step) Azure AI Foundry's Responses API and Agent service can attach an MCP server as a tool provider. The flow is: **your code → Foundry (with `mcp_servers` in the request) → MCP server → Query API**. **Entra ID required for MCP on Foundry.** Foundry's MCP tool support routes through the Agent service orchestration layer, which requires **Microsoft Entra ID** authentication (service principal or user identity). A plain Foundry API key is not accepted for MCP-enabled requests. If you need a "simple API key" façade for end users, place a thin wrapper in front that holds the service principal. ### Step 1 — Create an AI Foundry project 1. In the Azure portal, create an **AI Foundry** resource. 2. Inside it, create a project — e.g. `enterspeed-mcp-demo`. 3. Pick a region that supports the Responses API with MCP tool calling. `swedencentral` is EU-based and supported; `eastus` is the most feature-complete. 4. Deploy a model. `gpt-4o` is a good starting point; Claude models (`claude-sonnet-4`) are also available on `swedencentral`. ### Step 2 — Create a service principal ```bash theme={null} az ad sp create-for-rbac \ --name "enterspeed-mcp-agent" \ --role "Azure AI User" \ --scopes "/subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts/" ``` Save the returned `appId`, `password`, and `tenant`. If the subscription owner has not granted the service principal the **Azure AI User** role on the AI Services account, no MCP-enabled API call will succeed. This is the most common failure mode. ### Step 3 — Configure secrets ```bash theme={null} dotnet user-secrets set "AZURE_TENANT_ID" "" dotnet user-secrets set "AZURE_CLIENT_ID" "" dotnet user-secrets set "AZURE_CLIENT_SECRET" "" dotnet user-secrets set "FOUNDRY_ENDPOINT" "https://.services.ai.azure.com/api/projects/" dotnet user-secrets set "FOUNDRY_MODEL" "gpt-4o" dotnet user-secrets set "ENTERSPEED_MCP_KEY" "environment-xxxxxxxx-..." ``` ### Step 4 — Call the Responses API with MCP attached ```csharp theme={null} using Azure.Identity; using Azure.Core; using System.Net.Http.Json; using Microsoft.Extensions.Configuration; var config = new ConfigurationBuilder() .AddUserSecrets() .Build(); var credential = new ClientSecretCredential( config["AZURE_TENANT_ID"], config["AZURE_CLIENT_ID"], config["AZURE_CLIENT_SECRET"]); var token = await credential.GetTokenAsync( new TokenRequestContext(new[] { "https://ai.azure.com/.default" })); var http = new HttpClient { BaseAddress = new Uri(config["FOUNDRY_ENDPOINT"]!) }; http.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token.Token); var body = new { model = config["FOUNDRY_MODEL"], input = "List the Enterspeed indices I have access to.", tools = new object[] { new { type = "mcp", server_label = "enterspeedQuery", server_url = "https://mcp.query.enterspeed.com/", // Foundry forwards this header to the MCP server on every call headers = new Dictionary { ["x-api-key"] = config["ENTERSPEED_MCP_KEY"]! }, require_approval = "never" } } }; var response = await http.PostAsJsonAsync( "/openai/responses?api-version=2025-03-01-preview", body); response.EnsureSuccessStatusCode(); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ### Step 5 — Validate end-to-end Run this known-good smoke sequence: * **"List my Enterspeed indices"** — expect an index-listing tool call, then a list in the response. * **"Describe index ``"** — expect a `describe_index` call with fields grouped by type. * **A domain-specific question** — expect one or more `query_*` tool calls and a synthesised answer. If the model returns "I don't have access to that tool" after step 1 succeeded, the key most likely lacks the `Query API` scope. The tool was listed (because `MCP Server` is present) but the call is rejected by the Query API. ### Foundry Agent service (alternative) Foundry also has a persistent **Agent service**. The MCP wiring is identical (an `mcp` tool with `server_url` and `headers`) but the agent persists across calls. Use it when you want a long-running conversation with the same tool set attached. The same Entra ID requirement applies. ## Custom domain / enterprise note If you are proxying the MCP server behind your own domain: * The Query API still validates the scoped key — the proxy must forward `x-api-key` verbatim. * The proxy must support HTTP streaming (chunked / SSE). Several AWS ALB and classic CDN configurations do not by default. * The MCP transport is Streamable HTTP over HTTP/1.1. Check your hop-by-hop limits before forcing HTTP/2. ## Troubleshooting | Symptom | Likely cause | Fix | | --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | Foundry: `403 Identity (object id: ) does not have permissions for Microsoft.MachineLearningServices/workspaces/agents/action actions.` | Using a Foundry API key, or the service principal is missing the `Azure AI User` role | Switch to service principal auth and grant the role on the AI Services resource. | | Foundry returns instantly with no tool calls | MCP server not reachable from Foundry, or TLS cert mismatch | Curl the MCP health endpoint from a network location similar to Foundry's egress. | | Tool list arrives but every call returns empty | `x-api-key` lost along the proxy chain | Trace the header end-to-end; the MCP server requires it on every request, not just at session start. | | Agent times out on a long query | Default HTTP client timeout of 2 minutes exceeded | Paginate the prompt, or raise the timeout on your side. | ## Next steps * [Connecting a client](/enterspeed/mcp-server/query-mcp/connecting-a-client) — VS Code, Claude Code, Claude Desktop, and the Anthropic Messages API in C#. * [Overview](/enterspeed/mcp-server/query-mcp/overview) — sample prompts and the full scope table. # Overview Source: https://docs.enterspeed.com/enterspeed/mcp-server/query-mcp/overview Connect an AI client to the Enterspeed Query API — query your indexes, views, and auto-indexed source entities in natural language. The Enterspeed Query MCP Server turns the Enterspeed Query API into a set of [Model Context Protocol](https://modelcontextprotocol.io/) tools that AI clients (Claude, Cursor, custom agents, Azure AI Foundry, etc.) can discover and call. You do not need to host anything yourself — Enterspeed runs the server for you. This page tells you **where the server lives**, **how authentication works**, and **what scopes to request**. For step-by-step wiring guides, see [Connecting a client](/enterspeed/mcp-server/query-mcp/connecting-a-client) and [Connecting an agent](/enterspeed/mcp-server/query-mcp/connecting-an-agent). To configure your tenant rather than query it, see [Management MCP](/enterspeed/mcp-server/management-mcp/overview). ## Hostname | Environment | URL | Transport | | -------------- | ----------------------------------- | ------------------- | | **Production** | `https://mcp.query.enterspeed.com/` | MCP Streamable HTTP | A health check is available without authentication: ```bash theme={null} curl -s https://mcp.query.enterspeed.com/health # "ok" ``` The server speaks **MCP Streamable HTTP** on the root URL. Responses are delivered as a server-sent event stream on that same URL, so clients that label their transport *SSE* generally work when pointed at the root. There is no separate `/sse` endpoint — always configure the root URL. ## How authentication works The MCP server is a thin proxy. All authorisation decisions happen in the Enterspeed Query API. Your MCP client sends a scoped environment client key on every request; the MCP server forwards it as-is; the Query API validates the scope and any index restrictions before returning data. ``` AI client ──x-api-key──► mcp.query.enterspeed.com ──x-api-key──► query.enterspeed.com │ validates scopes + IndexScope ``` Clients authenticate with a single HTTP header on every MCP request: ``` x-api-key: environment-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx ``` The key is a **scoped environment client key** issued from the Enterspeed Management App. It is *not* a Management API key — that is what [Management MCP](/enterspeed/mcp-server/management-mcp/overview) uses. The server also accepts an `?apiKey=` query-string fallback for tools that cannot set headers. Prefer the header whenever possible: a key in a URL ends up in access logs along the whole request path. The key is checked when a tool is *called*, not when the session is opened. A client with a missing or invalid key can still connect and list the static tools — which is why an unexpectedly short tool list is the fastest signal that a key is wrong or under-scoped. ### Rate limit Requests are counted per API key over a rolling one-minute window, with a limit of **120 requests per minute**. Exceeding it returns: ``` HTTP 429 Too Many Requests Retry-After: Too many requests. Retry after seconds. ``` Well-behaved MCP clients back off and retry. The `/health` endpoint is exempt. ## Required scopes The platform supports component-scoped environment keys. For MCP use, your key **must** include: | Scope | Why | Grants | | ------------------------------ | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `MCP Server (AI Agent Access)` | Gates the MCP tool endpoints. Without it, the server sees zero dynamic or per-index tools. | Ability to list MCP tools. | | `Query API` | Needed to query your Enterspeed data. | Access to both **schema-transformed data** (the `query_*` tools and the unified `enterspeed_query` tool) and **auto-indexed data** (the source tools). | `MCP Server` alone is not useful — it must be combined with `Query API`, and Enterspeed enforces that pairing when you save the client. There are exactly four scopes: **Delivery API**, **Query API**, **Routes API**, and **MCP Server**. There is no separate Source API scope — access to auto-indexed source entities comes with **Query API**, narrowed by the [index scope](#index-scopes) below. ### Scope presets When creating an environment client, the Management App exposes these presets so you do not have to toggle scopes manually: | Preset | Scopes bundled | Typical use case | | ---------------- | ------------------------- | ------------------------------------------------------------------------------------ | | **Standard** | Delivery + Query + Routes | Traditional delivery clients, no AI. | | **AI Assistant** | Query + MCP Server | Full data access for AI agents — both schema-transformed data and auto-indexed data. | The **AI Assistant** preset is the right default for MCP integrations. See [Using environment clients](/enterspeed/getting-started/environment-clients#scope-presets) for the full table and how to manage scopes in the Management App. ## Index scopes On top of component scopes, each environment client can carry an optional **Index Scope** that restricts *which indices* the key can see. Patterns are matched against the fully-qualified index name. | Pattern | Matches | | ---------- | --------------------------------------------- | | `blog*` | every index whose name starts with `blog` | | `cms:*` | every index under the `cms` source group | | `*:entity` | every index ending in `entity` | | *(unset)* | no restriction — all indices the scope allows | Filtering is enforced by the Query API, so AI clients get a pre-trimmed tool list. This is ideal when you want to give a public-facing AI assistant access to, say, only a marketing-blog index, without exposing the rest of the environment. **An empty index scope means unrestricted, not restricted.** Leaving it unset grants every index the key's scopes allow. If you want to limit an AI assistant to a subset of your data, you must set a pattern explicitly — omitting the field is the *open* setting, not the safe one. Patterns are read in two families, split by the colon: * **Without a colon** — matches index names, for example `blog*`. * **With a colon** — matches auto-indexed source entities as `sourceGroup:entityType`, for example `cms:*`. Each family is only restricted if you supply at least one pattern for it. So a key configured with only `cms:*` restricts auto-indexed source entities while leaving index queries **unrestricted**. Set patterns in both families when you want to narrow both. ## Creating a scoped key 1. Sign in to the **Enterspeed Management App**. 2. Select the tenant and environment you want the AI client to query. 3. Go to **Environment → API Keys**. 4. Click **Create API key**. 5. Pick the **AI Assistant** preset. 6. *(Optional)* Set an **Index Scope** pattern like `blog*` to restrict access. 7. Give the key a descriptive name, e.g. `claude-prod-blog-assistant`. 8. Click **Create** — the key is shown **once**. Store it in your secret manager immediately. The new key has the form `environment-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`. See [Using environment clients](/enterspeed/getting-started/environment-clients) for the full environment-client flow, including regenerating a key. ## What tools the agent gets Every tool is a read — the server has no way to change your data or your configuration. To configure a tenant, an agent needs [Management MCP](/enterspeed/mcp-server/management-mcp/overview) instead. The tool list is assembled per key, so two clients pointed at the same server can see different tools. It comes in three layers. **Always present** — the same eight tools for everyone: | Tool | What it does | | ------------------------------------------------------- | ----------------------------------------------------------------------- | | `get_indices` | Lists the indexes the key can see. The usual starting point. | | `describe_index` | Describes one index — its fields, and which are searchable or sortable. | | `get_query_items` | Queries a single index with filters, sorting, pagination, and facets. | | `get_multiple_query_items` | Queries several indexes in one round-trip. | | `get_source_items` | Fetches auto-indexed source entities. | | `get_indices_raw_source_entities_by_source_group_alias` | Discovers which entity types exist in a source group. | | `get_operators` | Lists the filter operators available to you. | | `retrieve_query_api_status` | Reports Query API status. | **Per index** — one `query_` tool for each index the key can reach, generated from that index's own fields. These give an agent a typed, index-specific way to query, so it does not have to construct a generic filter by hand. **Unified** — an `enterspeed_query` tool that targets several indexes in a single call. All three layers respect the key's [index scope](#index-scopes), so the agent is handed a pre-trimmed list rather than being told "no" after it tries. If an agent only ever shows the eight tools above, the per-index layer is missing — which almost always means the key lacks the `MCP Server` scope, or is invalid. ## Sample prompts Use these when demoing or smoke-testing a fresh MCP connection. ### Connectivity check > List the Enterspeed indices you have access to, then tell me how many of them there are and what their naming convention looks like. Expected behaviour: the agent calls the index-listing tool and returns a list filtered by the key's Index Scope. ### Schema discovery > Describe the `blog` index. What fields does it have, which ones are searchable, and which are sortable? Expected behaviour: the agent calls `describe_index` with `indexAlias: blog`. A good answer groups fields by type (keyword, text, date, integer, etc.). ### Single-index query > Find the five most recent blog posts authored by `alice` and return their titles, publish dates, and URLs. Expected behaviour: the agent calls `query_blog` with a filter on `author = alice`, sort descending by `publishedAt`, and `pagination.pageSize = 5`. ### Multi-index query > Across the `products` and `productsv2` indices, find items with `category = beverages` and under 100 kcal per 100g. Return the top three sorted by energy ascending. Expected behaviour: the agent calls the unified `enterspeed_query` tool with a `queries` array that targets both indices in a single round-trip. ### Auto-indexed data exploration > Show me five raw source entities of type `article` from the `cms` source group. Which fields are set on the first one? Expected behaviour: the agent discovers types via `get_indices_raw_source_entities_by_source_group_alias`, then pulls five entities via `get_source_items`. Access to auto-indexed data comes with the `Query API` scope — no extra scope is needed beyond the standard `Query API` + `MCP Server` combination. ### Authorisation check > Which operators can I use to filter queries in Enterspeed? Expected behaviour: the agent calls the `get_operators` tool. The list typically includes `contains`, `equals`, `notEquals`, `in`, `lessThan`, `lessThanOrEquals`, `greaterThan`, and `greaterThanOrEquals` — the concrete set is data-driven, so trust the tool response over this page. ### Tool-listing sanity check > What tools do you have available and which Enterspeed indices are they tied to? Expected behaviour: the agent enumerates the full tool set. If the dynamic and per-index groups are empty, the key is most likely missing the `MCP Server` scope — that is the fastest way to spot a misconfigured key. ## Troubleshooting | Symptom | Likely cause | Fix | | ---------------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `401 Unauthorized` from the MCP server | `x-api-key` missing or malformed | Confirm the header is present on every MCP request. | | Agent lists only a small static set of tools | Key is missing the `MCP Server` scope | Recreate the key with the *AI Assistant* preset. | | Agent sees fewer indices than expected | `IndexScope` pattern is too narrow | Widen the Index Scope on the environment client, or remove it for all-index access. | | Tools listed but `query_*` returns "forbidden" | Key has `MCP Server` but not `Query API` | Add the `Query API` scope. | | `401` on every request | Wrong key type — a Management API key instead of an environment client key | Create an **environment client** key. Management API keys belong to [Management MCP](/enterspeed/mcp-server/management-mcp/overview). | | `429` with a `Retry-After` header | More than 120 requests in a minute on this key | Let the client back off, or split work across keys. | | Client cannot connect at all | Configured with a `/sse` path, or a proxy that buffers streamed responses | Use the root URL. Streamed responses must pass through unbuffered. | ## Known limitations **Custom connectors on claude.ai in the browser are not supported yet.** Browser-based custom connectors cannot send a custom HTTP header, and this server requires `x-api-key`. Use Claude Code, Claude Desktop, VS Code, Cursor, or your own agent instead. We are working on OAuth support to remove this restriction. Until it ships, a header-capable client is required. ## Next steps * [Connecting a client](/enterspeed/mcp-server/query-mcp/connecting-a-client) — VS Code, Claude Code, Claude Desktop, or the Anthropic Messages API in C#. * [Connecting an agent](/enterspeed/mcp-server/query-mcp/connecting-an-agent) — wire a custom C# agent or Azure AI Foundry to the MCP server. * [Management MCP](/enterspeed/mcp-server/management-mcp/overview) — configure your tenant instead of querying it. # Filter Expressions Source: https://docs.enterspeed.com/enterspeed/reference/filter-expressions Filter expressions are used when doing lookups in JSON or JavaScript schemas. Below is a list of examples of what you can do with the filter. If you are using JavaScript and your filter contains expressions, it's often easier to use string interpolation instead of string concatination. See [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#string_interpolation) . If you are comparing values containing URI special characters, you should URI encode the value in your filter expression. See [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent). ```javascript theme={null} filter(`originParentId eq '${encodeURIComponent(sourceEntity.originId)}'`) ``` ## Examples Examples of supported binary operators & expressions: ```javascript title="EQUALS operator" theme={null} // Property value equal to constant string value originId eq '100' // Property value equal to expressed string value originId eq '${sourceEntity.properties.selectedOtherPageId}' // Property value is null originParentId eq null // Property value equal to integer value properties.price eq 9 // Property value equal to decimal value properties.price eq 9.99 // Property value equal to boolean value (true/false) properties.isFeatured eq true ``` ```javascript title="EQUALS operator" theme={null} // Property value equal to constant string value originId eq '100' // Property value equal to expressed string value originId eq '{p.selectedOtherPageId}' // Property value is null originParentId eq null // Property value equal to integer value properties.price eq 9 // Property value equal to decimal value properties.price eq 9.99 // Property value equal to boolean value (true/false) properties.isFeatured eq true ``` ```javascript title="NOT EQUALS operator" theme={null} // Property value not equal to constant string value originId ne '100' // Property value not equal to expressed string value originId ne '${sourceEntity.properties.selectedOtherPageId}' // Property value is not null originParentId ne null // Property value not equal to integer value properties.price ne 9 // Property value not equal to decimal value properties.price ne 9.99 // Property value not equal to boolean value (true/false) properties.isFeatured ne true ``` ```javascript title="NOT EQUALS operator" theme={null} // Property value not equal to constant string value originId ne '100' // Property value not equal to expressed string value originId ne '{p.selectedOtherPageId}' // Property value is not null originParentId ne null // Property value not equal to integer value properties.price ne 9 // Property value not equal to decimal value properties.price ne 9.99 // Property value not equal to boolean value (true/false) properties.isFeatured ne true ``` ```javascript title="AND operator" theme={null} type eq 'article' and properties.isFeatured eq true ``` ```javascript title="AND operator" theme={null} type eq 'article' and properties.isFeatured eq true ``` ```javascript title="OR operator" theme={null} type eq 'article' or type eq 'contentPage' ``` ```javascript title="OR operator" theme={null} type eq 'article' or type eq 'contentPage' ``` ```javascript title="An array contains any matching value:" theme={null} // Contains specific redirect with constant string value redirects/any(r: r eq '/old-page') // Contains specific tags with expression string value properties.tags/any(t: t eq '${sourceEntity.properties.selectedTag}') // Contains articles that feature, matching constant boolean value properties.articles/any(a: a.isFeatured eq true) ``` ```javascript title="An array contains any matching value:" theme={null} // Contains specific redirect with constant string value redirects/any(r: r eq '/old-page') // Contains specific tags with expression string value properties.tags/any(t: t eq '{p.selectedTag}') // Contains articles that feature, matching constant boolean value properties.articles/any(a: a.isFeatured eq true) ``` Please note that our filter expressions only support one use of `any` per expression.\ E.g. `properties.tags/any(t: t eq 'tag1') or properties.tags/any(t: t eq 'tag2')` is not supported. ```javascript title="Check whether the element exists in this collection" theme={null} // Finds matches in predefined integers array properties.favoriteId in (100, 200) // Finds matches in predefined strings array properties.color in ('blue', 'red', 'green') // Finds matches in favorites selection (array of integers) properties.favoriteId in (${sourceEntity.properties.favoriteIds}) // Finds matches in colors selection (array of strings) properties.color in (${sourceEntity.properties.colors.map(c => `'${c}'`)}) ``` ```javascript title="Check whether the element exists in this collection" theme={null} // Finds matches in predefined integers array properties.favoriteId in (100, 200) // Finds matches in predefined strings array properties.color in ('blue', 'red', 'green') // Finds matches in favorites selection (array of integers) properties.favoriteId in {properties.favoriteIds} // Finds matches in colors selection (array of strings) properties.color in {properties.selectedColors} ``` ```js title="Full example" theme={null} export default { // ... properties: function (sourceEntity, context) { return { // ... sportArticles: context .reference('featuredSportArticle') .filter(`type eq 'article' and properties.tags/any(t: t eq '${sourceEntity.properties.tag}')`) .orderBy({ propertyName: 'properties.metaData.sortOrder', direction: "asc"}) .limit(5) } }, }; ``` ```json title="Full example" theme={null} "sportArticles": { "type": "array", "input": { "$lookup": { "filter": "type eq 'article' and properties.tags/any(t: t eq '{p.tag}')", "orderBy": { "property": "properties.metaData.sortOrder", "sort": "asc" }, "top": 5 } }, "var": "article", "items": { "type": "object", "properties": { "title": "{article.p.title}", "content": "{article.p.content}" } } } ``` # Organising your schemas Source: https://docs.enterspeed.com/enterspeed/reference/folders Schema folders To better structure and organize your schemas, we recommend using our folder feature. When naming your schema, you can organize them into folders by prepending the name with your desired folder name and slash (`/`). For instance, if you have a schema called `Currency`, you can name or rename it to `Global/Currency`. This will create a folder called `Global`, while the schema is still called `Currency`. If you want to delete the folder, simply remove the `Global/` part from the name. We support folders up to two levels deep, meaning a folder can also have subfolders, for instance `Global/Various/Currency`. When creating a schema, an alias is automatically generated while you're typing. In order to not have the folder names "spill" into the alias, folder names will automatically be removed from the alias as soon as you type a slash `/`. # Actions Source: https://docs.enterspeed.com/enterspeed/reference/js/full-schema/actions The `actions` method is used if you need to trigger new processing - which could be processing another schema or pushing generated views to a third-party application - using web hooks or such as Algolia. # ActionsContext object The `ActionsContext` object is passed into the actions method and gives you access to the `reprocess` and `destination` functions which are described below. ## Methods | Method | Description | | --------------------------- | ---------------------------------------------------------------- | | [reprocess](#reprocess) | Reprocess a schema based on its alias. | | [destination](#destination) | Specifies the destination where the generated view is pushed to. | ### reprocess `reprocess(schemaAlias)` Note that `reprocess` only triggers whenever the source entity is changed. This means that a deploy of the schema will not trigger the reprocess. Read more about how reprocessing is working, when to use it and when not to use it in the [key concepts](/enterspeed/key-concepts/reprocessing). #### Parameters | Parameter | Type | Description | | ------------- | ------ | ---------------------- | | `schemaAlias` | string | The alias of a schema. | #### Required function calls After the `reprocess` function it's required to call one of the following functions to define what source entities you want to reprocess. Use the `byOriginId` function to reprocess a specific entity based on its originId. #### Parameters | Parameter | Type | Description | | ---------- | ------ | --------------------------------------- | | `originId` | string | The original id from the source system. | ```js title="reprocess by byOriginId" theme={null} context .reprocess('mySchemaAlias') .byOriginId(sourceEntity.properties.link.id) ``` Use the `bySchema` function to reprocess all source entities a specific schema has a trigger on. Caution: Often it's better to reprocess a specific source entity instead a schema and all of its matching source entities, but sometimes it's necessary to reprocess an entire schema. Note: Even though you are reprocessing an entire schema, you still need to specific the source group (see optional function call) if it's different from the source group of the source entity triggering the reprocess. ```js title="reprocess by bySchema" theme={null} context .reprocess('mySchemaAlias') .bySchema() ``` Using the `filter` function lets you do a dynamic search for source entities you want to reprocess. #### Parameters | Parameter | Type | Description | | --------- | ------ | ------------------------------------------------------------------------------------------------ | | `filter` | string | Your filtering criteria. [See list of filter examples](/enterspeed/reference/filter-expressions) | ```js title="reprocess by filter" theme={null} context .reprocess('mySchemaAlias') .filter("type eq 'account' and properties.internalId eq '1234'") ``` The `filter` function is limited to a maximum number of items to return, based on the tenant plan. It can however be increased per tenant basis. Please reach out to Enterspeed if needed. The default limits are: * Free & Premium plans: 100 items * Enterprise plan: 500 items Use the `parent` function to reprocess the parent entity based on its originParentId. ```js title="reprocess by byOriginId" theme={null} context .reprocess('mySchemaAlias') .parent() ``` #### Optional function calls To filter the source entities you want to reprocess even further you can call some of the following optional functions. The `limit` function limits the number of source entities. #### Parameters | Parameter | Type | Description | | --------- | ------ | -------------------------------------- | | `limit` | number | The maximum number of source entities. | ```js title="reprocess by filter and limit" theme={null} context .reprocess('mySchemaAlias') .filter("type eq 'account'") .limit(5) ``` The `orderBy` function sorts the source entities. This is typically used in combination with `limit`. #### Parameters | Parameter | Type | Description | | --------- | ----------------------------------------------------- | ------------------------------------------------- | | `orderBy` | \{ propertyName: string, direction: "asc" \| "desc" } | Allows you to specify your desired sorting order. | ```js title="reprocess by filter and orderBy" theme={null} context .reprocess('mySchemaAlias') .filter("type eq 'account'") .orderBy({ propertyName: "properties.createdDate", direction: "desc"}) ``` Defines the source group of an source entity to process. If the source entity you want to reprocess is located in another source group than the current source entity, you must specific the source group. Note: `sourceGroup` can't be used on `parent` as parent is implicit in the same source group as the child. #### Parameters | Parameter | Type | Description | | ------------------ | ------ | -------------------------------------------------------------------------- | | `sourceGroupAlias` | string | The alias of the source group for the source entity you want to reprocess. | ```js title="reprocess by origin id and source group" theme={null} context .reprocess("mySchemaAlias") .byOriginId(sourceEntity.properties.link.id) .sourceGroup('anotherSourceGroup') ``` ### destination `destination(destinationAlias)` The `destination` method is used to push generated views for a schema to a webhook, to Algolia or another third-party application. #### Parameters | Parameter | Type | Description | | ------------------ | ------ | ---------------------------------------------------------------------- | | `destinationAlias` | string | The alias of the destination you want the generated view is pushed to. | ## Examples ```js title="actions example" theme={null} actions: function(sourceEntity, context) { context.reprocess('productCategory') .byOriginId(sourceEntity.properties.productCategoryPage.id) .sourceGroup('commerce'); context.reprocess('account') .filter("type eq 'account' and properties.internalId eq '1234'"); context.destination('webhook'); } ``` ```js title="actions can also be expressed as an arrow function expression to make it even more compact" theme={null} actions: (sourceEntity, context) => context.reprocess('productCategory').parent() ``` # Intro Source: https://docs.enterspeed.com/enterspeed/reference/js/full-schema/intro A full schema generates views of JSON data that you can fetch from the [Delivery API](/api-reference/delivery). In a full schema you can map properties, reference other full or partial schemas, define routes like URLs and more. ```js title="JavaScript full schema example" theme={null} /** @type {Enterspeed.FullSchema} */ export default { triggers: function(context) { context.triggers('cms', ['page']); }, routes: function(sourceEntity, context) { context.url(sourceEntity.url); }, properties: function (sourceEntity, context) { return { title: sourceEntity.properties.title, blocks: context.partial("blocks", sourceEntity.properties.blocks), aboutUsPage: context.reference("page").byOriginId(sourceEntity.properties.aboutUsPage.id), }; } } ``` # Properties Source: https://docs.enterspeed.com/enterspeed/reference/js/full-schema/properties The `properties` method is where you define the output that goes into the view you fetch from the [Delivery API](/api-reference/delivery) ```js title="Properties example" theme={null} properties: function (sourceEntity, context) { return { title: sourceEntity.properties.title, seo: { title: sourceEntity.properties.seoTitle, description: sourceEntity.properties.seoDescription, } categoryIds: sourceEntity.properties.categoryIds .map(categoryId => parseInt(categoryId)) }; } ``` The properties method must return an object with the data you want to include in your view. # PropertiesContext object The `PropertiesContext` object is passed into the `properties` method and gives you access to a set of methods described below. ## Methods | Method | Description | | ----------------------- | --------------------------------------------------------------------------------------------------------------------------- | | [lookup](#lookup) | Lookup allows you to search source entities using a filter string and work with the source entities directly in the schema. | | [partial](#partial) | Referencing a partial schema. Mapped data from a partial schema is embedded into the calling schema. | | [reference](#reference) | Referencing a full schema. References to other schemas are resolved on delivery request time. | ### lookup Lookup source entities. Lookup allows you to search source entities using a filter string and work with the data from the source entities directly in the schema. Note the `lookup` is an async function so you have to use [async/await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await) when you are using the `lookup` function. `lookup(filter)` The `lookup` function is limited to a maximum number of items to return, based on the tenant plan. It can however be increased per tenant basis. Please reach out to Enterspeed if needed. The default limits are: * Free & Premium plans: 100 items * Enterprise plan: 500 items #### Parameters | Parameter | Type | Description | | --------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `filter` | string | A filtering criteria. [See list of filter examples](/enterspeed/reference/filter-expressions) | | `lookupOptions` | object | An optional object with the following structure:
`{ excludeProperties?: boolean; }`

If `excludeProperties` is set to `true` the `lookup` will only return the base fields of the source entities and not all custom properties. This will help improve performance if you don't need the custom properties, but only some of the base fields like `originId`, `originParentId`, `url` | #### Required function calls After the `lookup` function it's required to call `toPromise` to excecute the query and return a [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). Using the toPromise function excecutes the query and return a promise you must resolve. ```js title="look toPromise" theme={null} const newsArticles: await context .lookup("type eq 'newsArticle'") .toPromise() ``` #### Optional function calls To filter the source entities even further you can call some of the following optional functions. The `limit` function limits the number of source entities. #### Parameters | Parameter | Type | Description | | --------- | ------ | ------------------------------------------------ | | `limit` | number | The maximum number of source entities to return. | ```js title="look and limit" theme={null} const newsArticles: await context .lookup("type eq 'newsArticle'") .limit(5) .toPromise() ``` The order sequence of the source entities. #### Parameters | Parameter | Type | Description | | --------- | ----------------------------------------------------- | ------------------------------------------------- | | `orderBy` | \{ propertyName: string, direction: "asc" \| "desc" } | Allows you to specify your desired sorting order. | ```js title="lookup and orderBy" theme={null} const newsArticles: await context .lookup("type eq 'newsArticle'") .orderBy({ propertyName: "properties.createdDate", direction: "desc"}) .toPromise() ``` The sourceGroup function lets you specify the source group. By default the source group of the current source entity is used. #### Parameters | Parameter | Type | Description | | ------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `sourceGroup` | string | Allows you to define a different source group. The sourceGroupAlias should be equal to the desired source group alias where you want to look for source entities. | If not defined, it uses the current source group. ```js title="lookup and sourceGroup" theme={null} const newsArticles: await context .lookup("type eq 'newsArticle'") .sourceGroup("anotherSourceGroup") .toPromise() ``` #### Examples ```js title="lookup with async/await" theme={null} properties: async function (sourceEntity, context) { const latestNews = await context .lookup("type eq 'newsArticle'") .orderBy({ propertyName: "properties.createdDate", direction: "desc"}) .limit(3) .toPromise(); const mappedNews = latestNews .map((news) => ({ url: news.url, title: news.properties.title, teaser: news.properties.teaser })); return { latestNews: mappedNews } } ``` ```js title="lookup without custom properties" theme={null} properties: async function (sourceEntity, context) { const baseNewsArticles = await context .lookup("type eq 'newsArticle'", { excludeProperties: true }) .limit(3) .toPromise(); const mappedNews = baseNewsArticles .map((news) => ({ originId: news.originId, url: news.url })); return { latestNews: mappedNews } } ``` *** ### partial Referencing a partial schema. The partial mapping property type allows for dynamically including partial schemas into the main schema. This is useful when you need to iterate an array of different objects that has an identifier, like an ID, alias or similar. Partial schemas are typically used when you want a reusable schema for mapping data that is part of the same entity. E.g. if metadata (title, description, etc.) is the same across different entity types, but the data lives on the entity itself. Read more about partial schemas [here](/enterspeed/key-concepts/partial-schemas) `partial(schemaAlias, input)` #### Parameters | Parameter | Type | Description | | ------------- | ------ | ------------------------------------------------------------ | | `schemaAlias` | string | The alias of a partial schema. | | `input` | object | You can pass whatever data you need for your partial schema. | #### Examples ```js title="partial" theme={null} contentBlocks: sourceEntity.properties.contentBlocks.map((contentBlock) => context.partial(`block-${contentBlock.contentType}`, contentBlock) ) ``` ```js title="passing extra properties to the partial schema" theme={null} contentBlocks: sourceEntity.properties.contentBlocks.map((contentBlock) => context.partial(`block-${contentBlock.contentType}`, { block: contentBlock, pageOriginId: sourceEntity.originId }) ) ``` The `input` defines what goes into the partial schema and can be any custom object, and the `schemaAlias` is used to resolve what partial schema to use. So, in this case, we could have a partial schema with an alias: block-headline. *** ### reference The `reference` are used to reference other views created from either this source entity or another source entity. When referenced, Enterspeed will resolve the view when requested by the Delivery API so that the data will stay up-to-date if a reference view is updated. The `reference` function is the starting point where you can use our fluent API to configure the reference(s) you want (by filter, by originId, take only top 5, and so on). Reference schemas are typically used when you are mapping data from another entity. E.g. a page has a reference to another page entity or media entity. Read more about reference schemas [here](/enterspeed/key-concepts/referencing-schemas) `reference(schemaAlias)` #### Parameters | Parameter | Type | Description | | ------------- | ------ | ---------------------- | | `schemaAlias` | string | The alias of a schema. | #### Required function calls After the `reference` function it's required to call one of the following functions to define what source entities you want references to. Using the byOriginId function lets you create a reference to a source entity by its oringinId. #### Parameters | Parameter | Type | Description | | ---------- | ------ | --------------------------------------- | | `originId` | string | The original id from the source system. | ```js title="reference by byOriginId" theme={null} contentTeaser: context .reference("contentTeaser") .byOriginId(sourceEntity.properties.link.id) ``` Using the byOriginIds function lets you create a references to a list of source entity by their oringinId. #### Parameters | Parameter | Type | Description | | ----------- | --------- | ---------------------------------------------- | | `originIds` | string\[] | A list of original ids from the source system. | ```js title="reference by byOriginIds" theme={null} contentTeaser: context .reference("contentTeaser") .byOriginIds(sourceEntity.properties.links.map(link => link.id)) ``` The children function creates a reference to all the children of the current source entity. It's basicly a shortcut for `.filter("originParentId eq '{sourceEntity.properties.originId}'")`. ```js title="reference by children" theme={null} childPages: context.reference("page").children() ``` ```js title="reference by children and type" theme={null} childPages: context.reference("page").children("type eq 'subpage'") ``` The `children` function is limited to a maximum number of items to return, based on the tenant plan. It can however be increased per tenant basis. Please reach out to Enterspeed if needed. The default limits are: * Free & Premium plans: 100 items * Enterprise plan: 500 items Using the filter function lets you do a dynamic search for source entities you want to make references to. #### Parameters | Parameter | Type | Description | | --------- | ------ | --------------------------------------------------------------------------------------------- | | `filter` | string | A filtering criteria. [See list of filter examples](/enterspeed/reference/filter-expressions) | ```js title="reference by filter" theme={null} newsTeasers: context .reference("newsTeaser") .filter("type eq 'newsArticle'") ``` The `filter` function is limited to a maximum number of items to return, based on the tenant plan. It can however be increased per tenant basis. Please reach out to Enterspeed if needed. The default limits are: * Free & Premium plans: 100 items * Enterprise plan: 500 items The parent function creates a reference to the parent of the current source entity. It's basicly a shortcut for `.filter("originId eq '{sourceEntity.properties.originParentId}'")`. ```js title="reference by parent" theme={null} parentPage: context .reference("page") .parent() ``` The self method creates a reference to a view created by the specified schema on the current source entity. The helper method is equivalent to calling `.byOriginId(sourceEntity.originId)`. ```js title="reference by self" theme={null} selfPage: context .reference("page") .self() ``` #### Optional function calls To filter the source entities you are making references to even further you can call some of the following optional functions. The `limit` function limits the number of references. #### Parameters | Parameter | Type | Description | | --------- | ------ | ------------------------------------------- | | `limit` | number | The maximum number of references to return. | ```js title="reference by filter and limit" theme={null} topFiveNewsTeasers: context .reference("newsTeaser") .filter("type eq 'newsArticle'") .limit(5) ``` The order sequence of references. #### Parameters | Parameter | Type | Description | | --------- | ----------------------------------------------------- | ------------------------------------------------- | | `orderBy` | \{ propertyName: string, direction: "asc" \| "desc" } | Allows you to specify your desired sorting order. | ```js title="reference by filter and orderBy" theme={null} newsArticles: context .reference("newsArticle") .filter("type eq 'newsArticle'") .orderBy({ propertyName: "properties.createdDate", direction: "desc"}) ``` The `sourceGroup` function lets you specify the source group. By default the source group of the current source entity is used. #### Parameters | Parameter | Type | Description | | ------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `sourceGroup` | string | Allows you to define a different source group. The sourceGroupAlias should be equal to the desired source group alias where you want to look for source entities. | If not defined, it uses the current source group. ```js title="reference by filter and sourceGroup" theme={null} newsTeasers: context .reference("newsTeaser") .filter("type eq 'newsArticle'") .sourceGroup("anotherSourceGroup") ``` The `first` function return the first item in the result set. The item i returned as an object instead of an array with one item. The function must always be called as the last call in the chain. ```js title="reference to the first news teaser" theme={null} newsTeaser: context .reference("newsTeaser") .filter("type eq 'newsArticle'") .orderBy({ propertyName: "properties.createdDate", direction: "desc"}) .first() ``` #### Examples ```js title="Property type: reference (static value) with originId" theme={null} seoData: context.reference("seo").byOriginId(sourceEntity.originId) ``` ```js title="reference by filter and sourceGroup" theme={null} products: context .reference("product") .filter("type eq 'product'") .orderBy({ propertyName: name, direction: "asc"}) .limit(10) .sourceGroup("commerce") ``` The `schemaAlias` can either be a static value, like "seo", or it can be a dynamic value being resolved from the Source Entity that is being processed by Enterspeed. This allows for supporting almost any use case. #### Reference response The response of references differs in V1 and V2+ of the delivery API. V1 return the id, type and wraps the properties from the referenced view in a `view` property. The response of references in V2+ is much cleaner and only returns the actual properties from the referenced schema. If a reference is not found in V2+, the reference information is added in the `missingViewReference` property in the `meta` object for debugging purpose. ```json title="Delivery API V1 uses a nested view property" theme={null} { // This example shows a response of a view with two schema references. // image1 is referencing a found view with a url and name property // and image2 is referencing a view that doesn't exist. "meta": { "missingViewReferences": [] }, "route": { "image1": { "id": "gid://Environment/8ef2bdc0-c352-4190-a344-c51d1f5e72ea/Source/dc5d9518-b96a-428a-a9b3-31fb601376c2/Entity/1234/View/image", "view": { "url": "https://test.com/how-to-write-a-good-blog-post.png", "name": "How To Write A Good Blog Post" }, "type": "ViewReference" }, "image2": { "id": "gid://Environment/8ef2bdc0-c352-4190-a344-c51d1f5e72ea/Source/dc5d9518-b96a-428a-a9b3-31fb601376c2/Entity/1235/View/image", "view": null, "type": "ViewReference" } } } ``` ```json title="Delivery API V2+ only returns the actual view properties" theme={null} { // This example shows a response of a view with two schema references. // image1 is referencing a found view with a url and name property // and image2 is referencing a view that doesn't exist. "meta": { "missingViewReferences": [ { "path": "image2", "viewId": "gid://Environment/8ef2bdc0-c352-4190-a344-c51d1f5e72ea/Source/dc5d9518-b96a-428a-a9b3-31fb601376c2/Entity/1235/View/image" } ] }, "route": { "image1": { "url": "https://test.com/how-to-write-a-good-blog-post.png", "name": "How To Write A Good Blog Post" }, "image2": null } } ``` # Routes Source: https://docs.enterspeed.com/enterspeed/reference/js/full-schema/routes The `routes` method is where you define how you fetch the generated view from the [Delivery API](/api-reference/delivery) If your view should be routable you must implement the `routes` method and call context methods to build routes. ```js title="Routes example" theme={null} routes: function(sourceEntity, context) { context.url(sourceEntity.url); context.handle('origin-' + sourceEntity.originId); } ``` # RoutesContext object The `RoutesContext` object is passed into the `routes` method and gives you access to a set of methods which is described below. ## Methods | Method | Description | | ----------------- | --------------------------------------------------------------------------------------------------------------------------- | | [handle](#handle) | A handle is a key from which you can fetch the view. A view can have multiple handles. | | [lookup](#lookup) | Lookup allows you to search source entities using a filter string and work with the source entities directly in the schema. | | [url](#url) | A string value that represents the url you want to fetch the view by. A view can only have one url. | ### handle Handle allows you to specify a key from which you can fetch the view from the Delivery API. `handle(handle)` #### Parameters | Parameter | Type | Description | | --------- | ------ | ------------------------------------------------------- | | `handle` | string | The key you use to fetch the view from the Delivery API | ### lookup Lookup allows you to search source entities using a filter string and build dynamic handles or URLS based on data from other source entities. Note the `lookup` is an async function so you have to use [async/await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await) or [`then`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then) when you are using the `lookup` function. `lookup(filter)` The `lookup` function is limited to a maximum number of items to return, based on the tenant plan. It can however be increased per tenant basis. Please reach out to Enterspeed if needed. The default limits are: * Free & Premium plans: 100 items * Enterprise plan: 500 items #### Parameters | Parameter | Type | Description | | --------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `filter` | string | A filtering criteria. [See list of filter examples](/enterspeed/reference/filter-expressions) | | `lookupOptions` | object | An optional object with the following structure:
`{ excludeProperties?: boolean; }`

If `excludeProperties` is set to `true` the `lookup` will only return the base fields of the source entities and not all custom properties. This will help improve performance if you don't need the custom properties, but only some of the base fields like `originId`, `originParentId`, `url` | #### Required function calls After the `lookup` function it's required to call `toPromise` to excecute the query and return a [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). Using the toPromise function excecutes the query and return a promise you must resolve. ```js title="look toPromise" theme={null} const category: await context .lookup(`originId eq '${sourceEntity.properties.categoryId}'`) .toPromise() ``` #### Optional function calls To filter the source entities even further you can call some of the following optional functions. The `limit` function limits the number of source entities. #### Parameters | Parameter | Type | Description | | --------- | ------ | ------------------------------------------------ | | `limit` | number | The maximum number of source entities to return. | ```js title="look and limit" theme={null} const mainCategory: await context .lookup(`type eq 'mainCategory'`) .limit(1) .toPromise() ``` The order sequence of the source entities. #### Parameters | Parameter | Type | Description | | --------- | ----------------------------------------------------- | ------------------------------------------------- | | `orderBy` | \{ propertyName: string, direction: "asc" \| "desc" } | Allows you to specify your desired sorting order. | ```js title="lookup and orderBy" theme={null} const categories: await context .lookup(`type eq 'category'`) .orderBy({ propertyName: "properties.createdDate", direction: "desc"}) .toPromise() ``` The sourceGroup function lets you specify the source group. By default the source group of the current source entity is used. #### Parameters | Parameter | Type | Description | | ------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `sourceGroup` | string | Allows you to define a different source group. The sourceGroupAlias should be equal to the desired source group alias where you want to look for source entities. | If not defined, it uses the current source group. ```js title="lookup and sourceGroup" theme={null} const categories: await context .lookup(`type eq 'category'`) .sourceGroup("anotherSourceGroup") .toPromise() ``` #### Examples ```js title="lookup using async/await" theme={null} routes: async function(sourceEntity, context) { const categories = await context.lookup(`originId in (${sourceEntity.properties.categoryIds.map(c => `'${c}'`)})`).toPromise(); categories.forEach((category) => { context.url(`${category.url}/${sourceEntity.properties.slug}`) }) } ``` ```js title="lookup using 'then'" theme={null} routes: function(sourceEntity, context) { context.lookup(`originId in (${sourceEntity.properties.categoryIds.map(c => `'${c}'`)})`) .toPromise() .then((categories) => { categories.forEach((category) => { context.url(`${category.url}/${sourceEntity.properties.slug}`) }) }); } ``` ### url url allows you to specify a url from which you can fetch the view from the delivery. `url(url)` #### Parameters | Parameter | Type | Description | | --------- | ------ | ------------------------------------------------------- | | `url` | string | The url you use to fetch the view from the Delivery API | ##### Optional function calls To the `url` function you can call some of the following optional functions. The `redirects` function creates incomming redirects to a specific url. Setting the redirects to an empty array clears any potential implicit redirects. #### Parameters | Parameter | Type | Description | | ----------- | --------- | ---------------------------------------------- | | `redirects` | string\[] | Adds a list of incomming redirects to the URL. | ```js title="routes with url and redirects" theme={null} context .url(sourceEntity.url) .redirects(sourceEntity.redirects); ``` ## Examples ```js title="routes example with url and multiple handles" theme={null} routes: function(sourceEntity, context) { context.url(sourceEntity.url); context.handle('origin-' + sourceEntity.originId); context.handle(sourceEntity.properties.entityKey); } ``` ```js title="routes example with single url" theme={null} routes: function(sourceEntity, context) { context.url(sourceEntity.url); } ``` ```js title="routes can also be expressed as an arrow function expression to make it even more compact" theme={null} routes: (sourceEntity, context) => context.url(sourceEntity.url) ``` You can then fetch the view using our [Delivery API](/api-reference/delivery) using either the url or one of the handles. You can also fetch multiple views in one request, although in this case it doesn't make sense to fetch the same view three time, but just to demonstrate if you want to fetch multiple different views in one request. ``` https://delivery.enterspeed.com/v2 ?url=/fairy-tales/the-emperors-new-clothes/ &handle=origin-1234 &handle=5678 ``` # Triggers Source: https://docs.enterspeed.com/enterspeed/reference/js/full-schema/triggers The `triggers` method is where you define the source group and the types the schema should process. You can add multiple triggers to a schema. ```js title="Triggers example" theme={null} triggers: function(context) { context.triggers('cms', ['contentPage', 'articlePage']); context.triggers('pim', ['product']); } ``` # TriggersContext object The `TriggersContext` object is passed into the `triggers` method and gives you access to a set of methods which is described below. ## Methods | Method | Description | | ----------------------- | ------------------------------------------------------------------------------- | | [triggers](#triggers-1) | Defines the source group and the source entity types the schema should process. | ### triggers Defines the source group and the source entity types the schema should process. `triggers(sourceGroupAlias, sourceEntityTypes)` #### Parameters | Parameter | Type | Description | | ------------------- | --------- | ---------------------------------------------------------------- | | `sourceGroupAlias` | string | The alias of the source group you want your schema to trigger on | | `sourceEntityTypes` | string\[] | A list of source entity types you want your schema to trigger on | ## Examples ```js title="example of triggers usage" theme={null} triggers: function(context) { context.triggers('cms', ['contentPage', 'articlePage']); } ``` ```js title="triggers can also be expressed as an arrow function expression to make it even more compact" theme={null} triggers: (context) => context.triggers('cms', ['contentPage', 'articlePage']) ``` # Index Source: https://docs.enterspeed.com/enterspeed/reference/js/index-schema/index-method The `index` object is where you define the structure of the index. The index defines what kind of data you can add to the index in the [properties](/enterspeed/reference/js/index-schema/properties) method. ```js title="Index example" theme={null} index: { fields: { sku: { type: "keyword" }, title: { type: "text" }, description: { type: "text" }, isActive: { type: "boolean" } } } ``` The index object must have a `fields` property with the fields you want to include in the index. Each field object must include `type`. The `type` specifies the data type for the index field. ## Metadata descriptions ### Index The `metadata` property on the index is optional. Use it to add a description to the index. The description is exposed by the [Enterspeed Query MCP](/enterspeed/mcp-server/query-mcp/overview) to give AI agents meaningful context about what the index contains. ```js title="Index description example" theme={null} index: { fields: { }, metadata: { description: "The product index contains all products available in the assortment" } } ``` ### Field The `metadata` property on a field is optional. Use it to add a description to the field. The description is exposed by the [Enterspeed Query MCP](/enterspeed/mcp-server/query-mcp/overview) to give AI agents meaningful context about what the field is used for. *The max character length of the field description is 50 characters.* ```js title="Field description example" theme={null} index: { fields: { isActive: { type: "boolean", metadata: { description: "A boolean value indicating if the SKU is active" } } } } ``` ## Types Setting the right types for the properties in your index is important. The types defines the intend of the fields and prevents data of other types from going into the index. The types also helps with effeciently index, search, and analyze of the data added to the index. ### Text | Type | Description | | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | keyword | The `Keyword` type is for non-analyzed string values used in e.g. filtering and sorting | | text | The `text` type will analyze string values. This makes it possible to do fuzzy search using the `search` property in the [Query API](/api-reference/query/query-items) but index time takes a bit longer | ### Numeric | Type | Description | | ------- | ------------------------------- | | integer | A 32 bit signed integer | | long | A 64 bit signed integer | | float | A 32 bit signed floating number | | double | A 64 bit signed floating number | ### Boolean | Type | Description | | ------- | ------------------------ | | boolean | A `true` / `false` value | ### Date | Type | Description | | ---- | ------------------------------------------------------------------------------------------------------------------------------------------ | | date | Support values like a Javascript Date object, a string like `2024-11-21` or `2024-11-21T23:00:00Z` or number of milliseconds since eclipse | ### List | Type | Description | | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | keyword\[] | List of the `Keyword` type for non-analyzed string values used in e.g. filtering and sorting | | text\[] | List of the `text` type for analyzing string values. This makes it possible to do fuzzy search using the `search` property in the [Query API](/api-reference/query/query-items) but index time takes a bit longer | | integer\[] | List of 32 bit signed integers | | long\[] | List of 64 bit signed integers | | float\[] | List of 32 bit signed floating numbers | | double\[] | List of 64 bit signed floating numbers | | boolean\[] | List of `true` / `false` values | | date\[] | List of dates with supported values like a Javascript Date object, strings like `2024-11-21T23:00:00Z` or numbers of milliseconds since eclipse | ### Range | Type | Description | | ------------ | ----------------------------------------------------------------------------------------------------------------------------- | | integerRange | Must be an object like
`{"gt": 10, "lt": 20}` or
`{"gte": 10, "lte": 20}` | | longRange | Must be an object like
`{"gt": 10, "lt": 20 }` or
`{"gte": 10, "lte": 20}` | | floatRange | Must be an object like
`{"gt": 10.0, "lt": 20.0}` or
`{"gte": 10.0, "lte": 20.0}` | | doubleRange | Must be an object like
`{"gt": 10.0, "lt": 20.0}` or
`{"gte": 10.0, "lte": 20.0}` | | dateRange | Must be an object like
`{"gt": "2025-01-01", "lt": "2025-02-01"}` or
`{"gte": "2025-01-01", "lte": "2025-02-01"}` | ### Object | Type | Description | | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | object | Supports any value of an object type, this object is not indexed and therefore not searchable, it can be used to associate a complex data type to an index item. | # Intro Source: https://docs.enterspeed.com/enterspeed/reference/js/index-schema/intro An index schema defines the structure of a Query index as well as the mapping of the items that goes into the index which are queryable from the [Query API](/api-reference/query). ```js title="JavaScript index schema example" theme={null} /** @type {Enterspeed.IndexSchema} */ export default { triggers: function(context) { context.triggers('pim', ['product']) }, index: { fields: { sku: { type: "keyword" }, title: { type: "text" }, description: { type: "text" }, isActive: { type: "boolean" } } }, properties: function (sourceEntity) { return { sku: sourceEntity.properties.sku, title: sourceEntity.properties.title, description: sourceEntity.properties.description, isActive: sourceEntity.properties.isActive } } } ``` # Properties Source: https://docs.enterspeed.com/enterspeed/reference/js/index-schema/properties The `properties` method is where you define the output of the items that goes into index you can query from the [Query API](/api-reference/query) ```js title="Properties example" theme={null} properties: function (sourceEntity) { return { sku: sourceEntity.properties.sku, title: sourceEntity.properties.title, description: sourceEntity.properties.description, isActive: sourceEntity.properties.isActive }; } ``` The properties method must return an object with the values for the fields you defined in the [index](/enterspeed/reference/js/index-schema/index-method) object. # Triggers Source: https://docs.enterspeed.com/enterspeed/reference/js/index-schema/triggers The `triggers` method is where you define the source group and the types the schema should process. You can add multiple triggers to a schema. ```js title="Triggers example" theme={null} triggers: function(context) { context.triggers('cms', ['contentPage', 'articlePage']); context.triggers('pim', ['product']); } ``` # TriggersContext object The `TriggersContext` object is passed into the `triggers` method and gives you access to a set of methods which is described below. ## Methods | Method | Description | | ----------------------- | ------------------------------------------------------------------------------- | | [triggers](#triggers-1) | Defines the source group and the source entity types the schema should process. | ### triggers Defines the source group and the source entity types the schema should process. `triggers(sourceGroupAlias, sourceEntityTypes)` #### Parameters | Parameter | Type | Description | | ------------------- | --------- | ---------------------------------------------------------------- | | `sourceGroupAlias` | string | The alias of the source group you want your schema to trigger on | | `sourceEntityTypes` | string\[] | A list of source entity types you want your schema to trigger on | ## Examples ```js title="example of triggers usage" theme={null} triggers: function(context) { context.triggers('cms', ['contentPage', 'articlePage']); } ``` ```js title="triggers can also be expressed as an arrow function expression to make it even more compact" theme={null} triggers: (context) => context.triggers('cms', ['contentPage', 'articlePage']) ``` # Intro Source: https://docs.enterspeed.com/enterspeed/reference/js/intro JavaScript is the default and prefered schema format in Enterspeed. With JavaScript schemas, you can build your schemas in a standard language most developers are already familiar with. This means that you have all the power and flexibility from JavaScript available when you are creating your schemas. ```js title="JavaScript schema example" theme={null} /** @type {Enterspeed.FullSchema} */ export default { triggers: function(context) { context.triggers('cms', ['page']); }, routes: function(sourceEntity, context) { context.url(sourceEntity.url); }, properties: function (sourceEntity, context) { const p = sourceEntity.properties; return { title: p.title, blocks: context.partial("blocks", p.blocks), aboutUsPage: context.reference("page").byOriginId(p.aboutUsPage.id), }; } } ``` The concepts in JavaScript schemas are similar to the concepts from JSON schemas, with triggers, route, properties and so on, and since the source entity and a `context` object (used for making references, partials, etc.) are passed in as parameters you can even create unit tests of your schemas if you want to. ## Destructuring You can destruct parameters, so the `routes` and `properties` methods in the above example can be simplified to: ```js title="JavaScript destruct schema example" highlight={6,9} theme={null} /** @type {Enterspeed.FullSchema} */ export default { triggers: function(context) { context.triggers('cms', ['page']); }, routes: function({url}, context) { context.url(url); }, properties: function ({properties: p}, context) { return { title: p.title, blocks: context.partial('blocks', p.blocks), aboutUsPage: context.reference('page').byOriginId(p.aboutUsPage.id) }; } } ``` ## Arrow function expression You can use arrow function expression to simplify or make your schema even more compact: ```js title="JavaScript arrow function expression schema example" theme={null} /** @type {Enterspeed.FullSchema} */ export default { triggers: (context) => context.triggers('cms', ['page']), routes: ({url}, context) => context.url(url), properties: ({properties: p}, context) =>({ title: p.title, blocks: context.partial('blocks', p.blocks), aboutUsPage: context.reference('page').byOriginId(p.aboutUsPage.id) }) } ``` ## Console object When debugging JavaScript it's often useful to use the [console object](https://developer.mozilla.org/en-US/docs/Web/API/console) to print out values to the console. When using the Test schema feature in the Enterspeed Management App, the following methods are supported: * debug * error * info * log * trace * warn ```js title="JavaScript schema with conole usage" highlight={10} theme={null} /** @type {Enterspeed.FullSchema} */ export default { triggers: function(context) { context.triggers('cms', ['page']); }, routes: function({url}, context) { context.url(url); }, properties: function ({properties: p}, context) { console.log('block type', p.block.type) return { title: p.title, block: context.partial(`block-${p.block.type}`, p.block), aboutUsPage: context.reference('page').byOriginId(p.aboutUsPage.id) }; } } ``` If you use any of the other native console methods, the schema is still working but the methods will just not output anything. ## Limitations Now, we said that you have all the power of the JavaScript language available for you in your JavaScript schemas, but we have added some limitations because of security. This means that the following areas has been restricted. * No access to the filesystem * No network traffic * Maximum processing time of 60 sec pr schema # JSDoc Source: https://docs.enterspeed.com/enterspeed/reference/js/jsdoc By default, JavaScript schemas are created with a `@type` expression, describing the type of schema and providing IntelliSense in the editor using [JSDoc](https://jsdoc.app/). ```js title="@type expression" theme={null} /** @type {Enterspeed.FullSchema} */ ``` With the `@type` expression, you get IntelliSense on all the functions in your schema, the parameters (sourceEntity and context objects), and the return types. Schema IntelliSense However, since Enterspeed is so flexible and you can ingest all types of data, Enterspeed doesn't know about the custom properties you have ingested in the source entities and therefore can't provide IntelliSense on these properties by default. The schema types are publicly available from our NPM package: [@enterspeed/js-schema-types](https://www.npmjs.com/package/@enterspeed/js-schema-types). ## Creating your own type definitions All schema types also come in a generic version where you can provide the type of your source entity described in JSDoc. Hereby enabling IntelliSense for all your custom properties of a source entity. First, you need to create a [type definition](https://jsdoc.app/tags-typedef) describing your source entity. ```js title="@type definition" theme={null} /** @typedef {object} ContentPage * @property {string} title * @property {string} content * @property {object} metaData * @property {boolean} metaData.isPublished * @property {number} metaData.sortOrder */ ``` If you prefer not to create your type definitions manually, there are many free only tools available that can generate them for you. Simply paste the JSON from your source entity into your chosen tool. [transform.tools](https://transform.tools/json-to-jsdoc) is a great example. ## Apply a custom type definition In order to use your custom type definition you simply use the generic version of the schema type you are working with (FullSchema, PartialSchema, ...). ```js title="Generic @type expression" theme={null} /** @type {Enterspeed.FullSchema} */ ``` You now have IntelliSense on your custom source entity properties. Custom schema IntelliSense ## Lookups and custom functions The generic version of the schema type only describes the source entity passed into the schema. But if you do lookups in your schema, you can also use JSDoc to describe the source entities you fetch with lookup. ```js title="Lookup call with defined return type" theme={null} const products = /** @type {Enterspeed.ISourceEntity[]} */ (await context.lookup("type eq 'product'").toPromise()); ``` For custom functions you can also describe the parameters and return type. ```js title="Function with described parameters" theme={null} /** * @param {Enterspeed.ISourceEntity} sourceEntity * @returns {boolean} */ function isProductAvailable(sourceEntity) { return sourceEntity.properties.status == 'available' && sourceEntity.properties.stockCount > 0; } ``` # Intro Source: https://docs.enterspeed.com/enterspeed/reference/js/partial-schema/intro A partial schema is a reusable component that can be used across multiple schemas, e.g. if you have repetitive logic or if you just want to seperate out some of the schema logic into smaller components. A partial schema does not generete views on it's own, instead the output is embedded in the schema wehere the partial schema i referenced. ```js title="JavaScript partial schema example" theme={null} /** @type {Enterspeed.PartialSchema} */ export default { properties: function (input, context) { return { seoTitle: input.seoTitle, seoDescription: input.seoDescription } } } ``` # Properties Source: https://docs.enterspeed.com/enterspeed/reference/js/partial-schema/properties The `properties` method is where you define the output that is embedded where the partial schema is referenced. ```js title="Properties example" theme={null} properties: function (input, context) { return { title: input.properties.title, seo: { title: input.properties.seoTitle, description: input.properties.seoDescription, } categoryIds: input.properties.categoryIds .map(categoryId => parseInt(categoryId)) }; } ``` The properties method must return an object with the data you want to return. # PropertiesContext object The `PropertiesContext` object is passed into the `properties` method and gives you access to a set of methods described below. ## Methods | Method | Description | | ----------------------- | --------------------------------------------------------------------------------------------------------------------------- | | [lookup](#lookup) | Lookup allows you to search source entities using a filter string and work with the source entities directly in the schema. | | [partial](#partial) | Referencing a partial schema. Mapped data from a partial schema is embedded into the calling schema. | | [reference](#reference) | Referencing a full schema. References to other schemas are resolved on delivery request time. | ### lookup Lookup source entities. Lookup allows you to search source entities using a filter string and work with the data from the source entities directly in the schema. Note the `lookup` is an async function so you have to use [async/await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await) when you are using the `lookup` function. `lookup(filter)` The `lookup` function is limited to a maximum number of items to return, based on the tenant plan. It can however be increased per tenant basis. Please reach out to Enterspeed if needed. The default limits are: * Free & Premium plans: 100 items * Enterprise plan: 500 items #### Parameters | Parameter | Type | Description | | --------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `filter` | string | A filtering criteria. [See list of filter examples](/enterspeed/reference/filter-expressions) | | `lookupOptions` | object | An optional object with the following structure:
`{ excludeProperties?: boolean; }`

If `excludeProperties` is set to `true` the `lookup` will only return the base fields of the source entities and not all custom properties. This will help improve performance if you don't need the custom properties, but only some of the base fields like `originId`, `originParentId`, `url` | #### Required function calls After the `lookup` function it's required to call `toPromise` to excecute the query and return a [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). Using the toPromise function excecutes the query and return a promise you must resolve. ```js title="look toPromise" theme={null} const newsArticles: await context .lookup("type eq 'newsArticle'") .toPromise() ``` #### Optional function calls To filter the source entities even further you can call some of the following optional functions. The `limit` function limits the number of source entities. #### Parameters | Parameter | Type | Description | | --------- | ------ | ------------------------------------------------ | | `limit` | number | The maximum number of source entities to return. | ```js title="look and limit" theme={null} const newsArticles: await context .lookup("type eq 'newsArticle'") .limit(5) .toPromise() ``` The order sequence of the source entities. #### Parameters | Parameter | Type | Description | | --------- | ----------------------------------------------------- | ------------------------------------------------- | | `orderBy` | \{ propertyName: string, direction: "asc" \| "desc" } | Allows you to specify your desired sorting order. | ```js title="lookup and orderBy" theme={null} const newsArticles: await context .lookup("type eq 'newsArticle'") .orderBy({ propertyName: "properties.createdDate", direction: "desc"}) .toPromise() ``` The sourceGroup function lets you specify the source group. By default the source group of the current source entity is used. #### Parameters | Parameter | Type | Description | | ------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `sourceGroup` | string | Allows you to define a different source group. The sourceGroupAlias should be equal to the desired source group alias where you want to look for source entities. | If not defined, it uses the current source group. ```js title="lookup and sourceGroup" theme={null} const newsArticles: await context .lookup("type eq 'newsArticle'") .sourceGroup("anotherSourceGroup") .toPromise() ``` #### Examples ```js title="lookup with async/await" theme={null} properties: async function (input, context) { const latestNews = await context .lookup("type eq 'newsArticle'") .orderBy({ propertyName: "properties.createdDate", direction: "desc"}) .limit(3) .toPromise(); const mappedNews = latestNews .map((news) => ({ url: news.url, title: news.properties.title, teaser: news.properties.teaser })); return { latestNews: mappedNews } } ``` ```js title="lookup without custom properties" theme={null} properties: async function (input, context) { const baseNewsArticles = await context .lookup("type eq 'newsArticle'", { excludeProperties: true }) .limit(3) .toPromise(); const mappedNews = baseNewsArticles .map((news) => ({ originId: news.originId, url: news.url })); return { latestNews: mappedNews } } ``` *** ### partial Referencing a partial schema. The partial mapping property type allows for dynamically including partial schemas into the main schema. This is useful when you need to iterate an array of different objects that has an identifier, like an ID, alias or similar. Partial schemas are typically used when you want a reusable schema for mapping data that is part of the same entity. E.g. if metadata (title, description, etc.) is the same across different entity types, but the data lives on the entity itself. Read more about partial schemas [here](/enterspeed/key-concepts/partial-schemas) `partial(schemaAlias, input)` #### Parameters | Parameter | Type | Description | | ------------- | ------ | ------------------------------------------------------------ | | `schemaAlias` | string | The alias of a partial schema. | | `input` | object | You can pass whatever data you need for your partial schema. | #### Examples ```js title="partial" theme={null} contentBlocks: input.properties.contentBlocks.map((contentBlock) => context.partial(`block-${contentBlock.contentType}`, contentBlock) ) ``` ```js title="passing extra properties to the partial schema" theme={null} contentBlocks: input.properties.contentBlocks.map((contentBlock) => context.partial(`block-${contentBlock.contentType}`, { block: contentBlock, pageOriginId: input.originId }) ) ``` The `input` defines what goes into the partial schema and can be any custom object, and the `schemaAlias` is used to resolve what partial schema to use. So, in this case, we could have a partial schema with an alias: block-headline. *** ### reference The `reference` are used to reference other views created from either this source entity or another source entity. When referenced, Enterspeed will resolve the view when requested by the Delivery API so that the data will stay up-to-date if a reference view is updated. The `reference` function is the starting point where you can use our fluent API to configure the reference(s) you want (by filter, by originId, take only top 5, and so on). Reference schemas are typically used when you are mapping data from another entity. E.g. a page has a reference to another page entity or media entity. Read more about reference schemas [here](/enterspeed/key-concepts/referencing-schemas) `reference(schemaAlias)` #### Parameters | Parameter | Type | Description | | ------------- | ------ | ---------------------- | | `schemaAlias` | string | The alias of a schema. | #### Required function calls After the `reference` function it's required to call one of the following functions to define what source entities you want references to. Using the byOriginId function lets you create a reference to a source entity by its oringinId. #### Parameters | Parameter | Type | Description | | ---------- | ------ | --------------------------------------- | | `originId` | string | The original id from the source system. | ```js title="reference by byOriginId" theme={null} contentTeaser: context .reference("contentTeaser") .byOriginId(input.properties.link.id) ``` Using the byOriginIds function lets you create a references to a list of source entity by their oringinId. #### Parameters | Parameter | Type | Description | | ----------- | --------- | ---------------------------------------------- | | `originIds` | string\[] | A list of original ids from the source system. | ```js title="reference by byOriginIds" theme={null} contentTeaser: context .reference("contentTeaser") .byOriginIds(input.properties.links.map(link => link.id)) ``` The children function creates a reference to all the children of the current source entity. It's basicly a shortcut for `.filter("originParentId eq '{input.properties.originId}'")`. ```js title="reference by children" theme={null} childPages: context.reference("page").children() ``` ```js title="reference by children and type" theme={null} childPages: context.reference("page").children("type eq 'subpage'") ``` The `children` function is limited to a maximum number of items to return, based on the tenant plan. It can however be increased per tenant basis. Please reach out to Enterspeed if needed. The default limits are: * Free & Premium plans: 100 items * Enterprise plan: 500 items Using the filter function lets you do a dynamic search for source entities you want to make references to. #### Parameters | Parameter | Type | Description | | --------- | ------ | --------------------------------------------------------------------------------------------- | | `filter` | string | A filtering criteria. [See list of filter examples](/enterspeed/reference/filter-expressions) | ```js title="reference by filter" theme={null} newsTeasers: context .reference("newsTeaser") .filter("type eq 'newsArticle'") ``` The `filter` function is limited to a maximum number of items to return, based on the tenant plan. It can however be increased per tenant basis. Please reach out to Enterspeed if needed. The default limits are: * Free & Premium plans: 100 items * Enterprise plan: 500 items The parent function creates a reference to the parent of the current source entity. It's basicly a shortcut for `.filter("originId eq '{input.properties.originParentId}'")`. ```js title="reference by parent" theme={null} parentPage: context .reference("page") .parent() ``` The self method creates a reference to a view created by the specified schema on the current source entity. The helper method is equivalent to calling `.byOriginId(input.originId)`. ```js title="reference by self" theme={null} selfPage: context .reference("page") .self() ``` #### Optional function calls To filter the source entities you are making references to even further you can call some of the following optional functions. The `limit` function limits the number of references. #### Parameters | Parameter | Type | Description | | --------- | ------ | ------------------------------------------- | | `limit` | number | The maximum number of references to return. | ```js title="reference by filter and limit" theme={null} topFiveNewsTeasers: context .reference("newsTeaser") .filter("type eq 'newsArticle'") .limit(5) ``` The order sequence of references. #### Parameters | Parameter | Type | Description | | --------- | ----------------------------------------------------- | ------------------------------------------------- | | `orderBy` | \{ propertyName: string, direction: "asc" \| "desc" } | Allows you to specify your desired sorting order. | ```js title="reference by filter and orderBy" theme={null} newsArticles: context .reference("newsArticle") .filter("type eq 'newsArticle'") .orderBy({ propertyName: "properties.createdDate", direction: "desc"}) ``` The `sourceGroup` function lets you specify the source group. By default the source group of the current source entity is used. #### Parameters | Parameter | Type | Description | | ------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `sourceGroup` | string | Allows you to define a different source group. The sourceGroupAlias should be equal to the desired source group alias where you want to look for source entities. | If not defined, it uses the current source group. ```js title="reference by filter and sourceGroup" theme={null} newsTeasers: context .reference("newsTeaser") .filter("type eq 'newsArticle'") .sourceGroup("anotherSourceGroup") ``` The `first` function return the first item in the result set. The item i returned as an object instead of an array with one item. The function must always be called as the last call in the chain. ```js title="reference to the first news teaser" theme={null} newsTeaser: context .reference("newsTeaser") .filter("type eq 'newsArticle'") .orderBy({ propertyName: "properties.createdDate", direction: "desc"}) .first() ``` #### Examples ```js title="Property type: reference (static value) with originId" theme={null} seoData: context.reference("seo").byOriginId(input.originId) ``` ```js title="reference by filter and sourceGroup" theme={null} products: context .reference("product") .filter("type eq 'product'") .orderBy({ propertyName: name, direction: "asc"}) .limit(10) .sourceGroup("commerce") ``` The `schemaAlias` can either be a static value, like "seo", or it can be a dynamic value being resolved from the Source Entity that is being processed by Enterspeed. This allows for supporting almost any use case. #### Reference response The response of references differs in V1 and V2+ of the delivery API. V1 return the id, type and wraps the properties from the referenced view in a `view` property. The response of references in V2+ is much cleaner and only returns the actual properties from the referenced schema. If a reference is not found in V2+, the reference information is added in the `missingViewReference` property in the `meta` object for debugging purpose. ```json title="Delivery API V1 uses a nested view property" theme={null} { // This example shows a response of a view with two schema references. // image1 is referencing a found view with a url and name property // and image2 is referencing a view that doesn't exist. "meta": { "missingViewReferences": [] }, "route": { "image1": { "id": "gid://Environment/8ef2bdc0-c352-4190-a344-c51d1f5e72ea/Source/dc5d9518-b96a-428a-a9b3-31fb601376c2/Entity/1234/View/image", "view": { "url": "https://test.com/how-to-write-a-good-blog-post.png", "name": "How To Write A Good Blog Post" }, "type": "ViewReference" }, "image2": { "id": "gid://Environment/8ef2bdc0-c352-4190-a344-c51d1f5e72ea/Source/dc5d9518-b96a-428a-a9b3-31fb601376c2/Entity/1235/View/image", "view": null, "type": "ViewReference" } } } ``` ```json title="Delivery API V2+ only returns the actual view properties" theme={null} { // This example shows a response of a view with two schema references. // image1 is referencing a found view with a url and name property // and image2 is referencing a view that doesn't exist. "meta": { "missingViewReferences": [ { "path": "image2", "viewId": "gid://Environment/8ef2bdc0-c352-4190-a344-c51d1f5e72ea/Source/dc5d9518-b96a-428a-a9b3-31fb601376c2/Entity/1235/View/image" } ] }, "route": { "image1": { "url": "https://test.com/how-to-write-a-good-blog-post.png", "name": "How To Write A Good Blog Post" }, "image2": null } } ``` # Expression Source: https://docs.enterspeed.com/enterspeed/reference/json/expression Expressions will often be used as simple placeholders to map data from the **source entity**. Most values in schemas and partial schemas can be expressed. An expression is identified by using curly brackets `{}`: ```json theme={null} "headline": "{p.title}" ``` A value that support expression can have multiple expressions: ```json theme={null} "headline": "{p.title}: {p.subTitle}" ``` In combination with regular text: ```json theme={null} "headline": "Blog post: {p.title}" ``` ## Null check Trying to access properties of a none existing object will cause the view generation to fail. If that's not intended add a null check using `?`. In the following example `headline` will be set to the value of `p.meta.description`. If `p.meta` is null (or doesn't exist) `headline`. ```json theme={null} "description": "{p.meta?.description}" ``` ### Null coalescing ```json theme={null} "headline": "{p.title ?? p.header}" ``` Combined with null check: ```json theme={null} "headline": "{p.meta?.description ?? p.description}" ``` The expression can also be grouped using parentheses: ```json theme={null} "x": "{(p.a ?? p.b) ?? p.c}" ``` ## Accessing properties Accessing properties can be done by typing `p.` followed by the name of the property. ```json title="Property with default type (string)" theme={null} { "triggers": { "umbraco": ["frontPage"] }, "route": { "url": "{url}" }, "properties": { "headline": "{p.title}" } } ``` The default type of a property is a **string**. If you need another property type, simply change your property to an object and use `type` and `value`. ```json title="Property with type number" theme={null} { "triggers": { "umbraco": ["frontPage"] }, "route": { "url": "{url}" }, "properties": { "stock": { "type": "number", "value": "{p.inventoryQuantity}" } } } ``` # Path selector Source: https://docs.enterspeed.com/enterspeed/reference/json/path-selector The path selector is based on JSON Path. It can be used to select and filter data from the source entity. This is available as input for [array](./property-types#path-input) and for the [dynamic property type](./property-types#dynamic) to map data from the source entity. For dynamic mapping, the selector should result in a property value (single token). When using `$path` for the array input the result is expected to be a list of property values (multiple tokens). Mapping all source entity properties: ```json theme={null} "product": { "*": "p" } ``` Consider, that you want the entire list of product features: ```json theme={null} "features": { "*": "p.features" } ``` If you only want the `name` of the features: ```json theme={null} "tabs": { "type": "array", "input": { "$path": "p.features[*]" }, "var": "feature", "items": { "type": "string", "value": "{item.name}" } } ``` If you want all features with its `display` property equals true: ```json theme={null} "tabs": { "type": "array", "input": { "$path": "p.features[?(@.display==true)]" }, "var": "feature", "items": { "type": "string", "value": "{item.name}" } } ``` You can combine dynamic mapping and arrays: ```json theme={null} "tabs": { "type": "array", "input": { "$path": "p.features[?(@.display==true)]" }, "var": "feature", "items": { "type": "object", "feature": { "*": "{feature}" } } } ``` If you want the feature with its `id` property equals to a dynamic value: ```json theme={null} "tabs": { "type": "array", "input": { "$path": "p.features[?(@.id=='{p.primaryFeatureId}')]" }, "var": "feature", "items": { "type": "string", "value": "{item.name}" } } ``` # Property types Source: https://docs.enterspeed.com/enterspeed/reference/json/property-types | Property | Description | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [String](#string) | Basic string mapping.

Accepts the following fields: `type` `value` `default`

Note: `type` and `value` are required if you aren't using the shorthand - e.g. `"title": "{p.headline}"` | | [Number](#number) | Basic number or integer mapping.

Required fields: `type` `value`

Optional fields: `default` `precision` | | [Boolean](#boolean) | Basic boolean mapping.

Required fields: `type` `value`

Optional fields: `default` | | [Array](#array) | Mapping of an array, defining the input to iterate and the items definition.

Required fields: `type` `input` `items`

Optional fields: `var` | | [Object](#object) | Mapping of an object.

Required fields: `type` `properties` | | [Reference](#reference) | Referencing another schema.

Required fields: `type` `alias` - use `id` **OR** `originId`

Optional fields: `source` | | [Partial](#partial) | Referencing a partial schema to map the data into.

Required fields: `type` `input` `alias` | | [Dynamic](#dynamic) | Dynamic mapping using the [path selector](./path-selector).

Required fields: `*` | There is no `Date` property type. But if you ingest your dates as strings in the following format `yyyy-MM-ddTHH:mm:ss`, you can still sort by date and the value can easily be parsed to Date on the client in JavaScript and other languages. ## string Basic string mapping. Since string is the most commonly used property type you can access it without writing the `type` and `value` property. This is meant as syntactic sugar making it easier and quicker to work with. ### Fields | Property | Required? | Description | | --------- | --------- | --------------------------------------------------------------- | | `type` | **Yes** | The property type - here `string` | | `value` | **Yes** | The value of the property | | `default` | No | The default value is used if `value` is null or an empty string | ### Examples ```json title="Property type: string" theme={null} "title": "{p.headline}" ``` ```json title="Property type: string with value and default" theme={null} "title": { "type": "string", "value": "{p.headline}", "default": "Unknown title" } ``` *** ## number Basic number or integer mapping. The size of the number is limited to the size of a [C# double](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/floating-point-numeric-types#characteristics-of-the-floating-point-types). ### Fields | Property | Required? | Description | | ----------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | **Yes** | The property type - here `number` | | `value` | **Yes** | The value of the property | | `default` | No | The default value is used if `value` is null or can be parsed as a number | | `precision` | No | Rounds a decimal value to a specified number of fractional digits, and rounds midpoint values to the nearest even number. Default value is 0 | ### Examples ```json title="Property type: number" theme={null} "stock": { "type": "number", "value": "{p.inventoryQuantity}" } ``` *** ## boolean Basic boolean mapping. ### Fields | Property | Required? | Description | | --------- | --------- | -------------------------------------------------------------------------- | | `type` | **Yes** | The property type - here `boolean` | | `value` | **Yes** | The value of the property | | `default` | No | The default value is used if `value` is null or can be parsed as a boolean | ### Examples ```json title="Property type: boolean" theme={null} "isPublished": { "type": "boolean", "value": "{p.published}", "default": true } ``` *** ## array Mapping of an array, defining the input to iterate and the items definition. Array property type is designed for working with collections. ### Fields | Property | Required? | Description | | -------- | --------- | ------------------------------------------------------------------------------------------------------------------ | | `type` | **Yes** | property type - here `array` | | `input` | **Yes** | States input, where to retrieve items collection to work with from. Support input types: `string` `$exp` `$lookup` | | `items` | No | Used for mapping results. | | `var` | No | Collection iteration variable name. Default value is - `item`. | ### `string` type A simple example showing how to use a `string` type in `items`. ```json title="string simple example" theme={null} "categories": { "type": "array", "input": "{p.categories}", "items": { "type": "string", "value": "{item}" } }, ``` ### `$exp` input With expression input, you can reference the desired property on the source entity that is an array. ```json title="$exp input" theme={null} "tabs": { "type": "array", "input": "{p.tabs}", "var": "tab", "items": { "type": "object", "properties": { "title": "{tab.title}", "content": "{tab.content}" } } } ``` ### `$path` input With path input, you can select the desired items on the source entity using the [path selector](./path-selector). ```json title="$path input" theme={null} "tabs": { "type": "array", "input": { "$path": "p.tabs[*]" }, "var": "tab", "items": { "type": "object", "properties": { "title": "{tab.title}", "content": "{tab.content}" } } } ``` Filter items: ```json title="$path input with filter" theme={null} "tabs": { "type": "array", "input": { "$path": "p.tabs[?(@.display==true)]" }, "var": "tab", "items": { "type": "object", "properties": { "title": "{tab.title}", "content": "{tab.content}" } } } ``` ### `$lookup` input Lookup input comparing to \$exp allows you to define query-like and criteria match source entities lookup conditions. #### `$lookup` with single property value match | Property | Required? | Description | | -------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | operator | **Yes** | The operator for the lookup. Supported operators: `equals` `contains` | | sourceEntityProperty | **Yes** | The property to match the value on | | matchValue | **Yes** | The value to match the sourceEntityProperty | | sourceEntityType | No | Type of source entity to use for lookup. Default value is include all entity types available: `*` | | orderBy | No | Allows you to specify your desired sorting order | | top | No | Allows limiting the size of items collection. Can be a number, a number as a text, or an expression. | | source | No | Allows you to define a different source as the property. The `source` should be equal to the desired source group alias, where you want to look for source entities.

If not defined, it uses the current source group. | ```json title="$lookup with a single property" theme={null} "navigationItems": { "type": "array", "input": { "$lookup": { "operator": "equals", "sourceEntityType": "*", "sourceEntityProperty": "originParentId", "matchValue": "{originId}", "orderBy": { "property": "properties.metaData.sortOrder", "sort": "asc" }, "top": 5 } }, "items": { "type": "reference", "id": "{input.id}", "alias": "NavigationItem" } } ``` #### `$lookup` with filter Particular lookup filter type allows you to be more flexible with your matching criteria. | Property | Required? | Description | | -------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | filter | **Yes** | Your filtering criteria. [See list of filter examples](/enterspeed/reference/filter-expressions) | | orderBy | No | Allows you to specify your desired sorting order | | top | No | Allows limiting the size of items collection. Can be a number, a number as a text, or an expression. | | source | No | Allows you to define a different source as the property. The `source` should be equal to the desired source group alias, where you want to look for source entities.

If not defined, it uses the current source group. | ### Additional Arrays have some additional properties available: * `root` For schemas it will contain the source entity and for partial schemas the input. It is possible to access everything from the source entity like `root.originId` or `root.properties.headline`. All property values in `items` that supports expressions can access `root`. ```json theme={null} "tabs": { "type": "array", "input": "{p.tabs}", "var": "tab", "items": { "type": "object", "properties": { "title": "{root.p.headline}: {tab.title}", "content": "{tab.content}" } } } ``` * `parent` If having multidimensional arrays `parent` can be used to access items of the parent array. For the first array `parent` will be equal to `root`. For the next levels `parent` will be equal to `item` of the parent array. All property values in `items` that supports expressions can use `parent`. ```json theme={null} "tabs": { "type": "array", "input": "{p.tabs}", "var": "tab", "items": { "type": "object", "properties": { "title": "{tab.title}", "content": "{tab.content}", "subTabs": { "type": "array", "input": "{tab.tabs}", "var": "subTab", "items": { "title": "{parent.title}: {subTab.title}", "content": "{subTab.content}", } } } } } ``` *** ## object Mapping of an object. ### Fields | Property | Required? | Description | | ------------ | --------- | --------------------------------- | | `type` | **Yes** | The property type - here `object` | | `properties` | **Yes** | The properties of the object | ### Examples ```json title="Property type: object" theme={null} "item": { "type": "object", "properties": { "title": "{p.title}", "content": "{p.content}" } } ``` *** ## reference Referencing another schema. The reference property type is a bit different than string, number, boolean, etc. This property types allows referencing other views created from either this Source Entity or from another source entity. When referenced Enterspeed will resolve the view when requested by the Delivery API, so that the data will stay up-to-date. In order to reference desired source entity, you can use `alias` of the schema and `id` or `originId` of the source entity and optionally a different source than the current source entity. Reference schemas are typically used when you are mapping data from another entity. Eg. a page has a reference another page entity or media entity. Read more about reference schemas [here](/enterspeed/key-concepts/referencing-schemas) ### Fields | Property | Required? | Description | | ---------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | **Yes** | The property type - here `alias` | | `alias` | **Yes** | The alias of the schema you wish to reference | | `id` | **Yes** / No | The id of the source entity. `originId` can be used instead | | `originId` | **Yes** / No | The originId of the source entity. `id` can be used instead | | `source` | No | Allows you to define a different source. The source should be equal to the desired source group alias, where the view is located.

If not defined, it uses the current source group.

Note: This property is only used if `originId` is used. If `id` is used the source of the id will take priority over the source | ### Examples ```json title="Property type: reference (static value) with originId" theme={null} "seoData": { "type": "reference", "originId": "{originId}", "alias": "Seo", "source": "anotherSourceGroupAlias" } ``` ```json title="Property type: reference (static value)" theme={null} "seoData": { "type": "reference", "id": "{id}", "alias": "Seo" } ``` ```json title="Property type: reference (dynamic value)" theme={null} "seoData": { "type": "reference", "id": "{id}", "alias": "{p.seoAlias}" } ``` The `alias` can either be a static value, like "Seo" or it can be a dynamic value being resolved from the Source Entity that is being processed by Enterspeed. This allows for supporting almost any use case. ### Reference response The response of references differs in V1 and V2+ of the Delivery API. V1 return the id, type and wraps the properties from the referenced view in a `view` property. The response of references in V2+ is much more clean and only return the actual properties from the referenced schema. If a reference is not found in V2+ the reference information are added in the `missingViewReference` property in the `meta` object for debug purpose. ```json title="Delivery API V1 uses a nested view property" theme={null} { // This example shows a response of a view with two schema references. // image1 is referencing a found view with a url and name property // and image2 is referencing a view that doesn't exist. "meta": { "missingViewReferences": [] }, "route": { "image1": { "id": "gid://Environment/8ef2bdc0-c352-4190-a344-c51d1f5e72ea/Source/dc5d9518-b96a-428a-a9b3-31fb601376c2/Entity/1234/View/image", "view": { "url": "https://test.com/how-to-write-a-good-blog-post.png", "name": "How To Write A Good Blog Post" }, "type": "ViewReference" }, "image2": { "id": "gid://Environment/8ef2bdc0-c352-4190-a344-c51d1f5e72ea/Source/dc5d9518-b96a-428a-a9b3-31fb601376c2/Entity/1235/View/image", "view": null, "type": "ViewReference" } } } ``` ```json title="Delivery API V2+ only returns the actual view properties" theme={null} { // This example shows a response of a view with two schema references. // image1 is referencing a found view with a url and name property // and image2 is referencing a view that doesn't exist. "meta": { "missingViewReferences": [ { "path": "image2", "viewId": "gid://Environment/8ef2bdc0-c352-4190-a344-c51d1f5e72ea/Source/dc5d9518-b96a-428a-a9b3-31fb601376c2/Entity/1235/View/image" } ] }, "route": { "image1": { "url": "https://test.com/how-to-write-a-good-blog-post.png", "name": "How To Write A Good Blog Post" }, "image2": null } } ``` *** ## partial Referencing a partial schema to map the data into. The partial mapping property type allows for dynamically including partial schemas into the main schema. This is useful when you need to iterate an array of different objects that has an identifier, like an ID, alias or similar. Partial schemas are typically used when you want a reusable schema for mapping data that is part of the same entity. Eg. meta data (title, description, ...) is the same across different entity types but the data lives on the entity it self. Read more about partial schemas [here](/enterspeed/key-concepts/partial-schemas) ### Fields | Property | Required? | Description | | -------- | --------- | --------------------------------------------- | | `type` | **Yes** | The property type - here `partial` | | `input` | **Yes** | Defines what goes into the partial schema | | `alias` | **Yes** | Is used to resolve what partial schema to use | ### Examples ```json title="Property type: partial" theme={null} "blocks": { "type": "array", "input": "{p.contentBlocks}", "items": { "type": "partial", "input": "{item}", "alias": "Block-{item.contentType}" } } ``` The `input` defines what goes into the partial schema and the `alias` is used to resolve what partial schema to use. So in this case we could have partial schema with alias: Block-headline. If you want to pass the entire current source entity to input, you can use `{root}`. *** ## dynamic ### Fields | Property | Required? | Description | | -------- | --------- | -------------------------------- | | `*` | **Yes** | [Path selector](./path-selector) | ### Examples ```json title="Property type: dynamic" theme={null} "blocks": { "*": "p.contentBlocks" } ``` # Overview Source: https://docs.enterspeed.com/enterspeed/reference/schema-example-library/overview Below you'll find a collection of useful schema snippets. Use these as a starting point or inspiration when designing your next schema. These snippets are meant as examples and are meant to be modified to fit your own data structure. Breadcrumb navigation schema example using references. Combining data from multiple source entities into a single view and returning it from the Query API Recommended ways of mapping dictionary items. Different ways of doing dynamic mapping of properties from the source entity. Schema example for listing the 3 latest news articles filtered by category. Partial schema example for a reuseable SEO composition across different page types. Example of a site settings schema with various configuration options and references to other pages. Examples of how to build a sitemap.xml or llms.txt file. # Service Limits Source: https://docs.enterspeed.com/enterspeed/service-limits Enterspeed sets some upper limits to its services to ensure stability and performance for all customers, please note that these limits may be capped before reaching the maximum by your plan. Limits that are marked with \* may be extended. **Service limits:** * Maximum monthly Delivery API and Query API requests (counted together in a single pooled quota)\*: 5,000,000 per tenant * Maximum monthly changed Ingest Requests\*: 500,000 per tenant * Maximum number of source entities\*: 200,000 per tenant * Maximum number of index items\*: 200,000 per tenant * Maximum index item size: 1 MB * Maximum storage\*: 2 GB per tenant * Ingest API rate limit\*: 25 requests/sec per tenant * Ingest API request size limit: 1 MB per request * Source entity property count limit: 5,000 per source entity # Continuous Deployment Source: https://docs.enterspeed.com/enterspeed/tooling/cli/continuous-deployment