@goatlab/typesense
Version:
Modern TypeScript wrapper for Typesense search engine API
469 lines • 17.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TypesenseApi = exports.createTypedApi = void 0;
// Collections
const createCollection_1 = require("./actions/collections/createCollection");
const getCollection_1 = require("./actions/collections/getCollection");
const updateCollection_1 = require("./actions/collections/updateCollection");
const deleteCollection_1 = require("./actions/collections/deleteCollection");
const listCollections_1 = require("./actions/collections/listCollections");
const getOrCreateCollection_1 = require("./actions/collections/getOrCreateCollection");
// Documents
const insertDocument_1 = require("./actions/documents/insertDocument");
const upsertDocument_1 = require("./actions/documents/upsertDocument");
const updateDocument_1 = require("./actions/documents/updateDocument");
const deleteDocument_1 = require("./actions/documents/deleteDocument");
const getDocumentById_1 = require("./actions/documents/getDocumentById");
const importDocuments_1 = require("./actions/documents/importDocuments");
const exportDocuments_1 = require("./actions/documents/exportDocuments");
const deleteByFilter_1 = require("./actions/documents/deleteByFilter");
const clearCollection_1 = require("./actions/documents/clearCollection");
// Search
const search_1 = require("./actions/search/search");
const multiSearch_1 = require("./actions/search/multiSearch");
// Admin
const health_1 = require("./actions/admin/health");
const metrics_1 = require("./actions/admin/metrics");
const getCollectionStats_1 = require("./actions/admin/getCollectionStats");
// Aliases
const createOrUpdateAlias_1 = require("./actions/aliases/createOrUpdateAlias");
const getAlias_1 = require("./actions/aliases/getAlias");
const listAliases_1 = require("./actions/aliases/listAliases");
const deleteAlias_1 = require("./actions/aliases/deleteAlias");
// Synonyms
const upsertSynonym_1 = require("./actions/synonyms/upsertSynonym");
const getSynonym_1 = require("./actions/synonyms/getSynonym");
const listSynonyms_1 = require("./actions/synonyms/listSynonyms");
const deleteSynonym_1 = require("./actions/synonyms/deleteSynonym");
// Overrides
const upsertOverride_1 = require("./actions/overrides/upsertOverride");
const getOverride_1 = require("./actions/overrides/getOverride");
const listOverrides_1 = require("./actions/overrides/listOverrides");
const deleteOverride_1 = require("./actions/overrides/deleteOverride");
// Presets
const upsertPreset_1 = require("./actions/presets/upsertPreset");
const getPreset_1 = require("./actions/presets/getPreset");
const listPresets_1 = require("./actions/presets/listPresets");
const deletePreset_1 = require("./actions/presets/deletePreset");
const schema_to_types_1 = require("./utils/schema-to-types");
const schema_typed_api_1 = require("./utils/schema-typed-api");
const http_client_1 = require("./components/http-client");
const resilience_policy_1 = require("./components/resilience-policy");
const schema_manager_1 = require("./components/schema-manager");
const tenant_1 = require("./utils/tenant");
/**
* Binds context to API functions by prepending context as first argument
* Preserves generic types through currying
*/
function bindCtx(ctx) {
return (fn) => (...args) => fn(ctx, ...args);
}
/**
* Modern, modular Typesense API client with grouped functionality
*
* @example
* ```typescript
* const api = new TypesenseApi({
* prefixUrl: 'http://localhost:8108',
* token: 'xyz',
* collectionName: 'products'
* })
*
* // Collections
* await api.collections.create({ name: 'products', fields: [...] })
* await api.collections.getOrCreate({ name: 'products', fields: [...] })
*
* // Documents
* await api.documents.insert({ id: '1', title: 'Product 1' })
* await api.documents.search({ q: 'product', query_by: 'title' })
*
* // Admin
* await api.admin.health()
* await api.admin.getMetrics()
* ```
*/
/**
* Factory helper for creating typed TypesenseApi instances
* @example
* ```typescript
* interface Product { id: string; title: string; price: number; }
* const productApi = createTypedApi<Product>()({
* prefixUrl: 'http://localhost:8108',
* token: 'xyz',
* collectionName: 'products'
* })
*
* // Now all document operations are typed
* await productApi.documents.insert({ id: "1", title: "Foo", price: 42 }) // ✅ typed
* ```
*/
const createTypedApi = () => (options) => new TypesenseApi(options);
exports.createTypedApi = createTypedApi;
class TypesenseApi {
ctx;
withCtx;
options;
// Expose components for advanced usage
httpClient;
resilience;
schemaManager;
/**
* Define a strongly-typed collection schema
* @example
* ```typescript
* const ProductCollection = TypesenseApi.defineCollection({
* name: 'products',
* fields: [
* { name: 'id', type: 'string' as const },
* { name: 'title', type: 'string' as const },
* { name: 'price', type: 'float' as const },
* { name: 'inStock', type: 'bool' as const }
* ] as const
* } as const)
* ```
*/
static defineCollection = schema_to_types_1.defineCollection;
/**
* Create a strongly-typed API instance from a collection schema
* @example
* ```typescript
* const ProductCollection = TypesenseApi.defineCollection({...})
*
* const api = TypesenseApi.createSchemaTypedApi(ProductCollection)({
* prefixUrl: 'http://localhost:8108',
* token: 'xyz'
* })
*
* // Now all document operations are fully typed
* await api.documents.insert({
* id: '1',
* title: 'Product',
* price: 99.99,
* inStock: true
* })
* ```
*/
static createSchemaTypedApi = schema_typed_api_1.createSchemaTypedApi;
/**
* Create a typed API from an inline collection definition (convenience method)
* @example
* ```typescript
* const api = TypesenseApi.createFromSchema({
* name: 'products',
* fields: [
* { name: 'id', type: 'string' as const },
* { name: 'title', type: 'string' as const },
* { name: 'price', type: 'float' as const }
* ] as const
* } as const)({
* prefixUrl: 'http://localhost:8108',
* token: 'xyz'
* })
* ```
*/
static createFromSchema(collection) {
return TypesenseApi.createSchemaTypedApi(TypesenseApi.defineCollection(collection));
}
constructor(options) {
this.options = options;
// Initialize resilience policy with observability callbacks
this.resilience = new resilience_policy_1.ResiliencePolicy({
...options.resilience,
onStateChange: options.onCircuitBreakerStateChange,
onRateLimitUpdate: options.onRateLimitUpdate
});
// Create request/response interceptors for circuit breaker
const beforeRequestHooks = [
async (_request) => {
// Check circuit breaker before making request
if (this.resilience.isCircuitOpen()) {
const error = new Error('Circuit breaker is open - service unavailable');
error.isCircuitBreakerError = true;
error.retriesRemaining = 0;
throw error;
}
},
...(options.beforeRequest || [])
];
const afterResponseHooks = [
async (_request, _options, response) => {
// Only record success for successful responses
if (response.ok) {
this.resilience.recordSuccess();
}
// Update rate limit info
this.resilience.updateRateLimit(response.headers);
return response;
},
...(options.afterResponse || [])
];
const beforeErrorHooks = [
(error) => {
// Record failure for circuit breaker before any error transformation
this.resilience.recordFailure();
// Check if circuit is now open after recording failure
if (this.resilience.isCircuitOpen()) {
// Replace the error with circuit breaker error
const circuitError = new Error('Circuit breaker is open');
circuitError.isCircuitBreakerError = true;
throw circuitError;
}
// Re-throw the original error
throw error;
}
];
// Initialize components with interceptors
this.httpClient = new http_client_1.TypesenseHttpClient({
prefixUrl: options.prefixUrl,
token: options.token,
searchTimeout: options.searchTimeout,
importTimeout: options.importTimeout,
defaultTimeout: options.defaultTimeout,
beforeRequest: beforeRequestHooks,
afterResponse: afterResponseHooks,
beforeError: beforeErrorHooks,
kyInstance: options.kyInstance,
enforceTLS: options.enforceTLS
});
this.schemaManager = new schema_manager_1.CollectionSchemaManager({
typesenseVersion: options.typesenseVersion,
cacheSize: options.schemaCacheSize,
cacheTtl: options.schemaCacheTtl,
suppressLogs: options.suppressLogs
});
// Validate and sanitize tenant ID if provided
const sanitizedTenantId = options.tenantId
? (0, tenant_1.sanitizeTenantId)(options.tenantId)
: undefined;
// Create context
this.ctx = {
httpClient: this.httpClient,
resilience: this.resilience,
schemaManager: this.schemaManager,
tenantId: sanitizedTenantId,
collectionName: options.collectionName || 'documents',
typesenseVersion: options.typesenseVersion,
autoCreateCollection: options.autoCreateCollection,
suppressLogs: options.suppressLogs,
fqcn: (baseCollectionName) => {
const base = baseCollectionName || this.ctx.collectionName;
return sanitizedTenantId ? (0, tenant_1.createFQCN)(sanitizedTenantId, base) : base;
}
};
this.withCtx = bindCtx(this.ctx);
// Check version if enabled
if (options.enableVersionCheck) {
this.checkVersion();
}
}
async checkVersion() {
try {
const stats = await this.admin.getStats();
if (stats.server_version) {
this.ctx.typesenseVersion = stats.server_version;
this.schemaManager.setTypesenseVersion(stats.server_version);
if (!this.options.suppressLogs) {
console.info(`Connected to Typesense v${stats.server_version}`);
}
}
}
catch (error) {
// Version check is optional, continue silently
}
}
/**
* Collection management operations
*/
get collections() {
return {
create: this.withCtx(createCollection_1.createCollection),
get: this.withCtx(getCollection_1.getCollection),
update: this.withCtx(updateCollection_1.updateCollection),
delete: this.withCtx(deleteCollection_1.deleteCollection),
list: this.withCtx(listCollections_1.listCollections),
getOrCreate: this.withCtx(getOrCreateCollection_1.getOrCreateCollection)
};
}
/**
* Document CRUD operations
*/
get documents() {
const ctx = this.ctx;
return {
insert: (document, options) => (0, insertDocument_1.insertDocument)(ctx, document, options),
upsert: (document, options) => (0, upsertDocument_1.upsertDocument)(ctx, document, options),
update: (document, options) => (0, updateDocument_1.updateDocument)(ctx, document, options),
delete: this.withCtx(deleteDocument_1.deleteDocument),
getById: this.withCtx(getDocumentById_1.getDocumentById),
import: this.withCtx(importDocuments_1.importDocuments),
export: this.withCtx(exportDocuments_1.exportDocuments),
exportStream: this.withCtx(exportDocuments_1.exportDocumentsStream),
deleteByFilter: this.withCtx(deleteByFilter_1.deleteByFilter),
clear: this.withCtx(clearCollection_1.clearCollection),
// Search operations
search: this.withCtx(search_1.search),
searchText: this.withCtx(search_1.searchText),
searchVector: this.withCtx(search_1.searchVector)
};
}
/**
* Search operations (alias for documents.search*)
*/
get search() {
return {
query: this.withCtx(search_1.search),
text: this.withCtx(search_1.searchText),
vector: this.withCtx(search_1.searchVector),
multi: this.withCtx(multiSearch_1.multiSearch)
};
}
/**
* Admin operations
*/
get admin() {
return {
health: this.withCtx(health_1.health),
waitForHealth: this.withCtx(health_1.waitForHealth),
getMetrics: this.withCtx(metrics_1.getMetrics),
getStats: this.withCtx(metrics_1.getStats),
getCollectionStats: this.withCtx(getCollectionStats_1.getCollectionStats)
};
}
/**
* Alias management (v29+)
*/
get aliases() {
return {
createOrUpdate: this.withCtx(createOrUpdateAlias_1.createOrUpdateAlias),
get: this.withCtx(getAlias_1.getAlias),
list: this.withCtx(listAliases_1.listAliases),
delete: this.withCtx(deleteAlias_1.deleteAlias)
};
}
/**
* Synonym management (v29+)
*/
get synonyms() {
return {
upsert: this.withCtx(upsertSynonym_1.upsertSynonym),
get: this.withCtx(getSynonym_1.getSynonym),
list: this.withCtx(listSynonyms_1.listSynonyms),
delete: this.withCtx(deleteSynonym_1.deleteSynonym)
};
}
/**
* Search override management (v29+)
*/
get overrides() {
return {
upsert: this.withCtx(upsertOverride_1.upsertOverride),
get: this.withCtx(getOverride_1.getOverride),
list: this.withCtx(listOverrides_1.listOverrides),
delete: this.withCtx(deleteOverride_1.deleteOverride)
};
}
/**
* Preset management (v29+)
*/
get presets() {
return {
upsert: this.withCtx(upsertPreset_1.upsertPreset),
get: this.withCtx(getPreset_1.getPreset),
list: this.withCtx(listPresets_1.listPresets),
delete: this.withCtx(deletePreset_1.deletePreset)
};
}
/**
* Get current resilience status
*/
getResilienceStatus() {
return this.resilience.getStatus();
}
/**
* Get current rate limit info
*/
getRateLimit() {
return this.resilience.getRateLimit();
}
/**
* Get cache statistics
*/
getCacheStats() {
return this.schemaManager.getCacheStats();
}
/**
* Get client version
*/
getVersion() {
return this.options.appVersion || '1.0.0';
}
/**
* Get Typesense server version
*/
getTypesenseVersion() {
return this.ctx.typesenseVersion || 'unknown';
}
/**
* Admin helper: List all collections for the current tenant
* Returns only collections that belong to the configured tenant
*/
async listTenantCollections() {
if (!this.ctx.tenantId) {
throw new Error('Tenant ID is required for listing tenant collections');
}
const allCollections = await this.collections.list();
const tenantPrefix = `${this.ctx.tenantId}__`;
return allCollections
.map(c => c.name)
.filter(name => name.startsWith(tenantPrefix));
}
/**
* Admin helper: Get base collection names for the current tenant
* Returns collection names without the tenant prefix
*/
async listTenantBaseCollectionNames() {
const tenantCollections = await this.listTenantCollections();
const tenantPrefix = `${this.ctx.tenantId}__`;
return tenantCollections.map(name => name.substring(tenantPrefix.length));
}
/**
* Admin helper: Delete all collections for the current tenant
* Use with caution - this will permanently delete all tenant data
*/
async deleteAllTenantCollections() {
if (!this.ctx.tenantId) {
throw new Error('Tenant ID is required for deleting tenant collections');
}
const tenantCollections = await this.listTenantCollections();
for (const collectionName of tenantCollections) {
await this.httpClient.request(`/collections/${collectionName}`, {
method: 'DELETE'
});
}
}
/**
* Admin helper: Check if a collection exists for the current tenant
*/
async tenantCollectionExists(baseCollectionName) {
try {
const collectionName = this.ctx.fqcn(baseCollectionName);
await this.collections.get(collectionName);
return true;
}
catch (error) {
if (error?.status === 404 || error?.response?.status === 404) {
return false;
}
throw error;
}
}
/**
* Destroy the client and clean up resources
*/
destroy() {
this.schemaManager.clearCache();
this.resilience.reset();
}
}
exports.TypesenseApi = TypesenseApi;
//# sourceMappingURL=TypesenseApi.js.map