@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.
- package/README.md +735 -0
- package/bin/tsctl.js +2 -0
- package/package.json +65 -0
- package/src/__tests__/analyticsrules.test.ts +303 -0
- package/src/__tests__/apikeys.test.ts +223 -0
- package/src/__tests__/apply.test.ts +245 -0
- package/src/__tests__/client.test.ts +48 -0
- package/src/__tests__/collection-advanced.test.ts +274 -0
- package/src/__tests__/config-loader.test.ts +217 -0
- package/src/__tests__/curationsets.test.ts +190 -0
- package/src/__tests__/helpers.ts +17 -0
- package/src/__tests__/import-drift.test.ts +231 -0
- package/src/__tests__/migrate-advanced.test.ts +197 -0
- package/src/__tests__/migrate.test.ts +220 -0
- package/src/__tests__/plan-new-resources.test.ts +258 -0
- package/src/__tests__/plan.test.ts +337 -0
- package/src/__tests__/presets.test.ts +97 -0
- package/src/__tests__/resources.test.ts +592 -0
- package/src/__tests__/setup.ts +77 -0
- package/src/__tests__/state.test.ts +312 -0
- package/src/__tests__/stemmingdictionaries.test.ts +111 -0
- package/src/__tests__/stopwords.test.ts +109 -0
- package/src/__tests__/synonymsets.test.ts +170 -0
- package/src/__tests__/types.test.ts +416 -0
- package/src/apply/index.ts +336 -0
- package/src/cli/index.ts +1106 -0
- package/src/client/index.ts +55 -0
- package/src/config/loader.ts +158 -0
- package/src/index.ts +45 -0
- package/src/migrate/index.ts +220 -0
- package/src/plan/index.ts +1333 -0
- package/src/resources/alias.ts +59 -0
- package/src/resources/analyticsrule.ts +134 -0
- package/src/resources/apikey.ts +203 -0
- package/src/resources/collection.ts +424 -0
- package/src/resources/curationset.ts +155 -0
- package/src/resources/index.ts +11 -0
- package/src/resources/override.ts +174 -0
- package/src/resources/preset.ts +83 -0
- package/src/resources/stemmingdictionary.ts +103 -0
- package/src/resources/stopword.ts +100 -0
- package/src/resources/synonym.ts +152 -0
- package/src/resources/synonymset.ts +144 -0
- package/src/state/index.ts +206 -0
- 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
|
+
}
|