@dotcms/client
Version:
Official JavaScript library for interacting with DotCMS REST APIs.
1,424 lines (1,401 loc) • 96.2 kB
JavaScript
'use strict';
var consola = require('consola');
var types = require('@dotcms/types');
var internal = require('./internal.cjs.js');
/**
* HTTP client implementation using the Fetch API.
*
* Extends BaseHttpClient to provide a standard interface for making HTTP requests.
* This implementation uses the native Fetch API and handles:
* - JSON and non-JSON response parsing
* - HTTP error response parsing and conversion to DotHttpError
* - Network error handling and wrapping
* - Content-Type detection for proper response handling
*
* @example
* ```typescript
* const client = new FetchHttpClient();
*
* // JSON request
* const data = await client.request<MyType>('/api/data', {
* method: 'GET',
* headers: { 'Authorization': 'Bearer token' }
* });
*
* // Non-JSON request (e.g., file download)
* const response = await client.request<Response>('/api/file.pdf', {
* method: 'GET'
* });
* ```
*/
class FetchHttpClient extends types.BaseHttpClient {
/**
* Sends an HTTP request using the Fetch API.
*
* Implements the abstract request method from BaseHttpClient using the native Fetch API.
* Automatically handles response parsing based on Content-Type headers and converts
* HTTP errors to standardized DotHttpError instances.
*
* @template T - The expected response type. For JSON responses, T should be the parsed object type.
* For non-JSON responses, T should be Response or the expected response type.
* @param url - The URL to send the request to.
* @param options - Optional fetch options including method, headers, body, etc.
* @returns Promise that resolves with the parsed response data or the Response object for non-JSON.
* @throws {DotHttpError} - Throws DotHttpError for HTTP errors (4xx/5xx status codes).
* @throws {DotHttpError} - Throws DotHttpError for network errors (connection issues, timeouts).
*
* @example
* ```typescript
* // JSON API request
* const user = await client.request<User>('/api/users/123', {
* method: 'GET',
* headers: { 'Accept': 'application/json' }
* });
*
* // POST request with JSON body
* const result = await client.request<CreateResult>('/api/users', {
* method: 'POST',
* headers: { 'Content-Type': 'application/json' },
* body: JSON.stringify({ name: 'John', email: 'john@example.com' })
* });
*
* // File download (non-JSON response)
* const response = await client.request<Response>('/api/files/document.pdf', {
* method: 'GET'
* });
* ```
*/
async request(url, options) {
try {
// Use native fetch API - no additional configuration needed
const response = await fetch(url, options);
if (!response.ok) {
// Parse response body for error context
let errorBody;
try {
const contentType = response.headers.get('content-type');
if (contentType?.includes('application/json')) {
errorBody = await response.json();
}
else {
errorBody = await response.text();
}
}
catch {
errorBody = response.statusText;
}
// Convert headers to plain object
const headers = {};
response.headers.forEach((value, key) => {
headers[key] = value;
});
throw this.createHttpError(response.status, response.statusText, headers, errorBody);
}
// Handle different response types
const contentType = response.headers.get('content-type');
if (contentType?.includes('application/json')) {
return response.json();
}
// For non-JSON responses, return the response object
// Sub-clients can handle specific response types as needed
return response;
}
catch (error) {
// Handle network errors (fetch throws TypeError for network issues)
if (error instanceof TypeError) {
throw this.createNetworkError(error);
}
throw error;
}
}
}
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
function __classPrivateFieldGet(receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
}
function __classPrivateFieldSet(receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
/**
* Utility functions for AI search parameter mapping and processing
*/
/**
* Appends mapped parameters to URLSearchParams based on a mapping configuration.
* Only includes parameters that have defined values.
*
* @param searchParams - The URLSearchParams object to append to
* @param sourceObject - The source object containing values
* @param mapping - Array of [targetKey, sourceKey] pairs for parameter mapping, or [key] to use same key
* @example
* ```typescript
* const params = new URLSearchParams();
* const query = { limit: 10, offset: 0, siteId: 'default', indexName: 'content' };
* const mapping = [
* ['searchLimit', 'limit'], // Maps limit -> searchLimit
* ['searchOffset', 'offset'], // Maps offset -> searchOffset
* ['site', 'siteId'], // Maps siteId -> site
* ['indexName'] // Uses indexName -> indexName (same key)
* ];
* appendMappedParams(params, query, mapping);
* // Results in: searchLimit=10&searchOffset=0&site=default&indexName=content
* ```
*/
function appendMappedParams(searchParams, sourceObject, mapping) {
mapping.forEach((item) => {
const targetKey = item[0];
const sourceKey = item[1] ?? item[0];
const value = sourceObject[sourceKey];
if (value !== undefined) {
searchParams.append(targetKey, String(value));
}
});
}
var _BaseApiClient_requestOptions, _BaseApiClient_config, _BaseApiClient_httpClient;
/**
* Base class for all DotCMS API clients.
* Provides common constructor parameters and properties that all API clients need.
* Uses JavaScript private fields (#) for true runtime privacy.
*/
class BaseApiClient {
/**
* Creates a new API client instance.
*
* @param {DotCMSClientConfig} config - Configuration options for the DotCMS client
* @param {DotRequestOptions} requestOptions - Options for fetch requests including authorization headers
* @param {DotHttpClient} httpClient - HTTP client for making requests
*/
constructor(config, requestOptions = {}, httpClient) {
/**
* Request options including authorization headers.
* @private
*/
_BaseApiClient_requestOptions.set(this, void 0);
/**
* DotCMS client configuration.
* @private
*/
_BaseApiClient_config.set(this, void 0);
/**
* HTTP client for making requests.
* @private
*/
_BaseApiClient_httpClient.set(this, void 0);
__classPrivateFieldSet(this, _BaseApiClient_config, {
siteId: '',
...config
}, "f");
__classPrivateFieldSet(this, _BaseApiClient_requestOptions, {
...requestOptions
}, "f");
__classPrivateFieldSet(this, _BaseApiClient_httpClient, httpClient, "f");
}
/**
* Gets the request options including authorization headers.
* @protected
*/
get requestOptions() {
return __classPrivateFieldGet(this, _BaseApiClient_requestOptions, "f");
}
/**
* Gets the DotCMS client configuration.
* @protected
*/
get config() {
return __classPrivateFieldGet(this, _BaseApiClient_config, "f");
}
/**
* Gets the HTTP client for making requests.
* @protected
*/
get httpClient() {
return __classPrivateFieldGet(this, _BaseApiClient_httpClient, "f");
}
/**
* Gets the DotCMS URL from config.
* @protected
*/
get dotcmsUrl() {
return __classPrivateFieldGet(this, _BaseApiClient_config, "f").dotcmsUrl;
}
/**
* Gets the site ID from config.
* @protected
*/
get siteId() {
return __classPrivateFieldGet(this, _BaseApiClient_config, "f").siteId || '';
}
}
_BaseApiClient_requestOptions = new WeakMap(), _BaseApiClient_config = new WeakMap(), _BaseApiClient_httpClient = new WeakMap();
/**
* Default values for AI configuration
*/
const DEFAULT_AI_CONFIG = {
threshold: 0.5,
distanceFunction: types.DISTANCE_FUNCTIONS.cosine,
responseLength: 1024
};
/**
* Default values for search query
*/
const DEFAULT_QUERY = {
limit: 1000,
offset: 0,
indexName: 'default'
};
var _AISearch_params, _AISearch_prompt, _AISearch_indexName;
/**
* Class for executing AI searches.
*
* @template T - The type of the contentlet.
* @param config - The configuration for the client.
* @param requestOptions - The request options for the client.
* @param httpClient - The HTTP client for the client.
* @param params - The parameters for the search.
* @param prompt - The prompt for the search.
*/
class AISearch extends BaseApiClient {
constructor(config, requestOptions, httpClient, prompt, indexName, params = {}) {
super(config, requestOptions, httpClient);
_AISearch_params.set(this, void 0);
_AISearch_prompt.set(this, void 0);
_AISearch_indexName.set(this, void 0);
__classPrivateFieldSet(this, _AISearch_params, params, "f");
__classPrivateFieldSet(this, _AISearch_prompt, prompt, "f");
__classPrivateFieldSet(this, _AISearch_indexName, indexName, "f");
}
/**
* Executes the AI search and returns a promise with the search results.
*
* @param onfulfilled - Callback function to handle the search results.
* @param onrejected - Callback function to handle the search error.
* @returns Promise with the search results or the error.
* @example
* ```typescript
* const results = await client.ai.search('machine learning articles', 'content_index', {
* query: {
* limit: 20,
* contentType: 'BlogPost',
* languageId: '1' // or 1
* },
* config: {
* threshold: 0.7
* }
* });
* ```
* @example
* ```typescript
* client.ai.search('machine learning articles', 'content_index', {
* query: {
* limit: 20,
* contentType: 'BlogPost',
* languageId: '1' // or 1
* },
* config: {
* threshold: 0.7
* }
* }).then((results) => {
* console.log(results);
* }).catch((error) => {
* console.error(error);
* });
* ```
*/
then(onfulfilled, onrejected) {
return this.fetch().then((data) => {
const response = {
...data,
results: data.dotCMSResults
};
if (typeof onfulfilled === 'function') {
const result = onfulfilled(response);
// Ensure we always return a value, fallback to data if callback returns undefined
return result ?? response;
}
return response;
}, (error) => {
// Wrap error in DotCMSContentError
let contentError;
if (error instanceof types.DotHttpError) {
contentError = new types.DotErrorAISearch({
message: `AI Search failed (fetch): ${error.message}`,
httpError: error,
params: __classPrivateFieldGet(this, _AISearch_params, "f"),
prompt: __classPrivateFieldGet(this, _AISearch_prompt, "f"),
indexName: __classPrivateFieldGet(this, _AISearch_indexName, "f")
});
}
else {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
contentError = new types.DotErrorAISearch({
message: `AI Search failed (fetch): ${errorMessage}`,
httpError: undefined,
params: __classPrivateFieldGet(this, _AISearch_params, "f"),
prompt: __classPrivateFieldGet(this, _AISearch_prompt, "f"),
indexName: __classPrivateFieldGet(this, _AISearch_indexName, "f")
});
}
if (typeof onrejected === 'function') {
const result = onrejected(contentError);
// Ensure we always return a value, fallback to original error if callback returns undefined
return result ?? contentError;
}
// Throw the wrapped error to trigger .catch()
throw contentError;
});
}
fetch() {
const searchParams = this.buildSearchParams(__classPrivateFieldGet(this, _AISearch_prompt, "f"), __classPrivateFieldGet(this, _AISearch_params, "f"));
const url = new URL('/api/v1/ai/search', this.dotcmsUrl);
url.search = searchParams.toString();
return this.httpClient.request(url.toString(), {
...this.requestOptions,
headers: {
...this.requestOptions.headers
},
method: 'GET'
});
}
/**
* Builds URLSearchParams from the SDK interface, mapping to backend parameter names.
* Only includes parameters that have values.
*
* @param params - Search parameters with SDK naming
* @returns URLSearchParams with backend parameter names
* @private
*/
buildSearchParams(prompt, params = {}) {
const searchParams = new URLSearchParams();
const { query = {}, config = {} } = params;
const combinedQuery = {
...DEFAULT_QUERY,
siteId: this.siteId,
...query
};
const combinedConfig = {
...DEFAULT_AI_CONFIG,
...config
};
const entriesQueryParameters = [
['searchLimit', 'limit'],
['searchOffset', 'offset'],
['site', 'siteId'],
['language', 'languageId'],
['contentType']
];
// Map SDK query parameters to backend parameter names
appendMappedParams(searchParams, combinedQuery, entriesQueryParameters);
// Map config parameters using the same key names
appendMappedParams(searchParams, combinedConfig, Object.keys(combinedConfig).map((key) => [key]));
// Add search-specific parameters
searchParams.append('indexName', __classPrivateFieldGet(this, _AISearch_indexName, "f"));
searchParams.append('query', prompt);
return searchParams;
}
}
_AISearch_params = new WeakMap(), _AISearch_prompt = new WeakMap(), _AISearch_indexName = new WeakMap();
/**
* Client for interacting with the DotCMS AI API.
* Provides methods to interact with AI features.
* @experimental This client is experimental and may be subject to change.
*/
class AIClient extends BaseApiClient {
/**
* Creates a new AIClient instance.
*
* @param {DotCMSClientConfig} config - Configuration options for the DotCMS client
* @param {DotRequestOptions} requestOptions - Options for fetch requests including authorization headers
* @param {DotHttpClient} httpClient - HTTP client for making requests
* @example
* ```typescript
* const aiClient = new AIClient(
* {
* dotcmsUrl: 'https://demo.dotcms.com',
* authToken: 'your-auth-token',
* siteId: 'demo.dotcms.com'
* },
* {
* headers: {
* Authorization: 'Bearer your-auth-token'
* }
* },
* httpClient
* );
* ```
*/
constructor(config, requestOptions, httpClient) {
super(config, requestOptions, httpClient);
}
/**
* Performs an AI-powered search.
*
* @param params - Search parameters with query and AI configuration
* @returns Promise with search results
* @experimental This method is experimental and may be subject to change.
* @template T - The type of the contentlet.
* @param prompt - The prompt for the search.
* @param indexName - The name of the index you want to search in.
* @param params - Search parameters with query and AI configuration.
* @example
* @example
* ```typescript
* const response = await client.ai.search('machine learning articles', 'content_index', {
* query: {
* limit: 20,
* contentType: 'BlogPost',
* languageId: "1" // or 1
* },
* config: {
* threshold: 0.7
* }
* });
* ```
* @example
* ```typescript
* client.ai.search('machine learning articles', 'content_index', {
* query: {
* limit: 20,
* contentType: 'BlogPost',
* languageId: "1" // or 1
* },
* config: {
* threshold: 0.7,
* distanceFunction: DISTANCE_FUNCTIONS.cosine
* }
* }).then((response) => {
* console.log(response.results);
* }).catch((error) => {
* console.error(error);
* });
*/
search(prompt, indexName, params = {}) {
return new AISearch(this.config, this.requestOptions, this.httpClient, prompt, indexName, params);
}
}
/**
* Default variant identifier used in the application.
*/
/**
* Identifier for the dotCMS System Host.
* Used when building queries that should include content from the System Host.
*/
const SYSTEM_HOST = 'SYSTEM_HOST';
/**
* Fields that should not be formatted when sanitizing the query.
* These fields are essential for maintaining the integrity of the content type.
*/
const CONTENT_TYPE_MAIN_FIELDS = [
'live',
'variant',
'contentType',
'languageId',
'conhost'
];
/**
* URL endpoint for the content API search functionality.
*/
const CONTENT_API_URL = '/api/content/_search';
var _BaseBuilder_page, _BaseBuilder_limit, _BaseBuilder_depth, _BaseBuilder_render, _BaseBuilder_sortBy;
/**
* Abstract base class for content builders that provides common functionality
* for querying content from the DotCMS Content API.
*
* This class extracts shared behavior between different builder implementations,
* including pagination, sorting, rendering, and response formatting.
*
* @export
* @abstract
* @class BaseBuilder
* @template T - The type of the content items (defaults to unknown)
*/
class BaseBuilder extends BaseApiClient {
/**
* Creates an instance of BaseBuilder.
*
* @param {object} params - Constructor parameters
* @param {DotRequestOptions} params.requestOptions - Options for the client request
* @param {DotCMSClientConfig} params.config - The client configuration
* @param {DotHttpClient} params.httpClient - HTTP client for making requests
* @memberof BaseBuilder
*/
constructor(params) {
super(params.config, params.requestOptions, params.httpClient);
/**
* Current page number for pagination.
* @private
*/
_BaseBuilder_page.set(this, 1);
/**
* Maximum number of items per page.
* @private
*/
_BaseBuilder_limit.set(this, 10);
/**
* Depth of related content to include in results.
* Valid values: 0-3
* @private
*/
_BaseBuilder_depth.set(this, 0);
/**
* Whether to server-side render widgets in content.
* @private
*/
_BaseBuilder_render.set(this, false);
/**
* Sorting configuration for results.
* @private
*/
_BaseBuilder_sortBy.set(this, void 0);
}
/**
* Returns the offset for pagination based on current page and limit.
*
* @readonly
* @protected
* @memberof BaseBuilder
*/
get offset() {
return __classPrivateFieldGet(this, _BaseBuilder_limit, "f") * (__classPrivateFieldGet(this, _BaseBuilder_page, "f") - 1);
}
/**
* Returns the sort query in the format: field order, field order, ...
*
* @readonly
* @protected
* @memberof BaseBuilder
*/
get sort() {
return __classPrivateFieldGet(this, _BaseBuilder_sortBy, "f")?.map((sort) => `${sort.field} ${sort.order}`).join(',');
}
/**
* Returns the full URL for the content API.
*
* @readonly
* @protected
* @memberof BaseBuilder
*/
get url() {
return `${this.config.dotcmsUrl}${CONTENT_API_URL}`;
}
/**
* Sets the maximum amount of content to fetch.
*
* @example
* ```typescript
* builder.limit(20);
* ```
*
* @param {number} limit - The maximum amount of content to fetch
* @return {this} The builder instance for method chaining
* @memberof BaseBuilder
*/
limit(limit) {
__classPrivateFieldSet(this, _BaseBuilder_limit, limit, "f");
return this;
}
/**
* Sets the page number to fetch.
*
* @example
* ```typescript
* builder.page(2);
* ```
*
* @param {number} page - The page number to fetch
* @return {this} The builder instance for method chaining
* @memberof BaseBuilder
*/
page(page) {
__classPrivateFieldSet(this, _BaseBuilder_page, page, "f");
return this;
}
/**
* Sorts the content by the specified fields and orders.
*
* @example
* ```typescript
* const sortBy = [
* { field: 'title', order: 'asc' },
* { field: 'modDate', order: 'desc' }
* ];
* builder.sortBy(sortBy);
* ```
*
* @param {SortBy[]} sortBy - Array of constraints to sort the content by
* @return {this} The builder instance for method chaining
* @memberof BaseBuilder
*/
sortBy(sortBy) {
__classPrivateFieldSet(this, _BaseBuilder_sortBy, sortBy, "f");
return this;
}
/**
* Setting this to true will server-side render (using velocity) any widgets
* that are returned by the content query.
*
* More information here: {@link https://www.dotcms.com/docs/latest/content-api-retrieval-and-querying#ParamsOptional}
*
* @example
* ```typescript
* builder.render();
* ```
*
* @return {this} The builder instance for method chaining
* @memberof BaseBuilder
*/
render() {
__classPrivateFieldSet(this, _BaseBuilder_render, true, "f");
return this;
}
/**
* Sets the depth of the relationships of the content.
* Specifies the depth of related content to return in the results.
*
* More information here: {@link https://www.dotcms.com/docs/latest/content-api-retrieval-and-querying#ParamsOptional}
*
* @example
* ```typescript
* builder.depth(2);
* ```
*
* @param {number} depth - The depth of the relationships of the content (0-3)
* @return {this} The builder instance for method chaining
* @throws {Error} When depth is not between 0 and 3
* @memberof BaseBuilder
*/
depth(depth) {
if (depth < 0 || depth > 3) {
throw new Error('Depth value must be between 0 and 3');
}
__classPrivateFieldSet(this, _BaseBuilder_depth, depth, "f");
return this;
}
/**
* Executes the fetch and returns a promise that resolves to the content or rejects with an error.
*
* @example
* ```typescript
* builder
* .limit(10)
* .then((response) => console.log(response))
* .catch((error) => console.error(error));
* ```
*
* @example Using async/await
* ```typescript
* const response = await builder.limit(10);
* ```
*
* @param {OnFullfilled} [onfulfilled] - A callback that is called when the fetch is successful
* @param {OnRejected} [onrejected] - A callback that is called when the fetch fails
* @return {Promise<GetCollectionResponse<T> | DotErrorContent>} A promise that resolves to the content or rejects with an error
* @memberof BaseBuilder
*/
then(onfulfilled, onrejected) {
return this.fetch().then((data) => {
const formattedResponse = this.formatResponse(data);
if (typeof onfulfilled === 'function') {
const result = onfulfilled(formattedResponse);
// Ensure we always return a value, fallback to formattedResponse if callback returns undefined
return result ?? formattedResponse;
}
return formattedResponse;
}, (error) => {
// Wrap error in DotErrorContent
const contentError = this.wrapError(error);
if (typeof onrejected === 'function') {
const result = onrejected(contentError);
// Ensure we always return a value, fallback to original error if callback returns undefined
return result ?? contentError;
}
// Throw the wrapped error to trigger .catch()
throw contentError;
});
}
/**
* Formats the response to the desired format.
*
* @protected
* @param {GetCollectionRawResponse<T>} data - The raw response data
* @return {GetCollectionResponse<T>} The formatted response
* @memberof BaseBuilder
*/
formatResponse(data) {
const contentlets = data.entity.jsonObjectView.contentlets;
const total = data.entity.resultsSize;
const mappedResponse = {
contentlets,
total,
page: __classPrivateFieldGet(this, _BaseBuilder_page, "f"),
size: contentlets.length
};
return __classPrivateFieldGet(this, _BaseBuilder_sortBy, "f")
? {
...mappedResponse,
sortedBy: __classPrivateFieldGet(this, _BaseBuilder_sortBy, "f")
}
: mappedResponse;
}
/**
* Calls the content API to fetch the content.
*
* @protected
* @return {Promise<GetCollectionRawResponse<T>>} The fetch response data
* @throws {DotHttpError} When the HTTP request fails
* @memberof BaseBuilder
*/
fetch() {
const finalQuery = this.buildFinalQuery();
const languageId = this.getLanguageId();
return this.httpClient.request(this.url, {
...this.requestOptions,
method: 'POST',
headers: {
...this.requestOptions.headers,
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: finalQuery,
render: __classPrivateFieldGet(this, _BaseBuilder_render, "f"),
sort: this.sort,
limit: __classPrivateFieldGet(this, _BaseBuilder_limit, "f"),
offset: this.offset,
depth: __classPrivateFieldGet(this, _BaseBuilder_depth, "f"),
...(languageId !== undefined ? { languageId } : {})
})
});
}
}
_BaseBuilder_page = new WeakMap(), _BaseBuilder_limit = new WeakMap(), _BaseBuilder_depth = new WeakMap(), _BaseBuilder_render = new WeakMap(), _BaseBuilder_sortBy = new WeakMap();
/**
* @description
* Sanitizes the query for the given content type.
* It replaces the fields that are not content type fields with the correct format.
* Example: +field: -> +contentTypeVar.field:
*
* @example
*
* ```ts
* const query = '+field: value';
* const contentType = 'contentTypeVar';
* const sanitizedQuery = sanitizeQueryForContentType(query, contentType); // Output: '+contentTypeVar.field: value'
* ```
*
* @export
* @param {string} query - The query string to be sanitized.
* @param {string} contentType - The content type to be used for formatting the fields.
* @returns {string} The sanitized query string.
*/
function sanitizeQueryForContentType(query, contentType) {
// Match field names that start with letter/underscore, followed by alphanumeric/underscore/dot
// This excludes Lucene grouping operators like +(...)
return query.replace(/\+([a-zA-Z_][a-zA-Z0-9_.]*):/g, (original, field) => {
return !CONTENT_TYPE_MAIN_FIELDS.includes(field) // Fields that are not content type fields
? `+${contentType}.${field}:` // Should have this format: +contentTypeVar.field:
: original; // Return the field if it is a content type field
});
}
/**
* @description
* Determines whether a site ID constraint should be added to a query based on existing constraints.
*
* The site ID constraint is added only when:
* - Query doesn't already contain a positive site constraint (+conhost)
* - Query doesn't explicitly exclude the specified site ID (-conhost:siteId)
* - Site ID is provided and configured
*
* @example
* ```ts
* const query = '+contentType:Blog +languageId:1';
* const siteId = '123';
* const shouldAdd = shouldAddSiteIdConstraint(query, siteId); // true
* ```
*
* @example
* ```ts
* const query = '+contentType:Blog -conhost:123';
* const siteId = '123';
* const shouldAdd = shouldAddSiteIdConstraint(query, siteId); // false (explicitly excluded)
* ```
*
* @example
* ```ts
* const query = '+contentType:Blog +conhost:456';
* const siteId = '123';
* const shouldAdd = shouldAddSiteIdConstraint(query, siteId); // false (already has constraint)
* ```
*
* @export
* @param {string} query - The Lucene query string to analyze
* @param {string | number | null | undefined} siteId - The site ID to check for
* @returns {boolean} True if site ID constraint should be added, false otherwise
*/
function shouldAddSiteIdConstraint(query, siteId) {
// No site ID configured
if (!siteId) {
return false;
}
// Query already contains a positive site constraint
const hasExistingSiteConstraint = /\+conhost/gi.test(query);
if (hasExistingSiteConstraint) {
return false;
}
// Query explicitly excludes this specific site ID
const hasThisSiteIdExclusion = new RegExp(`-conhost:${siteId}`, 'gi').test(query);
if (hasThisSiteIdExclusion) {
return false;
}
return true;
}
/**
* @description
* Collects all positive `+conhost:` values from a fully assembled Lucene query,
* removes them from their original positions, adds SYSTEM_HOST to the set,
* and rebuilds a single grouped constraint at the end of the query.
*
* This function is designed to be called on the final assembled query string
* (after raw query has been appended) so that conhosts from all sources —
* auto-injected siteId, builder-path conhost, and raw-path conhost — are
* all visible and handled uniformly.
*
* @example
* ```ts
* // Single siteId + SYSTEM_HOST
* buildConhostWithSystemHost('+contentType:Blog +languageId:1 +live:true +conhost:site-123')
* // → '+contentType:Blog +languageId:1 +live:true +(conhost:site-123 conhost:SYSTEM_HOST)'
* ```
*
* @example
* ```ts
* // Multiple conhosts (multisite) + SYSTEM_HOST
* buildConhostWithSystemHost('+contentType:Blog +live:true +conhost:site-a +conhost:site-b')
* // → '+contentType:Blog +live:true +(conhost:site-a conhost:site-b conhost:SYSTEM_HOST)'
* ```
*
* @example
* ```ts
* // No conhost in query (no siteId configured)
* buildConhostWithSystemHost('+contentType:Blog +languageId:1 +live:true')
* // → '+contentType:Blog +languageId:1 +live:true +conhost:SYSTEM_HOST'
* ```
*
* @export
* @param {string} query - The fully assembled Lucene query string to process.
* @returns {string} The query with all positive conhost constraints replaced by a single grouped constraint.
*/
function buildConhostWithSystemHost(query) {
// Collect all unique positive conhost values, excluding SYSTEM_HOST (we'll add it explicitly)
const conhostRegex = /\+conhost:([^\s)]+)/gi;
const values = [];
let match;
while ((match = conhostRegex.exec(query)) !== null) {
const value = match[1];
if (value.toUpperCase() !== SYSTEM_HOST && !values.includes(value)) {
values.push(value);
}
}
// Always include SYSTEM_HOST at the end
values.push(SYSTEM_HOST);
// Remove all existing +conhost: constraints from the query
const queryWithoutConhost = query
.replace(/\s*\+conhost:[^\s)]+/gi, '')
.replace(/\s+/g, ' ')
.trim();
// Build the final constraint:
// - Single value (only SYSTEM_HOST, no other conhost was present): simple form
// - Multiple values: grouped form so dotCMS treats them as OR
const conhostConstraint = values.length === 1
? `+conhost:${values[0]}`
: `+(${values.map((v) => `conhost:${v}`).join(' ')})`;
return `${queryWithoutConhost} ${conhostConstraint}`.trim();
}
var _Field_query;
/**
* The `Field` class is used to build a query with a specific field.
* A Lucene Field is a key used to search for a specific value in a document.
*
* @export
* @class Field
*/
class Field {
/**
* Creates an instance of the `Field` class.
*
* @param {string} query - The initial query string.
*/
constructor(query) {
this.query = query;
_Field_query.set(this, '');
__classPrivateFieldSet(this, _Field_query, this.query, "f");
}
/**
* Appends a term to the query that should be included in the search.
*
* @example
* ```typescript
* const field = new Field("+myField");
* field.equals("myValue");
* ```
*
* @param {string} term - The term that should be included in the search.
* @return {Equals} - An instance of `Equals`.
* @memberof Field
*/
equals(term) {
return buildEquals(__classPrivateFieldGet(this, _Field_query, "f"), term);
}
}
_Field_query = new WeakMap();
var _NotOperand_query;
/**
* 'NotOperand' Is a Typescript class that provides the ability to use the NOT operand in the lucene query string.
*
* @export
* @class NotOperand
*/
class NotOperand {
constructor(query) {
this.query = query;
_NotOperand_query.set(this, '');
__classPrivateFieldSet(this, _NotOperand_query, this.query, "f");
}
/**
* This method appends to the query a term that should be included in the search.
*
* @example
* ```typescript
* const notOperand = new NotOperand("+myField");
* notOperand.equals("myValue");
* ```
*
* @param {string} term - The term that should be included in the search.
* @return {*} {Equals} - An instance of Equals.
* @memberof NotOperand
*/
equals(term) {
return buildEquals(__classPrivateFieldGet(this, _NotOperand_query, "f"), term);
}
}
_NotOperand_query = new WeakMap();
var _Operand_query;
/**
* 'Operand' Is a Typescript class that provides the ability to use operands in the lucene query string.}
* An operand is a logical operator used to join two or more conditions in a query.
*
* @export
* @class Operand
*/
class Operand {
constructor(query) {
this.query = query;
_Operand_query.set(this, '');
__classPrivateFieldSet(this, _Operand_query, this.query, "f");
}
/**
* This method appends to the query a term that should be excluded in the search.
*
* Ex: "-myValue"
*
* @param {string} field - The field that should be excluded in the search.
* @return {*} {Field} - An instance of a Lucene Field. A field is a key used to search for a specific value in a document.
* @memberof Operand
*/
excludeField(field) {
return buildExcludeField(__classPrivateFieldGet(this, _Operand_query, "f"), field);
}
/**
* This method appends to the query a field that should be included in the search.
*
* Ex: "+myField:"
*
* @param {string} field - The field that should be included in the search.
* @return {*} {Field} - An instance of a Lucene Field. A field is a key used to search for a specific value in a document.
* @memberof Operand
*/
field(field) {
return buildField(__classPrivateFieldGet(this, _Operand_query, "f"), field);
}
/**
* This method appends to the query a term that should be included in the search.
*
* Ex: myValue or "My value"
*
* @param {string} term - The term that should be included in the search.
* @return {*} {Equals} - An instance of Equals.
* @memberof Operand
*/
equals(term) {
return buildEquals(__classPrivateFieldGet(this, _Operand_query, "f"), term);
}
}
_Operand_query = new WeakMap();
/**
* Enum for common Operands
*
* @export
* @enum {number}
*/
var OPERAND;
(function (OPERAND) {
OPERAND["OR"] = "OR";
OPERAND["AND"] = "AND";
OPERAND["NOT"] = "NOT";
})(OPERAND || (OPERAND = {}));
/**
* This function removes extra spaces from a string.
*
* @example
* ```ts
* sanitizeQuery(" my query "); // Output: "my query"
* ```
*
* @export
* @param {string} str
* @return {*} {string}
*/
function sanitizeQuery(str) {
return str.replace(/\s{2,}/g, ' ').trim();
}
/**
* This function sanitizes a term by adding quotes if it contains spaces.
* In lucene, a term with spaces should be enclosed in quotes.
*
* @example
* ```ts
* sanitizePhrases(`my term`); // Output: `"my term"`
* sanitizePhrases(`myterm`); // Output: `myterm`
* ```
*
* @export
* @param {string} term
* @return {*} {string}
*/
function sanitizePhrases(term) {
return term.includes(' ') ? `'${term}'` : term;
}
/**
* This function builds a term to be used in a lucene query.
* We need to sanitize the term before adding it to the query.
*
* @example
* ```ts
* const equals = buildEquals("+myField: ", "myValue"); // Current query: "+myField: myValue"
* ```
*
* @export
* @param {string} query
* @param {string} term
* @return {*} {Equals}
*/
function buildEquals(query, term) {
const newQuery = query + sanitizePhrases(term);
return new Equals(newQuery);
}
/**
* This function builds a term to be used in a lucene query.
* We need to sanitize the raw query before adding it to the query.
*
* @example
* ```ts
* const query = "+myField: myValue";
* const field = buildRawEquals(query, "-myField2: myValue2"); // Current query: "+myField: myValue -myField2: myValue"
* ```
*
* @export
* @param {string} query
* @param {string} raw
* @return {*} {Equals}
*/
function buildRawEquals(query, raw) {
const newQuery = query + ` ${raw}`;
return new Equals(sanitizeQuery(newQuery));
}
/**
* This function builds a field to be used in a lucene query.
* We need to format the field before adding it to the query.
*
* @example
* ```ts
* const field = buildField("+myField: ", "myValue"); // Current query: "+myField: myValue"
* ```
*
* @export
* @param {string} query
* @param {string} field
* @return {*} {Field}
*/
function buildField(query, field) {
const newQuery = query + ` +${field}:`;
return new Field(newQuery);
}
/**
* This function builds an exclude field to be used in a lucene query.
* We need to format the field before adding it to the query.
*
* @example
* ```ts
* const query = "+myField: myValue";
* const field = buildExcludeField(query, "myField2"); // Current query: "+myField: myValue -myField2:"
* ```
*
* @export
* @param {string} query
* @param {string} field
* @return {*} {Field}
*/
function buildExcludeField(query, field) {
const newQuery = query + ` -${field}:`;
return new Field(newQuery);
}
/**
* This function builds an operand to be used in a lucene query.
* We need to format the operand before adding it to the query.
*
* @example
* <caption>E.g. Using the AND operand</caption>
* ```ts
* const query = "+myField: myValue";
* const field = buildOperand(query, OPERAND.AND); // Current query: "+myField: myValue AND"
* ```
* @example
* <caption>E.g. Using the OR operand</caption>
* ```ts
* const query = "+myField: myValue";
* const field = buildOperand(query, OPERAND.OR); // Current query: "+myField: myValue OR"
* ```
* @export
* @param {string} query
* @param {OPERAND} operand
* @return {*} {Operand}
*/
function buildOperand(query, operand) {
const newQuery = query + ` ${operand} `;
return new Operand(newQuery);
}
/**
* This function builds a NOT operand to be used in a lucene query.
* We need to format the operand before adding it to the query.
*
* @example
* ```ts
* const query = "+myField: myValue";
* const field = buildNotOperand(query); // Current query: "+myField: myValue NOT"
* ```
*
* @export
* @param {string} query
* @return {*} {NotOperand}
*/
function buildNotOperand(query) {
const newQuery = query + ` ${OPERAND.NOT} `;
return new NotOperand(newQuery);
}
var _Equals_query;
/**
* 'Equal' Is a Typescript class that provides the ability to use terms in the lucene query string.
* A term is a value used to search for a specific value in a document. It can be a word or a phrase.
*
* Ex: myValue or "My Value"
*
* @export
* @class Equal
*/
class Equals {
constructor(query) {
this.query = query;
_Equals_query.set(this, '');
__classPrivateFieldSet(this, _Equals_query, this.query, "f");
}
/**
* This method appends to the query a term that should be excluded in the search.
*
* @example
* ```ts
* const equals = new Equals("+myField: myValue");
* equals.excludeField("myField2").equals("myValue2"); // Current query: "+myField: myValue -myField2: myValue2"
* ```
*
* @param {string} field - The field that should be excluded in the search.
* @return {*} {Field} - An instance of a Lucene Field. A field is a key used to search for a specific value in a document.
* @memberof Equal
*/
excludeField(field) {
return buildExcludeField(__classPrivateFieldGet(this, _Equals_query, "f"), field);
}
/**
* This method appends to the query a field that should be included in the search.
*
* @example
* ```ts
* const equals = new Equals("+myField: myValue");
* equals.field("myField2").equals("myValue2"); // Current query: "+myField: myValue +myField2: myValue2"
*```
* @param {string} field - The field that should be included in the search.
* @return {*} {Field} - An instance of a Lucene Field. A field is a key used to search for a specific value in a document.
* @memberof Equal
*/
field(field) {
return buildField(__classPrivateFieldGet(this, _Equals_query, "f"), field);
}
/**
* This method appends to the query an operand to use logic operators in the query.
*
* @example
* @example
* ```ts
* const equals = new Equals("+myField: myValue");
* equals.or().field("myField2").equals("myValue2"); // Current query: "+myField: myValue OR +myField2: myValue2"
* ```
*
* @return {*} {Operand} - An instance of a Lucene Operand. An operand is a logical operator used to combine terms or phrases in a query.
* @memberof Equal
*/
or() {
return buildOperand(__classPrivateFieldGet(this, _Equals_query, "f"), OPERAND.OR);
}
/**
* This method appends to the query an operand to use logic operators in the query.
*
* @example
* ```ts
* const equals = new Equals("+myField: myValue");
* equals.and().field("myField2").equals("myValue2"); // Current query: "+myField: myValue AND +myField2: myValue2"
* ```
*
* @return {*} {Operand} - An instance of a Lucene Operand. An operand is a logical operator used to combine terms or phrases in a query.
* @memberof Equal
*/
and() {
return buildOperand(__classPrivateFieldGet(this, _Equals_query, "f"), OPERAND.AND);
}
/**
* This method appends to the query an operand to use logic operators in the query.
*
* @example
* ```ts
* const equals = new Equals("+myField: myValue");
* equals.not().field("myField").equals("myValue2"); // Current query: "+myField: myValue NOT +myField: myValue2"
* ```
*
* @return {*} {NotOperand} - An instance of a Lucene Not Operand. A not operand is a logical operator used to exclude terms or phrases in a query.
* @memberof Equal
*/
not() {
return buildNotOperand(__classPrivateFieldGet(this, _Equals_query, "f"));
}
/**
* This method allows to pass a raw query string to the query builder.
* This raw query should end in a Lucene Equal.
* This method is useful when you want to append a complex query or an already written query to the query builder.
*
* @example
* ```ts
* // This builds the follow raw query "+myField: value AND (someOtherValue OR anotherValue)"
* const equals = new Equals("+myField: value");
* equals.raw("+myField2: value2"); // Current query: "+myField: value +myField2: anotherValue"
* ```
*
* @param {string} query - A raw query string.
* @return {*} {Equal} - An instance of a Lucene Equal. A term is a value used to search for a specific value in a document.
* @memberof QueryBuilder
*/
raw(query) {
return buildRawEquals(__classPrivateFieldGet(this, _Equals_query, "f"), query);
}
/**
* This method returns the final query string.
*
* @example
* ```ts
* const equals = new Equals("+myField: myValue");
* equals.field("myField2").equals("myValue2").build(); // Returns "+myField: myValue +myField2: myValue2"
* ```
*
* @return {*} {string} - The final query string.
* @memberof Equal
*/
build() {
return sanitizeQuery(__classPrivateFieldGet(this, _Equals_query, "f"));
}
}
_Equals_query = new WeakMap();
var _QueryBuilder_query;
/**
* 'QueryBuilder' Is a Typescript class that provides the ability to build a query string using the Lucene syntax in a more readable way.
* @example
* ```ts
* const qb = new QueryBuilder();
* const query = qb
* .field('contentType')
* .equals('Blog')
* .field('conhost')
* .equals('my-super-cool-site')
* .build(); // Output: `+contentType:Blog +conhost:my-super-cool-site"`
* ```
*
* @example
* ```ts
* const qb = new QueryBuilder();
* const query = qb
* .field('contentType')
* .equals('Blog')
* .field('title')
* .equals('Football')
* .excludeField('summary')
* .equals('Lionel Messi')
* .build(); // Output: `+contentType:Blog +title:Football -summary:"Lionel Messi"`
* ```
* @export
* @class QueryBuilder
*/
class QueryBuilder {
constructor() {
_QueryBuilder_query.set(this, '');
}
/**
* This method appends to the query a field that should be included in the search.
*
* @example
* ```ts
* const qb = new QueryBuilder();
* qb.field("+myField: ", "myValue"); // Current query: "+myField: myValue"
* ```
*
* @param {string} field - The field that should be included in the search.
* @return {*} {Field} - An instance of a Lucene Field. A field is a key used to search for a specific value in a document.
* @memberof QueryBuilder
*/
field(field) {
return buildField(__classPrivateFieldGet(this, _QueryBuilder_query, "f"), field);
}
/**
* This method appends to the query a field that should be excluded from the search.
*
* @example
* ```ts
* const qb = new QueryBuilder();
* qb.excludeField("myField").equals("myValue"); // Current query: "-myField: myValue"
* ```
*
* @param {string} field - The field that should be excluded from the search.
* @return {*} {Field} - An instance of a Lucene Exclude Field. An exclu