@tsctl/cli 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +735 -0
  2. package/bin/tsctl.js +2 -0
  3. package/package.json +65 -0
  4. package/src/__tests__/analyticsrules.test.ts +303 -0
  5. package/src/__tests__/apikeys.test.ts +223 -0
  6. package/src/__tests__/apply.test.ts +245 -0
  7. package/src/__tests__/client.test.ts +48 -0
  8. package/src/__tests__/collection-advanced.test.ts +274 -0
  9. package/src/__tests__/config-loader.test.ts +217 -0
  10. package/src/__tests__/curationsets.test.ts +190 -0
  11. package/src/__tests__/helpers.ts +17 -0
  12. package/src/__tests__/import-drift.test.ts +231 -0
  13. package/src/__tests__/migrate-advanced.test.ts +197 -0
  14. package/src/__tests__/migrate.test.ts +220 -0
  15. package/src/__tests__/plan-new-resources.test.ts +258 -0
  16. package/src/__tests__/plan.test.ts +337 -0
  17. package/src/__tests__/presets.test.ts +97 -0
  18. package/src/__tests__/resources.test.ts +592 -0
  19. package/src/__tests__/setup.ts +77 -0
  20. package/src/__tests__/state.test.ts +312 -0
  21. package/src/__tests__/stemmingdictionaries.test.ts +111 -0
  22. package/src/__tests__/stopwords.test.ts +109 -0
  23. package/src/__tests__/synonymsets.test.ts +170 -0
  24. package/src/__tests__/types.test.ts +416 -0
  25. package/src/apply/index.ts +336 -0
  26. package/src/cli/index.ts +1106 -0
  27. package/src/client/index.ts +55 -0
  28. package/src/config/loader.ts +158 -0
  29. package/src/index.ts +45 -0
  30. package/src/migrate/index.ts +220 -0
  31. package/src/plan/index.ts +1333 -0
  32. package/src/resources/alias.ts +59 -0
  33. package/src/resources/analyticsrule.ts +134 -0
  34. package/src/resources/apikey.ts +203 -0
  35. package/src/resources/collection.ts +424 -0
  36. package/src/resources/curationset.ts +155 -0
  37. package/src/resources/index.ts +11 -0
  38. package/src/resources/override.ts +174 -0
  39. package/src/resources/preset.ts +83 -0
  40. package/src/resources/stemmingdictionary.ts +103 -0
  41. package/src/resources/stopword.ts +100 -0
  42. package/src/resources/synonym.ts +152 -0
  43. package/src/resources/synonymset.ts +144 -0
  44. package/src/state/index.ts +206 -0
  45. package/src/types/index.ts +451 -0
