@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,592 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
+ import { setupClient, cleanupTypesense } from "./setup.js";
3
+ import { getTypesenseVersion } from "./helpers.js";
4
+ import {
5
+ createCollection,
6
+ getCollection,
7
+ listCollections,
8
+ updateCollection,
9
+ deleteCollection,
10
+ } from "../resources/collection.js";
11
+ import {
12
+ upsertAlias,
13
+ getAlias,
14
+ listAliases,
15
+ deleteAlias,
16
+ } from "../resources/alias.js";
17
+ import {
18
+ upsertSynonym,
19
+ getSynonym,
20
+ listSynonyms,
21
+ listAllSynonyms,
22
+ deleteSynonym,
23
+ } from "../resources/synonym.js";
24
+ import {
25
+ upsertOverride,
26
+ getOverride,
27
+ listOverrides,
28
+ listAllOverrides,
29
+ deleteOverride,
30
+ } from "../resources/override.js";
31
+ import {
32
+ createApiKey,
33
+ getApiKey,
34
+ listApiKeys,
35
+ deleteApiKey,
36
+ deleteApiKeyByDescription,
37
+ apiKeyConfigsEqual,
38
+ } from "../resources/apikey.js";
39
+ import {
40
+ createAnalyticsRule,
41
+ getAnalyticsRule,
42
+ listAnalyticsRules,
43
+ deleteAnalyticsRule,
44
+ analyticsRuleConfigsEqual,
45
+ } from "../resources/analyticsrule.js";
46
+ import type {
47
+ CollectionConfig,
48
+ AliasConfig,
49
+ SynonymConfig,
50
+ OverrideConfig,
51
+ ApiKeyConfig,
52
+ } from "../types/index.js";
53
+
54
+ describe("resources", () => {
55
+ beforeEach(async () => {
56
+ setupClient();
57
+ await cleanupTypesense();
58
+ });
59
+
60
+ afterEach(async () => {
61
+ await cleanupTypesense();
62
+ });
63
+
64
+ describe("collections", () => {
65
+ const productConfig: CollectionConfig = {
66
+ name: "products",
67
+ fields: [
68
+ { name: "title", type: "string" },
69
+ { name: "price", type: "float" },
70
+ { name: "category", type: "string", facet: true },
71
+ ],
72
+ default_sorting_field: "price",
73
+ };
74
+
75
+ test("createCollection creates a new collection", async () => {
76
+ await createCollection(productConfig);
77
+ const retrieved = await getCollection("products");
78
+ expect(retrieved).not.toBeNull();
79
+ expect(retrieved!.name).toBe("products");
80
+ expect(retrieved!.fields).toHaveLength(3);
81
+ expect(retrieved!.default_sorting_field).toBe("price");
82
+ });
83
+
84
+ test("getCollection returns null for non-existent", async () => {
85
+ const result = await getCollection("nonexistent");
86
+ expect(result).toBeNull();
87
+ });
88
+
89
+ test("listCollections returns all collections excluding state", async () => {
90
+ await createCollection(productConfig);
91
+ await createCollection({
92
+ name: "users",
93
+ fields: [{ name: "name", type: "string" }],
94
+ });
95
+
96
+ const collections = await listCollections();
97
+ const names = collections.map((c) => c.name);
98
+ expect(names).toContain("products");
99
+ expect(names).toContain("users");
100
+ expect(names).not.toContain("_tsctl_state");
101
+ });
102
+
103
+ test("updateCollection adds new fields", async () => {
104
+ await createCollection(productConfig);
105
+
106
+ const updatedConfig: CollectionConfig = {
107
+ ...productConfig,
108
+ fields: [
109
+ ...productConfig.fields,
110
+ { name: "description", type: "string", optional: true },
111
+ ],
112
+ };
113
+
114
+ const result = await updateCollection(updatedConfig, productConfig);
115
+ expect(result.fieldsToAdd).toHaveLength(1);
116
+ expect(result.fieldsToAdd[0]!.name).toBe("description");
117
+
118
+ const retrieved = await getCollection("products");
119
+ const fieldNames = retrieved!.fields.map((f) => f.name);
120
+ expect(fieldNames).toContain("description");
121
+ });
122
+
123
+ test("updateCollection drops removed fields", async () => {
124
+ await createCollection(productConfig);
125
+
126
+ const updatedConfig: CollectionConfig = {
127
+ name: "products",
128
+ fields: [
129
+ { name: "title", type: "string" },
130
+ { name: "price", type: "float" },
131
+ // category field removed
132
+ ],
133
+ default_sorting_field: "price",
134
+ };
135
+
136
+ const result = await updateCollection(updatedConfig, productConfig);
137
+ expect(result.fieldsToDrop).toContain("category");
138
+ });
139
+
140
+ test("deleteCollection removes collection", async () => {
141
+ await createCollection(productConfig);
142
+ await deleteCollection("products");
143
+ const result = await getCollection("products");
144
+ expect(result).toBeNull();
145
+ });
146
+
147
+ test("collection with token_separators and symbols_to_index", async () => {
148
+ const config: CollectionConfig = {
149
+ name: "custom",
150
+ fields: [{ name: "title", type: "string" }],
151
+ token_separators: ["-", "/"],
152
+ symbols_to_index: ["#", "@"],
153
+ };
154
+ await createCollection(config);
155
+ const retrieved = await getCollection("custom");
156
+ expect(retrieved!.token_separators).toEqual(["-", "/"]);
157
+ expect(retrieved!.symbols_to_index).toEqual(["#", "@"]);
158
+ });
159
+
160
+ test("collection with enable_nested_fields", async () => {
161
+ const config: CollectionConfig = {
162
+ name: "nested",
163
+ fields: [{ name: "metadata", type: "object" }],
164
+ enable_nested_fields: true,
165
+ };
166
+ await createCollection(config);
167
+ const retrieved = await getCollection("nested");
168
+ expect(retrieved!.enable_nested_fields).toBe(true);
169
+ });
170
+ });
171
+
172
+ describe("aliases", () => {
173
+ test("upsertAlias creates alias", async () => {
174
+ // Create target collection first
175
+ await createCollection({
176
+ name: "products",
177
+ fields: [{ name: "title", type: "string" }],
178
+ });
179
+
180
+ await upsertAlias({ name: "products_live", collection: "products" });
181
+ const alias = await getAlias("products_live");
182
+ expect(alias).not.toBeNull();
183
+ expect(alias!.name).toBe("products_live");
184
+ expect(alias!.collection).toBe("products");
185
+ });
186
+
187
+ test("getAlias returns null for non-existent", async () => {
188
+ const result = await getAlias("nonexistent");
189
+ expect(result).toBeNull();
190
+ });
191
+
192
+ test("upsertAlias updates existing alias", async () => {
193
+ await createCollection({
194
+ name: "products_v1",
195
+ fields: [{ name: "title", type: "string" }],
196
+ });
197
+ await createCollection({
198
+ name: "products_v2",
199
+ fields: [{ name: "title", type: "string" }],
200
+ });
201
+
202
+ await upsertAlias({ name: "products_live", collection: "products_v1" });
203
+ await upsertAlias({ name: "products_live", collection: "products_v2" });
204
+
205
+ const alias = await getAlias("products_live");
206
+ expect(alias!.collection).toBe("products_v2");
207
+ });
208
+
209
+ test("listAliases returns all aliases", async () => {
210
+ await createCollection({
211
+ name: "products",
212
+ fields: [{ name: "title", type: "string" }],
213
+ });
214
+ await createCollection({
215
+ name: "users",
216
+ fields: [{ name: "name", type: "string" }],
217
+ });
218
+
219
+ await upsertAlias({ name: "products_live", collection: "products" });
220
+ await upsertAlias({ name: "users_live", collection: "users" });
221
+
222
+ const aliases = await listAliases();
223
+ expect(aliases).toHaveLength(2);
224
+ const names = aliases.map((a) => a.name);
225
+ expect(names).toContain("products_live");
226
+ expect(names).toContain("users_live");
227
+ });
228
+
229
+ test("deleteAlias removes alias", async () => {
230
+ await createCollection({
231
+ name: "products",
232
+ fields: [{ name: "title", type: "string" }],
233
+ });
234
+ await upsertAlias({ name: "products_live", collection: "products" });
235
+ await deleteAlias("products_live");
236
+ const result = await getAlias("products_live");
237
+ expect(result).toBeNull();
238
+ });
239
+ });
240
+
241
+ describe("synonyms (legacy, pre-v30)", () => {
242
+ let version: number;
243
+
244
+ beforeEach(async () => {
245
+ version = await getTypesenseVersion();
246
+ await createCollection({
247
+ name: "products",
248
+ fields: [{ name: "title", type: "string" }],
249
+ });
250
+ });
251
+
252
+ test("upsertSynonym creates multi-way synonym", async () => {
253
+ if (version >= 30) return; // Legacy API removed in v30
254
+ await upsertSynonym({
255
+ id: "phone-synonyms",
256
+ collection: "products",
257
+ synonyms: ["phone", "mobile", "smartphone"],
258
+ });
259
+
260
+ const synonym = await getSynonym("phone-synonyms", "products");
261
+ expect(synonym).not.toBeNull();
262
+ expect(synonym!.synonyms).toEqual(["phone", "mobile", "smartphone"]);
263
+ });
264
+
265
+ test("upsertSynonym creates one-way synonym", async () => {
266
+ if (version >= 30) return;
267
+ await upsertSynonym({
268
+ id: "tv-synonym",
269
+ collection: "products",
270
+ root: "television",
271
+ synonyms: ["tv", "telly"],
272
+ });
273
+
274
+ const synonym = await getSynonym("tv-synonym", "products");
275
+ expect(synonym).not.toBeNull();
276
+ expect(synonym!.root).toBe("television");
277
+ });
278
+
279
+ test("getSynonym returns null for non-existent", async () => {
280
+ if (version >= 30) return;
281
+ const result = await getSynonym("nonexistent", "products");
282
+ expect(result).toBeNull();
283
+ });
284
+
285
+ test("listSynonyms returns all synonyms for collection", async () => {
286
+ if (version >= 30) return;
287
+ await upsertSynonym({
288
+ id: "syn1",
289
+ collection: "products",
290
+ synonyms: ["a", "b"],
291
+ });
292
+ await upsertSynonym({
293
+ id: "syn2",
294
+ collection: "products",
295
+ synonyms: ["c", "d"],
296
+ });
297
+
298
+ const synonyms = await listSynonyms("products");
299
+ expect(synonyms).toHaveLength(2);
300
+ });
301
+
302
+ test("listAllSynonyms aggregates across collections", async () => {
303
+ if (version >= 30) return;
304
+ await createCollection({
305
+ name: "users",
306
+ fields: [{ name: "name", type: "string" }],
307
+ });
308
+
309
+ await upsertSynonym({
310
+ id: "syn1",
311
+ collection: "products",
312
+ synonyms: ["a", "b"],
313
+ });
314
+ await upsertSynonym({
315
+ id: "syn2",
316
+ collection: "users",
317
+ synonyms: ["c", "d"],
318
+ });
319
+
320
+ const synonyms = await listAllSynonyms(["products", "users"]);
321
+ expect(synonyms).toHaveLength(2);
322
+ });
323
+
324
+ test("deleteSynonym removes synonym", async () => {
325
+ if (version >= 30) return;
326
+ await upsertSynonym({
327
+ id: "syn1",
328
+ collection: "products",
329
+ synonyms: ["a", "b"],
330
+ });
331
+ await deleteSynonym("syn1", "products");
332
+ const result = await getSynonym("syn1", "products");
333
+ expect(result).toBeNull();
334
+ });
335
+
336
+ test("upsertSynonym throws without synonyms or root", async () => {
337
+ if (version >= 30) return;
338
+ expect(
339
+ upsertSynonym({ id: "bad", collection: "products" })
340
+ ).rejects.toThrow("must have either 'synonyms' or 'root'");
341
+ });
342
+ });
343
+
344
+ describe("overrides (legacy, pre-v30)", () => {
345
+ let version: number;
346
+
347
+ beforeEach(async () => {
348
+ version = await getTypesenseVersion();
349
+ await createCollection({
350
+ name: "products",
351
+ fields: [{ name: "title", type: "string" }],
352
+ });
353
+ });
354
+
355
+ test("upsertOverride creates override with includes", async () => {
356
+ if (version >= 30) return;
357
+ await upsertOverride({
358
+ id: "pin-featured",
359
+ collection: "products",
360
+ rule: { query: "featured", match: "exact" },
361
+ includes: [{ id: "product-123", position: 1 }],
362
+ });
363
+
364
+ const override = await getOverride("pin-featured", "products");
365
+ expect(override).not.toBeNull();
366
+ expect(override!.includes).toHaveLength(1);
367
+ });
368
+
369
+ test("upsertOverride creates override with filter_by", async () => {
370
+ if (version >= 30) return;
371
+ await upsertOverride({
372
+ id: "boost-shoes",
373
+ collection: "products",
374
+ rule: { query: "shoes", match: "contains" },
375
+ filter_by: "category:=footwear",
376
+ });
377
+
378
+ const override = await getOverride("boost-shoes", "products");
379
+ expect(override).not.toBeNull();
380
+ expect(override!.filter_by).toBe("category:=footwear");
381
+ });
382
+
383
+ test("getOverride returns null for non-existent", async () => {
384
+ if (version >= 30) return;
385
+ const result = await getOverride("nonexistent", "products");
386
+ expect(result).toBeNull();
387
+ });
388
+
389
+ test("listOverrides returns all overrides for collection", async () => {
390
+ if (version >= 30) return;
391
+ await upsertOverride({
392
+ id: "ov1",
393
+ collection: "products",
394
+ rule: { query: "a", match: "exact" },
395
+ filter_by: "category:=shoes",
396
+ });
397
+ await upsertOverride({
398
+ id: "ov2",
399
+ collection: "products",
400
+ rule: { query: "b", match: "exact" },
401
+ filter_by: "category:=hats",
402
+ });
403
+
404
+ const overrides = await listOverrides("products");
405
+ expect(overrides).toHaveLength(2);
406
+ });
407
+
408
+ test("listAllOverrides aggregates across collections", async () => {
409
+ if (version >= 30) return;
410
+ await createCollection({
411
+ name: "users",
412
+ fields: [{ name: "name", type: "string" }],
413
+ });
414
+
415
+ await upsertOverride({
416
+ id: "ov1",
417
+ collection: "products",
418
+ rule: { query: "a", match: "exact" },
419
+ filter_by: "category:=shoes",
420
+ });
421
+ await upsertOverride({
422
+ id: "ov2",
423
+ collection: "users",
424
+ rule: { query: "b", match: "exact" },
425
+ filter_by: "role:=admin",
426
+ });
427
+
428
+ const overrides = await listAllOverrides(["products", "users"]);
429
+ expect(overrides).toHaveLength(2);
430
+ });
431
+
432
+ test("deleteOverride removes override", async () => {
433
+ if (version >= 30) return;
434
+ await upsertOverride({
435
+ id: "ov1",
436
+ collection: "products",
437
+ rule: { query: "a", match: "exact" },
438
+ filter_by: "category:=shoes",
439
+ });
440
+ await deleteOverride("ov1", "products");
441
+ const result = await getOverride("ov1", "products");
442
+ expect(result).toBeNull();
443
+ });
444
+ });
445
+
446
+ describe("apiKeys", () => {
447
+ test("createApiKey creates key and returns value", async () => {
448
+ const result = await createApiKey({
449
+ description: "Search key",
450
+ actions: ["documents:search"],
451
+ collections: ["products"],
452
+ });
453
+ expect(result.id).toBeDefined();
454
+ expect(result.value).toBeDefined();
455
+ expect(typeof result.value).toBe("string");
456
+ });
457
+
458
+ test("getApiKey retrieves key by description", async () => {
459
+ await createApiKey({
460
+ description: "Search key",
461
+ actions: ["documents:search"],
462
+ collections: ["products"],
463
+ });
464
+
465
+ const key = await getApiKey("Search key");
466
+ expect(key).not.toBeNull();
467
+ expect(key!.description).toBe("Search key");
468
+ expect(key!.actions).toEqual(["documents:search"]);
469
+ });
470
+
471
+ test("getApiKey returns null for non-existent", async () => {
472
+ const result = await getApiKey("nonexistent");
473
+ expect(result).toBeNull();
474
+ });
475
+
476
+ test("listApiKeys returns all keys", async () => {
477
+ await createApiKey({
478
+ description: "Key 1",
479
+ actions: ["documents:search"],
480
+ collections: ["*"],
481
+ });
482
+ await createApiKey({
483
+ description: "Key 2",
484
+ actions: ["*"],
485
+ collections: ["*"],
486
+ });
487
+
488
+ const keys = await listApiKeys();
489
+ const descriptions = keys.map((k) => k.description);
490
+ expect(descriptions).toContain("Key 1");
491
+ expect(descriptions).toContain("Key 2");
492
+ });
493
+
494
+ test("deleteApiKeyByDescription removes key", async () => {
495
+ await createApiKey({
496
+ description: "Temp key",
497
+ actions: ["*"],
498
+ collections: ["*"],
499
+ });
500
+
501
+ await deleteApiKeyByDescription("Temp key");
502
+ const result = await getApiKey("Temp key");
503
+ expect(result).toBeNull();
504
+ });
505
+
506
+ test("apiKeyConfigsEqual compares correctly", () => {
507
+ const a: ApiKeyConfig = {
508
+ description: "key",
509
+ actions: ["documents:search", "documents:get"],
510
+ collections: ["products", "users"],
511
+ };
512
+ const b: ApiKeyConfig = {
513
+ description: "key",
514
+ actions: ["documents:get", "documents:search"],
515
+ collections: ["users", "products"],
516
+ };
517
+ expect(apiKeyConfigsEqual(a, b)).toBe(true);
518
+ });
519
+
520
+ test("apiKeyConfigsEqual detects differences", () => {
521
+ const a: ApiKeyConfig = {
522
+ description: "key",
523
+ actions: ["documents:search"],
524
+ collections: ["products"],
525
+ };
526
+ const b: ApiKeyConfig = {
527
+ description: "key",
528
+ actions: ["*"],
529
+ collections: ["products"],
530
+ };
531
+ expect(apiKeyConfigsEqual(a, b)).toBe(false);
532
+ });
533
+
534
+ test("apiKeyConfigsEqual considers expires_at", () => {
535
+ const a: ApiKeyConfig = {
536
+ description: "key",
537
+ actions: ["*"],
538
+ collections: ["*"],
539
+ expires_at: 1000,
540
+ };
541
+ const b: ApiKeyConfig = {
542
+ description: "key",
543
+ actions: ["*"],
544
+ collections: ["*"],
545
+ expires_at: 2000,
546
+ };
547
+ expect(apiKeyConfigsEqual(a, b)).toBe(false);
548
+ });
549
+ });
550
+
551
+ describe("analyticsRules", () => {
552
+ test("analyticsRuleConfigsEqual compares correctly", () => {
553
+ const a = {
554
+ name: "popular",
555
+ type: "popular_queries" as const,
556
+ collection: "products",
557
+ event_type: "search" as const,
558
+ };
559
+ const b = { ...a };
560
+ expect(analyticsRuleConfigsEqual(a, b)).toBe(true);
561
+ });
562
+
563
+ test("analyticsRuleConfigsEqual detects differences", () => {
564
+ const a = {
565
+ name: "popular",
566
+ type: "popular_queries" as const,
567
+ collection: "products",
568
+ event_type: "search" as const,
569
+ };
570
+ const b = {
571
+ ...a,
572
+ event_type: "click" as const,
573
+ };
574
+ expect(analyticsRuleConfigsEqual(a, b)).toBe(false);
575
+ });
576
+
577
+ test("analyticsRuleConfigsEqual with params", () => {
578
+ const a = {
579
+ name: "popular",
580
+ type: "popular_queries" as const,
581
+ collection: "products",
582
+ event_type: "search" as const,
583
+ params: { limit: 100 },
584
+ };
585
+ const b = {
586
+ ...a,
587
+ params: { limit: 200 },
588
+ };
589
+ expect(analyticsRuleConfigsEqual(a, b)).toBe(false);
590
+ });
591
+ });
592
+ });
@@ -0,0 +1,77 @@
1
+ import { initClient, getClient } from "../client/index.js";
2
+
3
+ const TEST_API_KEY = "test-api-key";
4
+ const TEST_HOST = "localhost";
5
+ const TEST_PORT = 8108;
6
+ const TEST_PROTOCOL = "http" as const;
7
+
8
+ export function setupClient() {
9
+ return initClient({
10
+ nodes: [{ host: TEST_HOST, port: TEST_PORT, protocol: TEST_PROTOCOL }],
11
+ apiKey: TEST_API_KEY,
12
+ });
13
+ }
14
+
15
+ export async function cleanupTypesense() {
16
+ const client = getClient();
17
+
18
+ // Delete all collections
19
+ const collections = await client.collections().retrieve();
20
+ for (const collection of collections) {
21
+ await client.collections(collection.name).delete();
22
+ }
23
+
24
+ // Delete all aliases
25
+ const aliasesResponse = await client.aliases().retrieve();
26
+ for (const alias of aliasesResponse.aliases) {
27
+ await client.aliases(alias.name).delete();
28
+ }
29
+
30
+ // Delete all API keys (except the admin key, id=0)
31
+ const keysResponse = await client.keys().retrieve();
32
+ for (const key of keysResponse.keys) {
33
+ if (key.id !== 0) {
34
+ await client.keys(key.id).delete();
35
+ }
36
+ }
37
+
38
+ // Delete all analytics rules
39
+ try {
40
+ const rulesResponse = await client.analytics.rules().retrieve();
41
+ for (const rule of rulesResponse.rules || []) {
42
+ await client.analytics.rules(rule.name).delete();
43
+ }
44
+ } catch {
45
+ // analytics may not be available
46
+ }
47
+
48
+ // Delete all stopword sets
49
+ try {
50
+ const stopwordsResponse = await client.stopwords().retrieve();
51
+ for (const sw of stopwordsResponse.stopwords) {
52
+ await client.stopwords(sw.id).delete();
53
+ }
54
+ } catch {
55
+ // stopwords may not be available
56
+ }
57
+
58
+ // Delete all presets
59
+ try {
60
+ const presetsResponse = await client.presets().retrieve();
61
+ for (const preset of presetsResponse.presets) {
62
+ await client.presets(preset.name).delete();
63
+ }
64
+ } catch {
65
+ // presets may not be available
66
+ }
67
+
68
+ // Delete all curation sets (v30+)
69
+ try {
70
+ const curationSets = await client.curationSets().retrieve();
71
+ for (const set of curationSets) {
72
+ await client.curationSets(set.name).delete();
73
+ }
74
+ } catch {
75
+ // curation sets may not be available (pre-v30)
76
+ }
77
+ }