@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,59 @@
|
|
|
1
|
+
import { getClient } from "../client/index.js";
|
|
2
|
+
import type { AliasConfig } from "../types/index.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get an alias from Typesense
|
|
6
|
+
*/
|
|
7
|
+
export async function getAlias(name: string): Promise<AliasConfig | null> {
|
|
8
|
+
const client = getClient();
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const alias = await client.aliases(name).retrieve();
|
|
12
|
+
return {
|
|
13
|
+
name: alias.name,
|
|
14
|
+
collection: alias.collection_name,
|
|
15
|
+
};
|
|
16
|
+
} catch (error: unknown) {
|
|
17
|
+
if (
|
|
18
|
+
error &&
|
|
19
|
+
typeof error === "object" &&
|
|
20
|
+
"httpStatus" in error &&
|
|
21
|
+
error.httpStatus === 404
|
|
22
|
+
) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* List all aliases from Typesense
|
|
31
|
+
*/
|
|
32
|
+
export async function listAliases(): Promise<AliasConfig[]> {
|
|
33
|
+
const client = getClient();
|
|
34
|
+
const response = await client.aliases().retrieve();
|
|
35
|
+
|
|
36
|
+
return response.aliases.map((alias: { name: string; collection_name: string }) => ({
|
|
37
|
+
name: alias.name,
|
|
38
|
+
collection: alias.collection_name,
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create or update an alias in Typesense
|
|
44
|
+
* Aliases are upserted in Typesense
|
|
45
|
+
*/
|
|
46
|
+
export async function upsertAlias(config: AliasConfig): Promise<void> {
|
|
47
|
+
const client = getClient();
|
|
48
|
+
await client.aliases().upsert(config.name, {
|
|
49
|
+
collection_name: config.collection,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Delete an alias from Typesense
|
|
55
|
+
*/
|
|
56
|
+
export async function deleteAlias(name: string): Promise<void> {
|
|
57
|
+
const client = getClient();
|
|
58
|
+
await client.aliases(name).delete();
|
|
59
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { getClient } from "../client/index.js";
|
|
2
|
+
import type { AnalyticsRuleConfig } from "../types/index.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get an analytics rule from Typesense
|
|
6
|
+
*/
|
|
7
|
+
export async function getAnalyticsRule(
|
|
8
|
+
name: string
|
|
9
|
+
): Promise<AnalyticsRuleConfig | null> {
|
|
10
|
+
const client = getClient();
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const data = await client.analytics.rules(name).retrieve();
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
name: data.name,
|
|
17
|
+
type: data.type as AnalyticsRuleConfig["type"],
|
|
18
|
+
collection: data.collection,
|
|
19
|
+
event_type: data.event_type as AnalyticsRuleConfig["event_type"],
|
|
20
|
+
rule_tag: data.rule_tag,
|
|
21
|
+
params: data.params,
|
|
22
|
+
};
|
|
23
|
+
} catch (error: unknown) {
|
|
24
|
+
if (
|
|
25
|
+
error &&
|
|
26
|
+
typeof error === "object" &&
|
|
27
|
+
"httpStatus" in error &&
|
|
28
|
+
(error.httpStatus === 404 || error.httpStatus === 400)
|
|
29
|
+
) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* List all analytics rules from Typesense
|
|
38
|
+
*/
|
|
39
|
+
export async function listAnalyticsRules(): Promise<AnalyticsRuleConfig[]> {
|
|
40
|
+
const client = getClient();
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const response = await client.analytics.rules().retrieve();
|
|
44
|
+
const rules = response.rules || [];
|
|
45
|
+
|
|
46
|
+
return rules.map((rule) => {
|
|
47
|
+
const config: AnalyticsRuleConfig = {
|
|
48
|
+
name: rule.name,
|
|
49
|
+
type: rule.type as AnalyticsRuleConfig["type"],
|
|
50
|
+
collection: rule.collection,
|
|
51
|
+
event_type: rule.event_type as AnalyticsRuleConfig["event_type"],
|
|
52
|
+
};
|
|
53
|
+
if (rule.rule_tag) config.rule_tag = rule.rule_tag;
|
|
54
|
+
if (rule.params) config.params = rule.params;
|
|
55
|
+
return config;
|
|
56
|
+
});
|
|
57
|
+
} catch (error: unknown) {
|
|
58
|
+
// If analytics feature isn't available, return empty array
|
|
59
|
+
if (
|
|
60
|
+
error &&
|
|
61
|
+
typeof error === "object" &&
|
|
62
|
+
"httpStatus" in error &&
|
|
63
|
+
(error.httpStatus === 404 || error.httpStatus === 400)
|
|
64
|
+
) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Create an analytics rule in Typesense
|
|
73
|
+
*/
|
|
74
|
+
export async function createAnalyticsRule(
|
|
75
|
+
config: AnalyticsRuleConfig
|
|
76
|
+
): Promise<void> {
|
|
77
|
+
const client = getClient();
|
|
78
|
+
|
|
79
|
+
await client.analytics.rules().upsert(config.name, {
|
|
80
|
+
type: config.type,
|
|
81
|
+
collection: config.collection,
|
|
82
|
+
event_type: config.event_type,
|
|
83
|
+
rule_tag: config.rule_tag,
|
|
84
|
+
params: config.params,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Update an analytics rule in Typesense
|
|
90
|
+
*/
|
|
91
|
+
export async function updateAnalyticsRule(
|
|
92
|
+
config: AnalyticsRuleConfig
|
|
93
|
+
): Promise<void> {
|
|
94
|
+
const client = getClient();
|
|
95
|
+
|
|
96
|
+
await client.analytics.rules().upsert(config.name, {
|
|
97
|
+
type: config.type,
|
|
98
|
+
collection: config.collection,
|
|
99
|
+
event_type: config.event_type,
|
|
100
|
+
rule_tag: config.rule_tag,
|
|
101
|
+
params: config.params,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Delete an analytics rule from Typesense
|
|
107
|
+
*/
|
|
108
|
+
export async function deleteAnalyticsRule(name: string): Promise<void> {
|
|
109
|
+
const client = getClient();
|
|
110
|
+
await client.analytics.rules(name).delete();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Compare two analytics rule configs for equality
|
|
115
|
+
*/
|
|
116
|
+
export function analyticsRuleConfigsEqual(
|
|
117
|
+
a: AnalyticsRuleConfig,
|
|
118
|
+
b: AnalyticsRuleConfig
|
|
119
|
+
): boolean {
|
|
120
|
+
// Normalize by removing undefined values
|
|
121
|
+
const normalize = (config: AnalyticsRuleConfig) => {
|
|
122
|
+
const result: Record<string, unknown> = {
|
|
123
|
+
name: config.name,
|
|
124
|
+
type: config.type,
|
|
125
|
+
collection: config.collection,
|
|
126
|
+
event_type: config.event_type,
|
|
127
|
+
};
|
|
128
|
+
if (config.rule_tag !== undefined) result.rule_tag = config.rule_tag;
|
|
129
|
+
if (config.params !== undefined) result.params = config.params;
|
|
130
|
+
return result;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return JSON.stringify(normalize(a)) === JSON.stringify(normalize(b));
|
|
134
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { getClient } from "../client/index.js";
|
|
2
|
+
import type { ApiKeyConfig } from "../types/index.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Stored API key with its Typesense ID
|
|
6
|
+
* We need to track the ID to be able to delete keys later
|
|
7
|
+
*/
|
|
8
|
+
export interface StoredApiKey extends ApiKeyConfig {
|
|
9
|
+
id: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Convert our ApiKeyConfig to Typesense API format for creation
|
|
14
|
+
*/
|
|
15
|
+
function toTypesenseApiKey(config: ApiKeyConfig): {
|
|
16
|
+
description: string;
|
|
17
|
+
actions: string[];
|
|
18
|
+
collections: string[];
|
|
19
|
+
value?: string;
|
|
20
|
+
expires_at?: number;
|
|
21
|
+
autodelete?: boolean;
|
|
22
|
+
} {
|
|
23
|
+
const result: {
|
|
24
|
+
description: string;
|
|
25
|
+
actions: string[];
|
|
26
|
+
collections: string[];
|
|
27
|
+
value?: string;
|
|
28
|
+
expires_at?: number;
|
|
29
|
+
autodelete?: boolean;
|
|
30
|
+
} = {
|
|
31
|
+
description: config.description,
|
|
32
|
+
actions: config.actions,
|
|
33
|
+
collections: config.collections,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
if (config.value) result.value = config.value;
|
|
37
|
+
if (config.expires_at !== undefined) result.expires_at = config.expires_at;
|
|
38
|
+
if (config.autodelete !== undefined) result.autodelete = config.autodelete;
|
|
39
|
+
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Convert Typesense API key to our ApiKeyConfig format
|
|
45
|
+
*/
|
|
46
|
+
function fromTypesenseApiKey(apiKey: {
|
|
47
|
+
id: number;
|
|
48
|
+
description: string;
|
|
49
|
+
actions: string[];
|
|
50
|
+
collections: string[];
|
|
51
|
+
expires_at?: number;
|
|
52
|
+
}): StoredApiKey {
|
|
53
|
+
const config: StoredApiKey = {
|
|
54
|
+
id: apiKey.id,
|
|
55
|
+
description: apiKey.description,
|
|
56
|
+
actions: apiKey.actions,
|
|
57
|
+
collections: apiKey.collections,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
if (apiKey.expires_at !== undefined) config.expires_at = apiKey.expires_at;
|
|
61
|
+
|
|
62
|
+
return config;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get an API key by description from Typesense
|
|
67
|
+
* We use description as the identifier since we don't know IDs upfront
|
|
68
|
+
*/
|
|
69
|
+
export async function getApiKey(description: string): Promise<StoredApiKey | null> {
|
|
70
|
+
const client = getClient();
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const response = await client.keys().retrieve();
|
|
74
|
+
const key = response.keys.find((k) => k.description === description);
|
|
75
|
+
if (!key || !key.description) return null;
|
|
76
|
+
return fromTypesenseApiKey({
|
|
77
|
+
id: key.id,
|
|
78
|
+
description: key.description,
|
|
79
|
+
actions: key.actions,
|
|
80
|
+
collections: key.collections,
|
|
81
|
+
expires_at: key.expires_at,
|
|
82
|
+
});
|
|
83
|
+
} catch (error: unknown) {
|
|
84
|
+
if (
|
|
85
|
+
error &&
|
|
86
|
+
typeof error === "object" &&
|
|
87
|
+
"httpStatus" in error &&
|
|
88
|
+
error.httpStatus === 404
|
|
89
|
+
) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get an API key by ID from Typesense
|
|
98
|
+
*/
|
|
99
|
+
export async function getApiKeyById(id: number): Promise<StoredApiKey | null> {
|
|
100
|
+
const client = getClient();
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const key = await client.keys(id).retrieve();
|
|
104
|
+
return fromTypesenseApiKey(key as {
|
|
105
|
+
id: number;
|
|
106
|
+
description: string;
|
|
107
|
+
actions: string[];
|
|
108
|
+
collections: string[];
|
|
109
|
+
expires_at?: number;
|
|
110
|
+
autodelete?: boolean;
|
|
111
|
+
});
|
|
112
|
+
} catch (error: unknown) {
|
|
113
|
+
if (
|
|
114
|
+
error &&
|
|
115
|
+
typeof error === "object" &&
|
|
116
|
+
"httpStatus" in error &&
|
|
117
|
+
error.httpStatus === 404
|
|
118
|
+
) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* List all API keys from Typesense
|
|
127
|
+
*/
|
|
128
|
+
export async function listApiKeys(): Promise<StoredApiKey[]> {
|
|
129
|
+
const client = getClient();
|
|
130
|
+
|
|
131
|
+
const response = await client.keys().retrieve();
|
|
132
|
+
return response.keys
|
|
133
|
+
.filter((key) => key.description) // Only include keys with descriptions
|
|
134
|
+
.map((key) =>
|
|
135
|
+
fromTypesenseApiKey({
|
|
136
|
+
id: key.id,
|
|
137
|
+
description: key.description!,
|
|
138
|
+
actions: key.actions,
|
|
139
|
+
collections: key.collections,
|
|
140
|
+
expires_at: key.expires_at,
|
|
141
|
+
})
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Create an API key in Typesense
|
|
147
|
+
* Returns the created key with its ID and value (value is only shown once!)
|
|
148
|
+
*/
|
|
149
|
+
export async function createApiKey(config: ApiKeyConfig): Promise<{
|
|
150
|
+
id: number;
|
|
151
|
+
value: string;
|
|
152
|
+
}> {
|
|
153
|
+
const client = getClient();
|
|
154
|
+
const result = await client.keys().create(toTypesenseApiKey(config));
|
|
155
|
+
if (!result.value) {
|
|
156
|
+
throw new Error("API key creation did not return a value");
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
id: result.id,
|
|
160
|
+
value: result.value,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Delete an API key from Typesense by ID
|
|
166
|
+
*/
|
|
167
|
+
export async function deleteApiKey(id: number): Promise<void> {
|
|
168
|
+
const client = getClient();
|
|
169
|
+
await client.keys(id).delete();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Delete an API key by description
|
|
174
|
+
* Finds the key by description first, then deletes it
|
|
175
|
+
*/
|
|
176
|
+
export async function deleteApiKeyByDescription(description: string): Promise<void> {
|
|
177
|
+
const key = await getApiKey(description);
|
|
178
|
+
if (key) {
|
|
179
|
+
await deleteApiKey(key.id);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Compare two API key configs for equality
|
|
185
|
+
* Used to determine if an update is needed
|
|
186
|
+
*/
|
|
187
|
+
export function apiKeyConfigsEqual(a: ApiKeyConfig, b: ApiKeyConfig): boolean {
|
|
188
|
+
// Compare actions and collections as sets
|
|
189
|
+
const actionsEqual =
|
|
190
|
+
a.actions.length === b.actions.length &&
|
|
191
|
+
a.actions.every((action) => b.actions.includes(action));
|
|
192
|
+
|
|
193
|
+
const collectionsEqual =
|
|
194
|
+
a.collections.length === b.collections.length &&
|
|
195
|
+
a.collections.every((collection) => b.collections.includes(collection));
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
actionsEqual &&
|
|
199
|
+
collectionsEqual &&
|
|
200
|
+
a.expires_at === b.expires_at &&
|
|
201
|
+
a.autodelete === b.autodelete
|
|
202
|
+
);
|
|
203
|
+
}
|