@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,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
+ }