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