@@ -0,0 +1,424 @@
1
+ import type { CollectionCreateSchema } from "typesense/lib/Typesense/Collections.js";
2
+ import { getClient } from "../client/index.js";
3
+ import type { CollectionConfig, Field } from "../types/index.js";
4
+
5
+ /**
6
+ * Convert our field config to Typesense field schema
7
+ */
8
+ function toTypesenseField(field: Field): Record<string, unknown> {
9
+ const result: Record<string, unknown> = {
10
+ name: field.name,
11
+ type: field.type,
12
+ };
13
+
14
+ if (field.optional !== undefined) result["optional"] = field.optional;
15
+ if (field.facet !== undefined) result["facet"] = field.facet;
16
+ if (field.index !== undefined) result["index"] = field.index;
17
+ if (field.sort !== undefined) result["sort"] = field.sort;
18
+ if (field.infix !== undefined) result["infix"] = field.infix;
19
+ if (field.locale !== undefined) result["locale"] = field.locale;
20
+ if (field.stem !== undefined) result["stem"] = field.stem;
21
+ if (field.store !== undefined) result["store"] = field.store;
22
+ if (field.num_dim !== undefined) result["num_dim"] = field.num_dim;
23
+ if (field.vec_dist !== undefined) result["vec_dist"] = field.vec_dist;
24
+ if (field.reference !== undefined) result["reference"] = field.reference;
25
+ if (field.range_index !== undefined) result["range_index"] = field.range_index;
26
+ if (field.stem_dictionary !== undefined) result["stem_dictionary"] = field.stem_dictionary;
27
+ if (field.truncate_len !== undefined) result["truncate_len"] = field.truncate_len;
28
+ if (field.token_separators !== undefined) result["token_separators"] = field.token_separators;
29
+ if (field.symbols_to_index !== undefined) result["symbols_to_index"] = field.symbols_to_index;
30
+ if (field.embed !== undefined) result["embed"] = field.embed;
31
+
32
+ return result;
33
+ }
34
+
35
+ /**
36
+ * Field default values in Typesense
37
+ * These are stripped when importing to keep configs minimal
38
+ */
39
+ const FIELD_DEFAULTS = {
40
+ optional: false,
41
+ facet: false,
42
+ index: true,
43
+ sort: false,
44
+ infix: false,
45
+ stem: false,
46
+ store: true,
47
+ range_index: false,
48
+ } as const;
49
+
50
+ /**
51
+ * Types that have sort enabled by default in Typesense
52
+ * Typesense auto-enables sort for numeric and bool types
53
+ */
54
+ const SORT_ENABLED_BY_DEFAULT_TYPES = new Set([
55
+ "int32", "int32[]",
56
+ "int64", "int64[]",
57
+ "float", "float[]",
58
+ "bool", "bool[]",
59
+ ]);
60
+
61
+ /**
62
+ * Convert Typesense field to our Field type
63
+ * Strips default values to keep config minimal
64
+ */
65
+ function fromTypesenseField(f: Record<string, unknown>): Field {
66
+ const fieldType = f["type"] as string;
67
+ const field: Field = {
68
+ name: f["name"] as string,
69
+ type: fieldType as Field["type"],
70
+ };
71
+
72
+ // Only include non-default values
73
+ if (f["optional"] !== undefined && f["optional"] !== FIELD_DEFAULTS.optional)
74
+ field.optional = f["optional"] as boolean;
75
+ if (f["facet"] !== undefined && f["facet"] !== FIELD_DEFAULTS.facet)
76
+ field.facet = f["facet"] as boolean;
77
+ if (f["index"] !== undefined && f["index"] !== FIELD_DEFAULTS.index)
78
+ field.index = f["index"] as boolean;
79
+
80
+ // sort: Typesense auto-enables for numeric/bool types, so only include if different from type default
81
+ const sortDefault = SORT_ENABLED_BY_DEFAULT_TYPES.has(fieldType);
82
+ if (f["sort"] !== undefined && f["sort"] !== sortDefault)
83
+ field.sort = f["sort"] as boolean;
84
+
85
+ if (f["infix"] !== undefined && f["infix"] !== FIELD_DEFAULTS.infix)
86
+ field.infix = f["infix"] as boolean;
87
+ if (f["stem"] !== undefined && f["stem"] !== FIELD_DEFAULTS.stem)
88
+ field.stem = f["stem"] as boolean;
89
+ if (f["store"] !== undefined && f["store"] !== FIELD_DEFAULTS.store)
90
+ field.store = f["store"] as boolean;
91
+ if (f["range_index"] !== undefined && f["range_index"] !== FIELD_DEFAULTS.range_index)
92
+ field.range_index = f["range_index"] as boolean;
93
+
94
+ // locale: only include if non-empty string
95
+ if (f["locale"] !== undefined && f["locale"] !== "")
96
+ field.locale = f["locale"] as string;
97
+
98
+ // These have no defaults, always include if present
99
+ if (f["num_dim"] !== undefined) field.num_dim = f["num_dim"] as number;
100
+ if (f["vec_dist"] !== undefined) field.vec_dist = f["vec_dist"] as "cosine" | "ip";
101
+ if (f["reference"] !== undefined) field.reference = f["reference"] as string;
102
+ if (f["embed"] !== undefined) field.embed = f["embed"] as Field["embed"];
103
+ if (f["stem_dictionary"] !== undefined) field.stem_dictionary = f["stem_dictionary"] as string;
104
+ if (f["truncate_len"] !== undefined && f["truncate_len"] !== 100) field.truncate_len = f["truncate_len"] as number;
105
+ // Field-level token_separators and symbols_to_index
106
+ const fieldTokenSeparators = f["token_separators"] as string[] | undefined;
107
+ if (fieldTokenSeparators && fieldTokenSeparators.length > 0) field.token_separators = fieldTokenSeparators;
108
+ const fieldSymbolsToIndex = f["symbols_to_index"] as string[] | undefined;
109
+ if (fieldSymbolsToIndex && fieldSymbolsToIndex.length > 0) field.symbols_to_index = fieldSymbolsToIndex;
110
+
111
+ return field;
112
+ }
113
+
114
+ /**
115
+ * Convert our collection config to Typesense schema
116
+ */
117
+ function toTypesenseSchema(config: CollectionConfig): CollectionCreateSchema {
118
+ const schema: Record<string, unknown> = {
119
+ name: config.name,
120
+ fields: config.fields.map(toTypesenseField),
121
+ };
122
+
123
+ if (config.default_sorting_field) {
124
+ schema["default_sorting_field"] = config.default_sorting_field;
125
+ }
126
+ if (config.token_separators) {
127
+ schema["token_separators"] = config.token_separators;
128
+ }
129
+ if (config.symbols_to_index) {
130
+ schema["symbols_to_index"] = config.symbols_to_index;
131
+ }
132
+ if (config.enable_nested_fields !== undefined) {
133
+ schema["enable_nested_fields"] = config.enable_nested_fields;
134
+ }
135
+ // Typesense 30.0+: synonym sets linking
136
+ if (config.synonym_sets && config.synonym_sets.length > 0) {
137
+ schema["synonym_sets"] = config.synonym_sets;
138
+ }
139
+ // Typesense 30.0+: curation sets linking
140
+ if (config.curation_sets && config.curation_sets.length > 0) {
141
+ schema["curation_sets"] = config.curation_sets;
142
+ }
143
+ // Custom metadata
144
+ if (config.metadata && Object.keys(config.metadata).length > 0) {
145
+ schema["metadata"] = config.metadata;
146
+ }
147
+
148
+ return schema as unknown as CollectionCreateSchema;
149
+ }
150
+
151
+ /**
152
+ * Get a collection from Typesense
153
+ */
154
+ export async function getCollection(
155
+ name: string
156
+ ): Promise<CollectionConfig | null> {
157
+ const client = getClient();
158
+
159
+ try {
160
+ const collection = await client.collections(name).retrieve();
161
+ const collectionData = collection as unknown as Record<string, unknown>;
162
+ const fields = collectionData["fields"] as Array<Record<string, unknown>> | undefined;
163
+
164
+ // Convert back to our config format
165
+ const config: CollectionConfig = {
166
+ name: collectionData["name"] as string,
167
+ fields: (fields || []).map(fromTypesenseField),
168
+ };
169
+
170
+ if (collectionData["default_sorting_field"]) {
171
+ config.default_sorting_field = collectionData["default_sorting_field"] as string;
172
+ }
173
+ // Only include if non-empty array
174
+ const tokenSeparators = collectionData["token_separators"] as string[] | undefined;
175
+ if (tokenSeparators && tokenSeparators.length > 0) {
176
+ config.token_separators = tokenSeparators;
177
+ }
178
+ // Only include if non-empty array
179
+ const symbolsToIndex = collectionData["symbols_to_index"] as string[] | undefined;
180
+ if (symbolsToIndex && symbolsToIndex.length > 0) {
181
+ config.symbols_to_index = symbolsToIndex;
182
+ }
183
+ // Only include enable_nested_fields if not the default (false)
184
+ if (collectionData["enable_nested_fields"] === true) {
185
+ config.enable_nested_fields = true;
186
+ }
187
+ // Typesense 30.0+: synonym sets linking
188
+ const synonymSets = collectionData["synonym_sets"] as string[] | undefined;
189
+ if (synonymSets && synonymSets.length > 0) {
190
+ config.synonym_sets = synonymSets;
191
+ }
192
+ // Typesense 30.0+: curation sets linking
193
+ const curationSets = collectionData["curation_sets"] as string[] | undefined;
194
+ if (curationSets && curationSets.length > 0) {
195
+ config.curation_sets = curationSets;
196
+ }
197
+ // Custom metadata
198
+ const metadata = collectionData["metadata"] as Record<string, unknown> | undefined;
199
+ if (metadata && Object.keys(metadata).length > 0) {
200
+ config.metadata = metadata;
201
+ }
202
+
203
+ return config;
204
+ } catch (error: unknown) {
205
+ if (
206
+ error &&
207
+ typeof error === "object" &&
208
+ "httpStatus" in error &&
209
+ error.httpStatus === 404
210
+ ) {
211
+ return null;
212
+ }
213
+ throw error;
214
+ }
215
+ }
216
+
217
+ /**
218
+ * List all collections from Typesense
219
+ */
220
+ export async function listCollections(): Promise<CollectionConfig[]> {
221
+ const client = getClient();
222
+ const collections = await client.collections().retrieve();
223
+
224
+ return (collections as unknown as Array<Record<string, unknown>>)
225
+ .filter((c) => !(c["name"] as string).startsWith("_tsctl_")) // Exclude our state collection
226
+ .map((collection) => {
227
+ const fields = collection["fields"] as Array<Record<string, unknown>> | undefined;
228
+
229
+ const config: CollectionConfig = {
230
+ name: collection["name"] as string,
231
+ fields: (fields || []).map(fromTypesenseField),
232
+ };
233
+
234
+ if (collection["default_sorting_field"]) {
235
+ config.default_sorting_field = collection["default_sorting_field"] as string;
236
+ }
237
+ // Only include if non-empty array
238
+ const tokenSeparators = collection["token_separators"] as string[] | undefined;
239
+ if (tokenSeparators && tokenSeparators.length > 0) {
240
+ config.token_separators = tokenSeparators;
241
+ }
242
+ // Only include if non-empty array
243
+ const symbolsToIndex = collection["symbols_to_index"] as string[] | undefined;
244
+ if (symbolsToIndex && symbolsToIndex.length > 0) {
245
+ config.symbols_to_index = symbolsToIndex;
246
+ }
247
+ // Only include enable_nested_fields if not the default (false)
248
+ if (collection["enable_nested_fields"] === true) {
249
+ config.enable_nested_fields = true;
250
+ }
251
+ // Typesense 30.0+: synonym sets linking
252
+ const synonymSets = collection["synonym_sets"] as string[] | undefined;
253
+ if (synonymSets && synonymSets.length > 0) {
254
+ config.synonym_sets = synonymSets;
255
+ }
256
+ // Typesense 30.0+: curation sets linking
257
+ const curationSets = collection["curation_sets"] as string[] | undefined;
258
+ if (curationSets && curationSets.length > 0) {
259
+ config.curation_sets = curationSets;
260
+ }
261
+ // Custom metadata
262
+ const metadata = collection["metadata"] as Record<string, unknown> | undefined;
263
+ if (metadata && Object.keys(metadata).length > 0) {
264
+ config.metadata = metadata;
265
+ }
266
+
267
+ return config;
268
+ });
269
+ }
270
+
271
+ /**
272
+ * Create a new collection in Typesense
273
+ */
274
+ export async function createCollection(config: CollectionConfig): Promise<void> {
275
+ const client = getClient();
276
+ const schema = toTypesenseSchema(config);
277
+ await client.collections().create(schema);
278
+ }
279
+
280
+ /**
281
+ * Check if two fields are equal (ignoring order of properties)
282
+ */
283
+ function fieldsEqual(a: Field, b: Field): boolean {
284
+ return JSON.stringify(normalizeField(a)) === JSON.stringify(normalizeField(b));
285
+ }
286
+
287
+ /**
288
+ * Normalize a field for comparison
289
+ */
290
+ function normalizeField(f: Field): Record<string, unknown> {
291
+ const result: Record<string, unknown> = {
292
+ name: f.name,
293
+ type: f.type,
294
+ };
295
+ if (f.optional !== undefined) result.optional = f.optional;
296
+ if (f.facet !== undefined) result.facet = f.facet;
297
+ if (f.index !== undefined) result.index = f.index;
298
+ if (f.sort !== undefined) result.sort = f.sort;
299
+ if (f.infix !== undefined) result.infix = f.infix;
300
+ if (f.locale !== undefined) result.locale = f.locale;
301
+ if (f.stem !== undefined) result.stem = f.stem;
302
+ if (f.store !== undefined) result.store = f.store;
303
+ if (f.num_dim !== undefined) result.num_dim = f.num_dim;
304
+ if (f.vec_dist !== undefined) result.vec_dist = f.vec_dist;
305
+ if (f.reference !== undefined) result.reference = f.reference;
306
+ if (f.range_index !== undefined) result.range_index = f.range_index;
307
+ if (f.stem_dictionary !== undefined) result.stem_dictionary = f.stem_dictionary;
308
+ if (f.truncate_len !== undefined) result.truncate_len = f.truncate_len;
309
+ if (f.token_separators !== undefined) result.token_separators = f.token_separators;
310
+ if (f.symbols_to_index !== undefined) result.symbols_to_index = f.symbols_to_index;
311
+ if (f.embed !== undefined) result.embed = f.embed;
312
+ return result;
313
+ }
314
+
315
+ /**
316
+ * Update a collection in Typesense
317
+ * Handles field modifications by dropping and re-adding fields
318
+ * Also handles synonym_sets updates
319
+ */
320
+ export async function updateCollection(
321
+ config: CollectionConfig,
322
+ _existing: CollectionConfig
323
+ ): Promise<{ requiresRecreate: boolean; fieldsToAdd: Field[]; fieldsToDrop: string[]; fieldsToModify: Field[] }> {
324
+ const client = getClient();
325
+
326
+ const existingFieldNames = new Set(_existing.fields.map((f) => f.name));
327
+ const newFieldNames = new Set(config.fields.map((f) => f.name));
328
+ const existingFieldMap = new Map(_existing.fields.map((f) => [f.name, f]));
329
+
330
+ // Fields to add (new fields not in existing)
331
+ const fieldsToAdd = config.fields.filter((f) => !existingFieldNames.has(f.name));
332
+
333
+ // Fields to drop (in existing but not in new config)
334
+ const fieldsToDrop = _existing.fields
335
+ .filter((f) => !newFieldNames.has(f.name))
336
+ .map((f) => f.name);
337
+
338
+ // Fields to modify (exist in both but have different config)
339
+ // These will be dropped and re-added
340
+ const fieldsToModify: Field[] = [];
341
+ for (const field of config.fields) {
342
+ const existingField = existingFieldMap.get(field.name);
343
+ if (existingField && !fieldsEqual(existingField, field)) {
344
+ fieldsToModify.push(field);
345
+ }
346
+ }
347
+
348
+ // Build update schema
349
+ const updateSchema: Record<string, unknown> = {};
350
+
351
+ // Build field updates
352
+ const updateFields: Array<Record<string, unknown>> = [];
353
+
354
+ // First, drop fields that need to be removed or modified
355
+ for (const name of fieldsToDrop) {
356
+ updateFields.push({ name, drop: true });
357
+ }
358
+ for (const field of fieldsToModify) {
359
+ updateFields.push({ name: field.name, drop: true });
360
+ }
361
+
362
+ // Then, add new fields and re-add modified fields
363
+ for (const field of fieldsToAdd) {
364
+ updateFields.push(toTypesenseField(field));
365
+ }
366
+ for (const field of fieldsToModify) {
367
+ updateFields.push(toTypesenseField(field));
368
+ }
369
+
370
+ if (updateFields.length > 0) {
371
+ updateSchema.fields = updateFields;
372
+ }
373
+
374
+ // Check if synonym_sets changed
375
+ const existingSynonymSets = _existing.synonym_sets || [];
376
+ const newSynonymSets = config.synonym_sets || [];
377
+ const synonymSetsChanged =
378
+ JSON.stringify(existingSynonymSets.sort()) !== JSON.stringify(newSynonymSets.sort());
379
+
380
+ if (synonymSetsChanged) {
381
+ updateSchema.synonym_sets = newSynonymSets;
382
+ }
383
+
384
+ // Check if curation_sets changed
385
+ const existingCurationSets = _existing.curation_sets || [];
386
+ const newCurationSets = config.curation_sets || [];
387
+ const curationSetsChanged =
388
+ JSON.stringify(existingCurationSets.sort()) !== JSON.stringify(newCurationSets.sort());
389
+
390
+ if (curationSetsChanged) {
391
+ updateSchema.curation_sets = newCurationSets;
392
+ }
393
+
394
+ // Check if metadata changed
395
+ const existingMetadata = _existing.metadata || {};
396
+ const newMetadata = config.metadata || {};
397
+ if (JSON.stringify(existingMetadata) !== JSON.stringify(newMetadata)) {
398
+ updateSchema.metadata = newMetadata;
399
+ }
400
+
401
+ // Apply changes if any
402
+ if (Object.keys(updateSchema).length > 0) {
403
+ await client.collections(config.name).update(updateSchema as never);
404
+ }
405
+
406
+ return { requiresRecreate: false, fieldsToAdd, fieldsToDrop, fieldsToModify };
407
+ }
408
+
409
+ /**
410
+ * Delete a collection from Typesense
411
+ */
412
+ export async function deleteCollection(name: string): Promise<void> {
413
+ const client = getClient();
414
+ await client.collections(name).delete();
415
+ }
416
+
417
+ /**
418
+ * Recreate a collection (delete and create)
419
+ * WARNING: This will delete all documents!
420
+ */
421
+ export async function recreateCollection(config: CollectionConfig): Promise<void> {
422
+ await deleteCollection(config.name);
423
+ await createCollection(config);
424
+ }
@@ -0,0 +1,155 @@
1
+ import { getClient } from "../client/index.js";
2
+ import type { CurationSetConfig, CurationItem } from "../types/index.js";
3
+
4
+ /**
5
+ * Convert Typesense curation item to our CurationItem format
6
+ * Strips default values to keep config minimal
7
+ */
8
+ function fromTypesenseCurationItem(
9
+ item: Record<string, unknown>
10
+ ): CurationItem {
11
+ const config: CurationItem = {
12
+ id: item.id as string,
13
+ };
14
+
15
+ if (item.rule) config.rule = item.rule as CurationItem["rule"];
16
+ if (item.includes)
17
+ config.includes = item.includes as CurationItem["includes"];
18
+ if (item.excludes)
19
+ config.excludes = item.excludes as CurationItem["excludes"];
20
+ if (item.filter_by) config.filter_by = item.filter_by as string;
21
+ if (item.sort_by) config.sort_by = item.sort_by as string;
22
+ if (item.replace_query)
23
+ config.replace_query = item.replace_query as string;
24
+ if (item.metadata && Object.keys(item.metadata as object).length > 0)
25
+ config.metadata = item.metadata as Record<string, unknown>;
26
+
27
+ // Only include non-default values
28
+ if (
29
+ item.remove_matched_tokens !== undefined &&
30
+ item.remove_matched_tokens !== true
31
+ )
32
+ config.remove_matched_tokens = item.remove_matched_tokens as boolean;
33
+ if (
34
+ item.filter_curated_hits !== undefined &&
35
+ item.filter_curated_hits !== false
36
+ )
37
+ config.filter_curated_hits = item.filter_curated_hits as boolean;
38
+ if (
39
+ item.stop_processing !== undefined &&
40
+ item.stop_processing !== true
41
+ )
42
+ config.stop_processing = item.stop_processing as boolean;
43
+
44
+ if (item.effective_from_ts !== undefined)
45
+ config.effective_from_ts = item.effective_from_ts as number;
46
+ if (item.effective_to_ts !== undefined)
47
+ config.effective_to_ts = item.effective_to_ts as number;
48
+
49
+ return config;
50
+ }
51
+
52
+ /**
53
+ * Get a curation set from Typesense
54
+ */
55
+ export async function getCurationSet(
56
+ name: string
57
+ ): Promise<CurationSetConfig | null> {
58
+ const client = getClient();
59
+
60
+ try {
61
+ const data = await client.curationSets(name).retrieve();
62
+ const items = (data.items || []) as unknown as Array<Record<string, unknown>>;
63
+
64
+ return {
65
+ name,
66
+ items: items.map(fromTypesenseCurationItem),
67
+ };
68
+ } catch (error: unknown) {
69
+ if (
70
+ error &&
71
+ typeof error === "object" &&
72
+ "httpStatus" in error &&
73
+ error.httpStatus === 404
74
+ ) {
75
+ return null;
76
+ }
77
+ throw error;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * List all curation sets from Typesense
83
+ */
84
+ export async function listCurationSets(): Promise<CurationSetConfig[]> {
85
+ const client = getClient();
86
+
87
+ try {
88
+ const sets = await client.curationSets().retrieve();
89
+ return sets.map((set) => ({
90
+ name: set.name,
91
+ items: (set.items || []).map((item) =>
92
+ fromTypesenseCurationItem(item as unknown as Record<string, unknown>)
93
+ ),
94
+ }));
95
+ } catch (error: unknown) {
96
+ if (
97
+ error &&
98
+ typeof error === "object" &&
99
+ "httpStatus" in error &&
100
+ (error.httpStatus === 404 || error.httpStatus === 400)
101
+ ) {
102
+ return [];
103
+ }
104
+ throw error;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Create a curation set in Typesense
110
+ */
111
+ export async function createCurationSet(
112
+ config: CurationSetConfig
113
+ ): Promise<void> {
114
+ const client = getClient();
115
+ await client.curationSets(config.name).upsert({
116
+ items: config.items as any,
117
+ });
118
+ }
119
+
120
+ /**
121
+ * Update a curation set in Typesense
122
+ */
123
+ export async function updateCurationSet(
124
+ config: CurationSetConfig,
125
+ _existing: CurationSetConfig
126
+ ): Promise<void> {
127
+ const client = getClient();
128
+ await client.curationSets(config.name).upsert({
129
+ items: config.items as any,
130
+ });
131
+ }
132
+
133
+ /**
134
+ * Delete a curation set from Typesense
135
+ */
136
+ export async function deleteCurationSet(name: string): Promise<void> {
137
+ const client = getClient();
138
+ await client.curationSets(name).delete();
139
+ }
140
+
141
+ /**
142
+ * Compare two curation set configs for equality
143
+ */
144
+ export function curationSetConfigsEqual(
145
+ a: CurationSetConfig,
146
+ b: CurationSetConfig
147
+ ): boolean {
148
+ if (a.name !== b.name) return false;
149
+ if (a.items.length !== b.items.length) return false;
150
+
151
+ const aItems = [...a.items].sort((x, y) => x.id.localeCompare(y.id));
152
+ const bItems = [...b.items].sort((x, y) => x.id.localeCompare(y.id));
153
+
154
+ return JSON.stringify(aItems) === JSON.stringify(bItems);
155
+ }
@@ -0,0 +1,11 @@
1
+ export * from "./collection.js";
2
+ export * from "./alias.js";
3
+ export * from "./synonym.js";
4
+ export * from "./synonymset.js";
5
+ export * from "./override.js";
6
+ export * from "./curationset.js";
7
+ export * from "./analyticsrule.js";
8
+ export * from "./apikey.js";
9
+ export * from "./stopword.js";
10
+ export * from "./preset.js";
11
+ export * from "./stemmingdictionary.js";