Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion src/Typesense/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ import NLSearchModels from "./NLSearchModels";
import NLSearchModel from "./NLSearchModel";
import SynonymSets from "./SynonymSets";
import SynonymSet from "./SynonymSet";

import CurationSets from "./CurationSets";
import CurationSet from "./CurationSet";
export default class Client {
configuration: Configuration;
apiCall: ApiCall;
Expand Down Expand Up @@ -56,6 +57,8 @@ export default class Client {
private readonly individualNLSearchModels: Record<string, NLSearchModel>;
private readonly _synonymSets: SynonymSets;
private readonly individualSynonymSets: Record<string, SynonymSet>;
private readonly _curationSets: CurationSets;
private readonly individualCurationSets: Record<string, CurationSet>;

constructor(options: ConfigurationOptions) {
options.sendApiKeyAsQueryParam = options.sendApiKeyAsQueryParam ?? false;
Expand Down Expand Up @@ -87,6 +90,8 @@ export default class Client {
this.individualNLSearchModels = {};
this._synonymSets = new SynonymSets(this.apiCall);
this.individualSynonymSets = {};
this._curationSets = new CurationSets(this.apiCall);
this.individualCurationSets = {};
}

collections(): Collections;
Expand Down Expand Up @@ -201,4 +206,20 @@ export default class Client {
return this.individualSynonymSets[synonymSetName];
}
}

curationSets(): CurationSets;
curationSets(name: string): CurationSet;
curationSets(name?: string): CurationSets | CurationSet {
if (name === undefined) {
return this._curationSets;
} else {
if (this.individualCurationSets[name] === undefined) {
this.individualCurationSets[name] = new CurationSet(
name,
this.apiCall,
);
}
return this.individualCurationSets[name];
}
}
}
54 changes: 54 additions & 0 deletions src/Typesense/CurationSet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import ApiCall from "./ApiCall";
import CurationSets, {
CurationSetDeleteResponseSchema,
CurationSetSchema,
CurationSetUpsertSchema,
} from "./CurationSets";
import CurationSetItems from "./CurationSetItems";
import CurationSetItem from "./CurationSetItem";

export default class CurationSet {
private readonly _items: CurationSetItems;
private individualItems: Record<string, CurationSetItem> = {};

constructor(private name: string, private apiCall: ApiCall) {
this._items = new CurationSetItems(this.name, apiCall);
}

async upsert(params: CurationSetUpsertSchema): Promise<CurationSetSchema> {
return this.apiCall.put<CurationSetSchema>(this.endpointPath(), params);
}

async retrieve(): Promise<CurationSetSchema> {
return this.apiCall.get<CurationSetSchema>(this.endpointPath());
}

async delete(): Promise<CurationSetDeleteResponseSchema> {
return this.apiCall.delete<CurationSetDeleteResponseSchema>(
this.endpointPath(),
);
}

items(): CurationSetItems;
items(itemId: string): CurationSetItem;
items(itemId?: string): CurationSetItems | CurationSetItem {
if (itemId === undefined) {
return this._items;
} else {
if (this.individualItems[itemId] === undefined) {
this.individualItems[itemId] = new CurationSetItem(
this.name,
itemId,
this.apiCall,
);
}
return this.individualItems[itemId];
}
}

private endpointPath(): string {
return `${CurationSets.RESOURCEPATH}/${encodeURIComponent(this.name)}`;
}
}


36 changes: 36 additions & 0 deletions src/Typesense/CurationSetItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import ApiCall from "./ApiCall";
import CurationSets, { CurationObjectSchema } from "./CurationSets";

export interface CurationItemDeleteResponseSchema {
id: string;
}

export default class CurationSetItem {
constructor(
private name: string,
private itemId: string,
private apiCall: ApiCall,
) {}

async retrieve(): Promise<CurationObjectSchema> {
return this.apiCall.get<CurationObjectSchema>(this.endpointPath());
}

async upsert(params: CurationObjectSchema): Promise<CurationObjectSchema> {
return this.apiCall.put<CurationObjectSchema>(this.endpointPath(), params);
}

async delete(): Promise<CurationItemDeleteResponseSchema> {
return this.apiCall.delete<CurationItemDeleteResponseSchema>(
this.endpointPath(),
);
}

private endpointPath(): string {
return `${CurationSets.RESOURCEPATH}/${encodeURIComponent(
this.name,
)}/items/${encodeURIComponent(this.itemId)}`;
}
}


18 changes: 18 additions & 0 deletions src/Typesense/CurationSetItems.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import ApiCall from "./ApiCall";
import CurationSets, { CurationObjectSchema } from "./CurationSets";

export default class CurationSetItems {
constructor(private name: string, private apiCall: ApiCall) {}

async retrieve(): Promise<CurationObjectSchema[]> {
return this.apiCall.get<CurationObjectSchema[]>(this.endpointPath());
}

private endpointPath(operation?: string): string {
return `${CurationSets.RESOURCEPATH}/${encodeURIComponent(this.name)}/items${
operation === undefined ? "" : "/" + encodeURIComponent(operation)
}`;
}
}


63 changes: 63 additions & 0 deletions src/Typesense/CurationSets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import ApiCall from "./ApiCall";

export interface CurationIncludeSchema {
id: string;
position: number;
}

export interface CurationExcludeSchema {
id: string;
}

export interface CurationRuleSchema {
query?: string;
match?: "exact" | "contains";
filter_by?: string;
tags?: string[];
}

export interface CurationObjectSchema {
id: string;
rule?: CurationRuleSchema;
includes?: CurationIncludeSchema[];
excludes?: CurationExcludeSchema[];
filter_by?: string;
sort_by?: string;
replace_query?: string;
remove_matched_tokens?: boolean;
filter_curated_hits?: boolean;
stop_processing?: boolean;
metadata?: Record<string, unknown>;
}

export interface CurationSetUpsertSchema {
items: CurationObjectSchema[];
}

export interface CurationSetSchema extends CurationSetUpsertSchema {
name?: string;
}

export interface CurationSetsListEntrySchema {
name: string;
items: CurationObjectSchema[];
}

export type CurationSetsListResponseSchema = CurationSetsListEntrySchema[];

export interface CurationSetDeleteResponseSchema {
name: string;
}

export default class CurationSets {
constructor(private apiCall: ApiCall) {}
static readonly RESOURCEPATH = "/curation_sets";

async retrieve(): Promise<CurationSetsListResponseSchema> {
return this.apiCall.get<CurationSetsListResponseSchema>(
CurationSets.RESOURCEPATH,
);
}
}


2 changes: 1 addition & 1 deletion test/Typesense/AnalyticsRules.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ describe.skipIf(!(await isV30OrAbove(typesense)))("AnalyticsRules", function ()
name: testRuleName,
type: "popular_queries",
collection: "products",
event_type: "query",
event_type: "search",
params: {
destination_collection: "products_top_queries",
expand_query: true,
Expand Down
90 changes: 90 additions & 0 deletions test/Typesense/CurationSetItems.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { Client as TypesenseClient } from "../../src/Typesense";
import { ObjectNotFound } from "../../src/Typesense/Errors";
import { isV30OrAbove } from "../utils";

const typesense = new TypesenseClient({
nodes: [
{
host: "localhost",
port: 8108,
protocol: "http",
},
],
apiKey: "xyz",
connectionTimeoutSeconds: 180,
});

describe.skipIf(!(await isV30OrAbove(typesense)))(
"CurationSetItems",
function () {
const testCurationSetName = "test-curation-set-items";
const initialData = {
items: [
{
id: "rule-1",
rule: {
query: "test",
match: "exact" as const,
},
includes: [{ id: "123", position: 1 }],
},
],
};

beforeEach(async function () {
try {
await typesense.curationSets(testCurationSetName).delete();
} catch (error) {
// ignore
}
});

afterEach(async function () {
try {
await typesense.curationSets(testCurationSetName).delete();
} catch (error) {
if (!(error instanceof ObjectNotFound)) {
console.warn("Failed to cleanup test curation set:", error);
}
}
});

it("lists items in a curation set", async function () {
await typesense.curationSets(testCurationSetName).upsert(initialData);

const items = await typesense
.curationSets(testCurationSetName)
.items()
.retrieve();

expect(Array.isArray(items)).toBe(true);
expect(items.length).toBeGreaterThan(0);
expect(items[0].includes?.[0].id).toBe("123");
});

it("upserts, retrieves and deletes an item", async function () {
await typesense.curationSets(testCurationSetName).upsert(initialData);

const upserted = await typesense
.curationSets(testCurationSetName)
.items("rule-1")
.upsert({ id: "rule-1", rule: { query: "test", match: "exact" as const }, includes: [{ id: "999", position: 1 }] });
expect(upserted.id).toBe("rule-1");

const fetched = await typesense
.curationSets(testCurationSetName)
.items("rule-1")
.retrieve();
expect(fetched.includes?.[0].id).toBe("999");

const deletion = await typesense
.curationSets(testCurationSetName)
.items("rule-1")
.delete();
expect(deletion.id).toBe("rule-1");
});
},
);


Loading
Loading