@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,144 @@
1
+ import { getClient } from "../client/index.js";
2
+ import type { SynonymSetConfig, SynonymSetItem } from "../types/index.js";
3
+
4
+ /**
5
+ * Get a synonym set from Typesense
6
+ */
7
+ export async function getSynonymSet(
8
+ name: string
9
+ ): Promise<SynonymSetConfig | null> {
10
+ const client = getClient();
11
+
12
+ try {
13
+ const data = await client.synonymSets(name).retrieve();
14
+ const items = data.items || [];
15
+
16
+ return {
17
+ name,
18
+ items: items.map((s) => {
19
+ const item: SynonymSetItem = {
20
+ id: s.id,
21
+ };
22
+ if (s.synonyms) item.synonyms = s.synonyms;
23
+ if (s.root) item.root = s.root;
24
+ if (s.symbols_to_index) item.symbols_to_index = s.symbols_to_index;
25
+ if (s.locale) item.locale = s.locale;
26
+ return item;
27
+ }),
28
+ };
29
+ } catch (error: unknown) {
30
+ if (
31
+ error &&
32
+ typeof error === "object" &&
33
+ "httpStatus" in error &&
34
+ error.httpStatus === 404
35
+ ) {
36
+ return null;
37
+ }
38
+ throw error;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * List all synonym sets from Typesense
44
+ */
45
+ export async function listSynonymSets(): Promise<SynonymSetConfig[]> {
46
+ const client = getClient();
47
+
48
+ try {
49
+ const sets = await client.synonymSets().retrieve();
50
+ const result: SynonymSetConfig[] = [];
51
+
52
+ for (const set of sets) {
53
+ result.push({
54
+ name: set.name,
55
+ items: (set.items || []).map((s) => {
56
+ const item: SynonymSetItem = {
57
+ id: s.id,
58
+ };
59
+ if (s.synonyms) item.synonyms = s.synonyms;
60
+ if (s.root) item.root = s.root;
61
+ if (s.symbols_to_index) item.symbols_to_index = s.symbols_to_index;
62
+ if (s.locale) item.locale = s.locale;
63
+ return item;
64
+ }),
65
+ });
66
+ }
67
+
68
+ return result;
69
+ } catch (error: unknown) {
70
+ // If synonym sets feature isn't available, return empty array
71
+ if (
72
+ error &&
73
+ typeof error === "object" &&
74
+ "httpStatus" in error &&
75
+ (error.httpStatus === 404 || error.httpStatus === 400)
76
+ ) {
77
+ return [];
78
+ }
79
+ throw error;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Create a synonym set in Typesense
85
+ */
86
+ export async function createSynonymSet(config: SynonymSetConfig): Promise<void> {
87
+ const client = getClient();
88
+
89
+ // Upsert creates the set with all items
90
+ await client.synonymSets(config.name).upsert({
91
+ items: config.items.map((item) => ({
92
+ id: item.id,
93
+ synonyms: item.synonyms || [],
94
+ root: item.root,
95
+ locale: item.locale,
96
+ symbols_to_index: item.symbols_to_index,
97
+ })),
98
+ });
99
+ }
100
+
101
+ /**
102
+ * Delete a synonym set from Typesense
103
+ */
104
+ export async function deleteSynonymSet(name: string): Promise<void> {
105
+ const client = getClient();
106
+ await client.synonymSets(name).delete();
107
+ }
108
+
109
+ /**
110
+ * Update a synonym set (upsert with new items)
111
+ */
112
+ export async function updateSynonymSet(
113
+ config: SynonymSetConfig,
114
+ _existing: SynonymSetConfig
115
+ ): Promise<void> {
116
+ const client = getClient();
117
+
118
+ // Upsert replaces all items
119
+ await client.synonymSets(config.name).upsert({
120
+ items: config.items.map((item) => ({
121
+ id: item.id,
122
+ synonyms: item.synonyms || [],
123
+ root: item.root,
124
+ locale: item.locale,
125
+ symbols_to_index: item.symbols_to_index,
126
+ })),
127
+ });
128
+ }
129
+
130
+ /**
131
+ * Compare two synonym set configs for equality
132
+ */
133
+ export function synonymSetConfigsEqual(
134
+ a: SynonymSetConfig,
135
+ b: SynonymSetConfig
136
+ ): boolean {
137
+ if (a.name !== b.name) return false;
138
+ if (a.items.length !== b.items.length) return false;
139
+
140
+ const aItems = [...a.items].sort((x, y) => x.id.localeCompare(y.id));
141
+ const bItems = [...b.items].sort((x, y) => x.id.localeCompare(y.id));
142
+
143
+ return JSON.stringify(aItems) === JSON.stringify(bItems);
144
+ }
@@ -0,0 +1,206 @@
1
+ import { createHash } from "crypto";
2
+ import { getClient } from "../client/index.js";
3
+ import type {
4
+ State,
5
+ ManagedResource,
6
+ ResourceIdentifier,
7
+ } from "../types/index.js";
8
+
9
+ const STATE_COLLECTION_NAME = "_tsctl_state";
10
+ const STATE_DOC_ID = "state";
11
+
12
+ interface StateDocument {
13
+ id: string;
14
+ state: string; // JSON stringified State
15
+ updated_at: number;
16
+ }
17
+
18
+ /**
19
+ * Ensures the state collection exists in Typesense
20
+ */
21
+ export async function ensureStateCollection(): Promise<void> {
22
+ const client = getClient();
23
+
24
+ try {
25
+ await client.collections(STATE_COLLECTION_NAME).retrieve();
26
+ } catch (error: unknown) {
27
+ if (
28
+ error &&
29
+ typeof error === "object" &&
30
+ "httpStatus" in error &&
31
+ error.httpStatus === 404
32
+ ) {
33
+ // Collection doesn't exist, create it
34
+ await client.collections().create({
35
+ name: STATE_COLLECTION_NAME,
36
+ fields: [
37
+ { name: "state", type: "string" },
38
+ { name: "updated_at", type: "int64" },
39
+ ],
40
+ });
41
+ } else {
42
+ throw error;
43
+ }
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Load the current state from Typesense
49
+ */
50
+ export async function loadState(): Promise<State> {
51
+ const client = getClient();
52
+
53
+ try {
54
+ await ensureStateCollection();
55
+
56
+ const doc = (await client
57
+ .collections(STATE_COLLECTION_NAME)
58
+ .documents(STATE_DOC_ID)
59
+ .retrieve()) as StateDocument;
60
+
61
+ return JSON.parse(doc.state) as State;
62
+ } catch (error: unknown) {
63
+ if (
64
+ error &&
65
+ typeof error === "object" &&
66
+ "httpStatus" in error &&
67
+ error.httpStatus === 404
68
+ ) {
69
+ // No state exists yet
70
+ return {
71
+ version: "1.0",
72
+ resources: [],
73
+ };
74
+ }
75
+ throw error;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Save the state to Typesense
81
+ */
82
+ export async function saveState(state: State): Promise<void> {
83
+ const client = getClient();
84
+
85
+ await ensureStateCollection();
86
+
87
+ const doc: StateDocument = {
88
+ id: STATE_DOC_ID,
89
+ state: JSON.stringify(state),
90
+ updated_at: Date.now(),
91
+ };
92
+
93
+ try {
94
+ await client
95
+ .collections(STATE_COLLECTION_NAME)
96
+ .documents()
97
+ .upsert(doc);
98
+ } catch {
99
+ // If upsert fails, try create
100
+ await client
101
+ .collections(STATE_COLLECTION_NAME)
102
+ .documents()
103
+ .create(doc);
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Compute a checksum for a resource config
109
+ */
110
+ export function computeChecksum(
111
+ config: Record<string, unknown>
112
+ ): string {
113
+ const normalized = JSON.stringify(config, Object.keys(config).sort());
114
+ return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
115
+ }
116
+
117
+ /**
118
+ * Create a resource identifier string for display
119
+ */
120
+ export function formatResourceId(identifier: ResourceIdentifier): string {
121
+ if (identifier.collection) {
122
+ return `${identifier.type}.${identifier.collection}.${identifier.name}`;
123
+ }
124
+ return `${identifier.type}.${identifier.name}`;
125
+ }
126
+
127
+ /**
128
+ * Parse a resource identifier string
129
+ */
130
+ export function parseResourceId(id: string): ResourceIdentifier {
131
+ const parts = id.split(".");
132
+ if (parts.length === 3) {
133
+ return {
134
+ type: parts[0] as ResourceIdentifier["type"],
135
+ collection: parts[1],
136
+ name: parts[2]!,
137
+ };
138
+ }
139
+ return {
140
+ type: parts[0] as ResourceIdentifier["type"],
141
+ name: parts[1]!,
142
+ };
143
+ }
144
+
145
+ /**
146
+ * Find a resource in state
147
+ */
148
+ export function findResource(
149
+ state: State,
150
+ identifier: ResourceIdentifier
151
+ ): ManagedResource | undefined {
152
+ return state.resources.find(
153
+ (r) =>
154
+ r.identifier.type === identifier.type &&
155
+ r.identifier.name === identifier.name &&
156
+ r.identifier.collection === identifier.collection
157
+ );
158
+ }
159
+
160
+ /**
161
+ * Add or update a resource in state
162
+ */
163
+ export function upsertResource(
164
+ state: State,
165
+ resource: ManagedResource
166
+ ): State {
167
+ const existingIndex = state.resources.findIndex(
168
+ (r) =>
169
+ r.identifier.type === resource.identifier.type &&
170
+ r.identifier.name === resource.identifier.name &&
171
+ r.identifier.collection === resource.identifier.collection
172
+ );
173
+
174
+ const newResources = [...state.resources];
175
+
176
+ if (existingIndex >= 0) {
177
+ newResources[existingIndex] = resource;
178
+ } else {
179
+ newResources.push(resource);
180
+ }
181
+
182
+ return {
183
+ ...state,
184
+ resources: newResources,
185
+ };
186
+ }
187
+
188
+ /**
189
+ * Remove a resource from state
190
+ */
191
+ export function removeResource(
192
+ state: State,
193
+ identifier: ResourceIdentifier
194
+ ): State {
195
+ return {
196
+ ...state,
197
+ resources: state.resources.filter(
198
+ (r) =>
199
+ !(
200
+ r.identifier.type === identifier.type &&
201
+ r.identifier.name === identifier.name &&
202
+ r.identifier.collection === identifier.collection
203
+ )
204
+ ),
205
+ };
206
+ }