@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,174 @@
1
+ import { getClient } from "../client/index.js";
2
+ import type { OverrideConfig } from "../types/index.js";
3
+
4
+ /**
5
+ * Convert our OverrideConfig to Typesense API format
6
+ */
7
+ function toTypesenseOverride(config: OverrideConfig): Record<string, unknown> {
8
+ const result: Record<string, unknown> = {
9
+ rule: config.rule,
10
+ };
11
+
12
+ if (config.includes) result.includes = config.includes;
13
+ if (config.excludes) result.excludes = config.excludes;
14
+ if (config.filter_by) result.filter_by = config.filter_by;
15
+ if (config.sort_by) result.sort_by = config.sort_by;
16
+ if (config.replace_query) result.replace_query = config.replace_query;
17
+ if (config.remove_matched_tokens !== undefined)
18
+ result.remove_matched_tokens = config.remove_matched_tokens;
19
+ if (config.filter_curated_hits !== undefined)
20
+ result.filter_curated_hits = config.filter_curated_hits;
21
+ if (config.effective_from_ts !== undefined)
22
+ result.effective_from_ts = config.effective_from_ts;
23
+ if (config.effective_to_ts !== undefined)
24
+ result.effective_to_ts = config.effective_to_ts;
25
+ if (config.stop_processing !== undefined)
26
+ result.stop_processing = config.stop_processing;
27
+
28
+ return result;
29
+ }
30
+
31
+ /**
32
+ * Override default values in Typesense
33
+ * These are stripped when importing to keep configs minimal
34
+ */
35
+ const OVERRIDE_DEFAULTS = {
36
+ remove_matched_tokens: false,
37
+ filter_curated_hits: false,
38
+ stop_processing: true,
39
+ } as const;
40
+
41
+ /**
42
+ * Convert Typesense API override to our OverrideConfig format
43
+ * Strips default values to keep config minimal
44
+ */
45
+ function fromTypesenseOverride(
46
+ override: Record<string, unknown>,
47
+ collection: string
48
+ ): OverrideConfig {
49
+ const config: OverrideConfig = {
50
+ id: override.id as string,
51
+ collection,
52
+ rule: override.rule as OverrideConfig["rule"],
53
+ };
54
+
55
+ // These have no defaults, always include if present
56
+ if (override.includes)
57
+ config.includes = override.includes as OverrideConfig["includes"];
58
+ if (override.excludes)
59
+ config.excludes = override.excludes as OverrideConfig["excludes"];
60
+ if (override.filter_by) config.filter_by = override.filter_by as string;
61
+ if (override.sort_by) config.sort_by = override.sort_by as string;
62
+ if (override.replace_query)
63
+ config.replace_query = override.replace_query as string;
64
+ if (override.effective_from_ts !== undefined)
65
+ config.effective_from_ts = override.effective_from_ts as number;
66
+ if (override.effective_to_ts !== undefined)
67
+ config.effective_to_ts = override.effective_to_ts as number;
68
+
69
+ // Only include non-default values
70
+ if (
71
+ override.remove_matched_tokens !== undefined &&
72
+ override.remove_matched_tokens !== OVERRIDE_DEFAULTS.remove_matched_tokens
73
+ )
74
+ config.remove_matched_tokens = override.remove_matched_tokens as boolean;
75
+ if (
76
+ override.filter_curated_hits !== undefined &&
77
+ override.filter_curated_hits !== OVERRIDE_DEFAULTS.filter_curated_hits
78
+ )
79
+ config.filter_curated_hits = override.filter_curated_hits as boolean;
80
+ if (
81
+ override.stop_processing !== undefined &&
82
+ override.stop_processing !== OVERRIDE_DEFAULTS.stop_processing
83
+ )
84
+ config.stop_processing = override.stop_processing as boolean;
85
+
86
+ return config;
87
+ }
88
+
89
+ /**
90
+ * Get an override from Typesense
91
+ */
92
+ export async function getOverride(
93
+ id: string,
94
+ collection: string
95
+ ): Promise<OverrideConfig | null> {
96
+ const client = getClient();
97
+
98
+ try {
99
+ const override = await client.collections(collection).overrides(id).retrieve();
100
+ return fromTypesenseOverride(override as unknown as Record<string, unknown>, collection);
101
+ } catch (error: unknown) {
102
+ if (
103
+ error &&
104
+ typeof error === "object" &&
105
+ "httpStatus" in error &&
106
+ error.httpStatus === 404
107
+ ) {
108
+ return null;
109
+ }
110
+ throw error;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * List all overrides for a collection from Typesense
116
+ */
117
+ export async function listOverrides(collection: string): Promise<OverrideConfig[]> {
118
+ const client = getClient();
119
+
120
+ try {
121
+ const response = await client.collections(collection).overrides().retrieve();
122
+ return response.overrides.map((override) =>
123
+ fromTypesenseOverride(override as unknown as Record<string, unknown>, collection)
124
+ );
125
+ } catch (error: unknown) {
126
+ // Collection might not exist
127
+ if (
128
+ error &&
129
+ typeof error === "object" &&
130
+ "httpStatus" in error &&
131
+ error.httpStatus === 404
132
+ ) {
133
+ return [];
134
+ }
135
+ throw error;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * List all overrides across all collections
141
+ */
142
+ export async function listAllOverrides(
143
+ collections: string[]
144
+ ): Promise<OverrideConfig[]> {
145
+ const allOverrides: OverrideConfig[] = [];
146
+
147
+ for (const collection of collections) {
148
+ const overrides = await listOverrides(collection);
149
+ allOverrides.push(...overrides);
150
+ }
151
+
152
+ return allOverrides;
153
+ }
154
+
155
+ /**
156
+ * Create or update an override in Typesense
157
+ * Overrides are upserted in Typesense
158
+ */
159
+ export async function upsertOverride(config: OverrideConfig): Promise<void> {
160
+ const client = getClient();
161
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
162
+ await client
163
+ .collections(config.collection)
164
+ .overrides()
165
+ .upsert(config.id, toTypesenseOverride(config) as any);
166
+ }
167
+
168
+ /**
169
+ * Delete an override from Typesense
170
+ */
171
+ export async function deleteOverride(id: string, collection: string): Promise<void> {
172
+ const client = getClient();
173
+ await client.collections(collection).overrides(id).delete();
174
+ }
@@ -0,0 +1,83 @@
1
+ import { getClient } from "../client/index.js";
2
+ import type { PresetConfig } from "../types/index.js";
3
+
4
+ /**
5
+ * Get a preset from Typesense
6
+ */
7
+ export async function getPreset(
8
+ name: string
9
+ ): Promise<PresetConfig | null> {
10
+ const client = getClient();
11
+
12
+ try {
13
+ const data = await client.presets(name).retrieve();
14
+ return {
15
+ name: data.name,
16
+ value: data.value as Record<string, unknown>,
17
+ };
18
+ } catch (error: unknown) {
19
+ if (
20
+ error &&
21
+ typeof error === "object" &&
22
+ "httpStatus" in error &&
23
+ error.httpStatus === 404
24
+ ) {
25
+ return null;
26
+ }
27
+ throw error;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * List all presets from Typesense
33
+ */
34
+ export async function listPresets(): Promise<PresetConfig[]> {
35
+ const client = getClient();
36
+
37
+ try {
38
+ const response = await client.presets().retrieve();
39
+ return response.presets.map((p) => ({
40
+ name: p.name,
41
+ value: p.value as Record<string, unknown>,
42
+ }));
43
+ } catch (error: unknown) {
44
+ if (
45
+ error &&
46
+ typeof error === "object" &&
47
+ "httpStatus" in error &&
48
+ (error.httpStatus === 404 || error.httpStatus === 400)
49
+ ) {
50
+ return [];
51
+ }
52
+ throw error;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Create or update a preset in Typesense
58
+ */
59
+ export async function upsertPreset(config: PresetConfig): Promise<void> {
60
+ const client = getClient();
61
+ await client.presets().upsert(config.name, {
62
+ value: config.value as any,
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Delete a preset from Typesense
68
+ */
69
+ export async function deletePreset(name: string): Promise<void> {
70
+ const client = getClient();
71
+ await client.presets(name).delete();
72
+ }
73
+
74
+ /**
75
+ * Compare two preset configs for equality
76
+ */
77
+ export function presetConfigsEqual(
78
+ a: PresetConfig,
79
+ b: PresetConfig
80
+ ): boolean {
81
+ if (a.name !== b.name) return false;
82
+ return JSON.stringify(a.value) === JSON.stringify(b.value);
83
+ }
@@ -0,0 +1,103 @@
1
+ import { getClient } from "../client/index.js";
2
+ import type { StemmingDictionaryConfig, StemmingWord } from "../types/index.js";
3
+
4
+ /**
5
+ * Get a stemming dictionary from Typesense
6
+ */
7
+ export async function getStemmingDictionary(
8
+ id: string
9
+ ): Promise<StemmingDictionaryConfig | null> {
10
+ const client = getClient();
11
+
12
+ try {
13
+ const data = await client.stemming.dictionaries(id).retrieve();
14
+ return {
15
+ id: data.id,
16
+ words: data.words.map((w) => ({
17
+ word: w.word,
18
+ root: w.root,
19
+ })),
20
+ };
21
+ } catch (error: unknown) {
22
+ if (
23
+ error &&
24
+ typeof error === "object" &&
25
+ "httpStatus" in error &&
26
+ (error.httpStatus === 404 || error.httpStatus === 400)
27
+ ) {
28
+ return null;
29
+ }
30
+ throw error;
31
+ }
32
+ }
33
+
34
+ /**
35
+ * List all stemming dictionaries from Typesense
36
+ */
37
+ export async function listStemmingDictionaries(): Promise<
38
+ StemmingDictionaryConfig[]
39
+ > {
40
+ const client = getClient();
41
+
42
+ try {
43
+ const response = await client.stemming.dictionaries().retrieve();
44
+ const dictionaries: StemmingDictionaryConfig[] = [];
45
+
46
+ for (const dictId of response.dictionaries) {
47
+ try {
48
+ const dict = await client.stemming.dictionaries(dictId).retrieve();
49
+ dictionaries.push({
50
+ id: dict.id,
51
+ words: dict.words.map((w) => ({
52
+ word: w.word,
53
+ root: w.root,
54
+ })),
55
+ });
56
+ } catch {
57
+ // Skip dictionaries we can't retrieve
58
+ }
59
+ }
60
+
61
+ return dictionaries;
62
+ } catch (error: unknown) {
63
+ if (
64
+ error &&
65
+ typeof error === "object" &&
66
+ "httpStatus" in error &&
67
+ (error.httpStatus === 404 || error.httpStatus === 400)
68
+ ) {
69
+ return [];
70
+ }
71
+ throw error;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Create or update a stemming dictionary in Typesense
77
+ * Stemming dictionaries are uploaded as arrays of word/root pairs
78
+ */
79
+ export async function upsertStemmingDictionary(
80
+ config: StemmingDictionaryConfig
81
+ ): Promise<void> {
82
+ const client = getClient();
83
+ await client.stemming.dictionaries().upsert(
84
+ config.id,
85
+ config.words.map((w) => ({ word: w.word, root: w.root }))
86
+ );
87
+ }
88
+
89
+ /**
90
+ * Compare two stemming dictionary configs for equality
91
+ */
92
+ export function stemmingDictionaryConfigsEqual(
93
+ a: StemmingDictionaryConfig,
94
+ b: StemmingDictionaryConfig
95
+ ): boolean {
96
+ if (a.id !== b.id) return false;
97
+ if (a.words.length !== b.words.length) return false;
98
+
99
+ const aWords = [...a.words].sort((x, y) => x.word.localeCompare(y.word));
100
+ const bWords = [...b.words].sort((x, y) => x.word.localeCompare(y.word));
101
+
102
+ return JSON.stringify(aWords) === JSON.stringify(bWords);
103
+ }
@@ -0,0 +1,100 @@
1
+ import { getClient } from "../client/index.js";
2
+ import type { StopwordSetConfig } from "../types/index.js";
3
+
4
+ /**
5
+ * Get a stopword set from Typesense
6
+ */
7
+ export async function getStopwordSet(
8
+ id: string
9
+ ): Promise<StopwordSetConfig | null> {
10
+ const client = getClient();
11
+
12
+ try {
13
+ const raw = await client.stopwords(id).retrieve();
14
+ // The retrieve response may wrap the data in a "stopwords" key
15
+ const data = (raw as any).stopwords || raw;
16
+ const config: StopwordSetConfig = {
17
+ id: data.id || id,
18
+ stopwords: Array.isArray(data.stopwords) ? data.stopwords : [],
19
+ };
20
+ if (data.locale) config.locale = data.locale;
21
+ return config;
22
+ } catch (error: unknown) {
23
+ if (
24
+ error &&
25
+ typeof error === "object" &&
26
+ "httpStatus" in error &&
27
+ error.httpStatus === 404
28
+ ) {
29
+ return null;
30
+ }
31
+ throw error;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * List all stopword sets from Typesense
37
+ */
38
+ export async function listStopwordSets(): Promise<StopwordSetConfig[]> {
39
+ const client = getClient();
40
+
41
+ try {
42
+ const response = await client.stopwords().retrieve();
43
+ return response.stopwords.map((s) => {
44
+ const config: StopwordSetConfig = {
45
+ id: s.id,
46
+ stopwords: Array.isArray(s.stopwords) ? s.stopwords : [],
47
+ };
48
+ if (s.locale) config.locale = s.locale;
49
+ return config;
50
+ });
51
+ } catch (error: unknown) {
52
+ if (
53
+ error &&
54
+ typeof error === "object" &&
55
+ "httpStatus" in error &&
56
+ (error.httpStatus === 404 || error.httpStatus === 400)
57
+ ) {
58
+ return [];
59
+ }
60
+ throw error;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Create or update a stopword set in Typesense
66
+ */
67
+ export async function upsertStopwordSet(
68
+ config: StopwordSetConfig
69
+ ): Promise<void> {
70
+ const client = getClient();
71
+
72
+ const params: { stopwords: string[]; locale?: string } = {
73
+ stopwords: config.stopwords,
74
+ };
75
+ if (config.locale) params.locale = config.locale;
76
+
77
+ await client.stopwords().upsert(config.id, params);
78
+ }
79
+
80
+ /**
81
+ * Delete a stopword set from Typesense
82
+ */
83
+ export async function deleteStopwordSet(id: string): Promise<void> {
84
+ const client = getClient();
85
+ await client.stopwords(id).delete();
86
+ }
87
+
88
+ /**
89
+ * Compare two stopword set configs for equality
90
+ */
91
+ export function stopwordSetConfigsEqual(
92
+ a: StopwordSetConfig,
93
+ b: StopwordSetConfig
94
+ ): boolean {
95
+ if (a.id !== b.id) return false;
96
+ if (a.locale !== b.locale) return false;
97
+ const aSorted = [...a.stopwords].sort();
98
+ const bSorted = [...b.stopwords].sort();
99
+ return JSON.stringify(aSorted) === JSON.stringify(bSorted);
100
+ }
@@ -0,0 +1,152 @@
1
+ import { getClient } from "../client/index.js";
2
+ import type { SynonymConfig } from "../types/index.js";
3
+
4
+ /**
5
+ * Convert our SynonymConfig to Typesense API format
6
+ */
7
+ function toTypesenseSynonym(config: SynonymConfig): {
8
+ synonyms?: string[];
9
+ root?: string;
10
+ symbols_to_index?: string[];
11
+ locale?: string;
12
+ } {
13
+ const result: {
14
+ synonyms?: string[];
15
+ root?: string;
16
+ symbols_to_index?: string[];
17
+ locale?: string;
18
+ } = {};
19
+
20
+ if (config.synonyms) result.synonyms = config.synonyms;
21
+ if (config.root) result.root = config.root;
22
+ if (config.symbols_to_index) result.symbols_to_index = config.symbols_to_index;
23
+ if (config.locale) result.locale = config.locale;
24
+
25
+ return result;
26
+ }
27
+
28
+ /**
29
+ * Convert Typesense API synonym to our SynonymConfig format
30
+ */
31
+ function fromTypesenseSynonym(
32
+ synonym: {
33
+ id: string;
34
+ synonyms?: string[];
35
+ root?: string;
36
+ symbols_to_index?: string[];
37
+ locale?: string;
38
+ },
39
+ collection: string
40
+ ): SynonymConfig {
41
+ const config: SynonymConfig = {
42
+ id: synonym.id,
43
+ collection,
44
+ };
45
+
46
+ if (synonym.synonyms) config.synonyms = synonym.synonyms;
47
+ if (synonym.root) config.root = synonym.root;
48
+ if (synonym.symbols_to_index) config.symbols_to_index = synonym.symbols_to_index;
49
+ if (synonym.locale) config.locale = synonym.locale;
50
+
51
+ return config;
52
+ }
53
+
54
+ /**
55
+ * Get a synonym from Typesense
56
+ */
57
+ export async function getSynonym(
58
+ id: string,
59
+ collection: string
60
+ ): Promise<SynonymConfig | null> {
61
+ const client = getClient();
62
+
63
+ try {
64
+ const synonym = await client.collections(collection).synonyms(id).retrieve();
65
+ return fromTypesenseSynonym(synonym, collection);
66
+ } catch (error: unknown) {
67
+ if (
68
+ error &&
69
+ typeof error === "object" &&
70
+ "httpStatus" in error &&
71
+ error.httpStatus === 404
72
+ ) {
73
+ return null;
74
+ }
75
+ throw error;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * List all synonyms for a collection from Typesense
81
+ */
82
+ export async function listSynonyms(collection: string): Promise<SynonymConfig[]> {
83
+ const client = getClient();
84
+
85
+ try {
86
+ const response = await client.collections(collection).synonyms().retrieve();
87
+ return response.synonyms.map((synonym: {
88
+ id: string;
89
+ synonyms?: string[];
90
+ root?: string;
91
+ symbols_to_index?: string[];
92
+ locale?: string;
93
+ }) => fromTypesenseSynonym(synonym, collection));
94
+ } catch (error: unknown) {
95
+ // Collection might not exist
96
+ if (
97
+ error &&
98
+ typeof error === "object" &&
99
+ "httpStatus" in error &&
100
+ error.httpStatus === 404
101
+ ) {
102
+ return [];
103
+ }
104
+ throw error;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * List all synonyms across all collections
110
+ */
111
+ export async function listAllSynonyms(
112
+ collections: string[]
113
+ ): Promise<SynonymConfig[]> {
114
+ const allSynonyms: SynonymConfig[] = [];
115
+
116
+ for (const collection of collections) {
117
+ const synonyms = await listSynonyms(collection);
118
+ allSynonyms.push(...synonyms);
119
+ }
120
+
121
+ return allSynonyms;
122
+ }
123
+
124
+ /**
125
+ * Create or update a synonym in Typesense
126
+ * Synonyms are upserted in Typesense
127
+ */
128
+ export async function upsertSynonym(config: SynonymConfig): Promise<void> {
129
+ const client = getClient();
130
+ const synonymData = toTypesenseSynonym(config);
131
+
132
+ // Typesense requires either synonyms or root to be present
133
+ if (!synonymData.synonyms && !synonymData.root) {
134
+ throw new Error(
135
+ `Synonym ${config.id} must have either 'synonyms' or 'root' defined`
136
+ );
137
+ }
138
+
139
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
140
+ await client
141
+ .collections(config.collection)
142
+ .synonyms()
143
+ .upsert(config.id, synonymData as any);
144
+ }
145
+
146
+ /**
147
+ * Delete a synonym from Typesense
148
+ */
149
+ export async function deleteSynonym(id: string, collection: string): Promise<void> {
150
+ const client = getClient();
151
+ await client.collections(collection).synonyms(id).delete();
152
+ }