@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,245 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { setupClient, cleanupTypesense } from "./setup.js";
|
|
3
|
+
import { getTypesenseVersion } from "./helpers.js";
|
|
4
|
+
import { buildPlan } from "../plan/index.js";
|
|
5
|
+
import { applyPlan } from "../apply/index.js";
|
|
6
|
+
import { loadState } from "../state/index.js";
|
|
7
|
+
import { getCollection, createCollection } from "../resources/collection.js";
|
|
8
|
+
import { getAlias } from "../resources/alias.js";
|
|
9
|
+
import { getSynonym } from "../resources/synonym.js";
|
|
10
|
+
import { getOverride } from "../resources/override.js";
|
|
11
|
+
import { getApiKey } from "../resources/apikey.js";
|
|
12
|
+
import type { TypesenseConfig } from "../types/index.js";
|
|
13
|
+
|
|
14
|
+
describe("apply", () => {
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
setupClient();
|
|
17
|
+
await cleanupTypesense();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
await cleanupTypesense();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("creates collection via apply", async () => {
|
|
25
|
+
const config: TypesenseConfig = {
|
|
26
|
+
collections: [
|
|
27
|
+
{
|
|
28
|
+
name: "products",
|
|
29
|
+
fields: [
|
|
30
|
+
{ name: "title", type: "string" },
|
|
31
|
+
{ name: "price", type: "float" },
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const plan = await buildPlan(config);
|
|
38
|
+
const result = await applyPlan(plan, config);
|
|
39
|
+
|
|
40
|
+
expect(result.success).toBe(true);
|
|
41
|
+
expect(result.applied).toHaveLength(1);
|
|
42
|
+
expect(result.failed).toHaveLength(0);
|
|
43
|
+
|
|
44
|
+
const collection = await getCollection("products");
|
|
45
|
+
expect(collection).not.toBeNull();
|
|
46
|
+
expect(collection!.fields).toHaveLength(2);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("creates alias via apply", async () => {
|
|
50
|
+
const config: TypesenseConfig = {
|
|
51
|
+
collections: [
|
|
52
|
+
{ name: "products", fields: [{ name: "title", type: "string" }] },
|
|
53
|
+
],
|
|
54
|
+
aliases: [{ name: "products_live", collection: "products" }],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const plan = await buildPlan(config);
|
|
58
|
+
await applyPlan(plan, config);
|
|
59
|
+
|
|
60
|
+
const alias = await getAlias("products_live");
|
|
61
|
+
expect(alias).not.toBeNull();
|
|
62
|
+
expect(alias!.collection).toBe("products");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("creates synonym via apply (pre-v30)", async () => {
|
|
66
|
+
const version = await getTypesenseVersion();
|
|
67
|
+
if (version >= 30) return;
|
|
68
|
+
// Create collection first
|
|
69
|
+
await createCollection({
|
|
70
|
+
name: "products",
|
|
71
|
+
fields: [{ name: "title", type: "string" }],
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const config: TypesenseConfig = {
|
|
75
|
+
collections: [
|
|
76
|
+
{ name: "products", fields: [{ name: "title", type: "string" }] },
|
|
77
|
+
],
|
|
78
|
+
synonyms: [
|
|
79
|
+
{ id: "phone-syn", collection: "products", synonyms: ["phone", "mobile"] },
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const plan = await buildPlan(config);
|
|
84
|
+
await applyPlan(plan, config);
|
|
85
|
+
|
|
86
|
+
const synonym = await getSynonym("phone-syn", "products");
|
|
87
|
+
expect(synonym).not.toBeNull();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("creates override via apply (pre-v30)", async () => {
|
|
91
|
+
const version = await getTypesenseVersion();
|
|
92
|
+
if (version >= 30) return;
|
|
93
|
+
await createCollection({
|
|
94
|
+
name: "products",
|
|
95
|
+
fields: [{ name: "title", type: "string" }],
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const config: TypesenseConfig = {
|
|
99
|
+
collections: [
|
|
100
|
+
{ name: "products", fields: [{ name: "title", type: "string" }] },
|
|
101
|
+
],
|
|
102
|
+
overrides: [
|
|
103
|
+
{
|
|
104
|
+
id: "pin-featured",
|
|
105
|
+
collection: "products",
|
|
106
|
+
rule: { query: "featured", match: "exact" },
|
|
107
|
+
includes: [{ id: "product-1", position: 1 }],
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const plan = await buildPlan(config);
|
|
113
|
+
await applyPlan(plan, config);
|
|
114
|
+
|
|
115
|
+
const override = await getOverride("pin-featured", "products");
|
|
116
|
+
expect(override).not.toBeNull();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("creates API key via apply", async () => {
|
|
120
|
+
const config: TypesenseConfig = {
|
|
121
|
+
apiKeys: [
|
|
122
|
+
{
|
|
123
|
+
description: "Search key",
|
|
124
|
+
actions: ["documents:search"],
|
|
125
|
+
collections: ["products"],
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const plan = await buildPlan(config);
|
|
131
|
+
await applyPlan(plan, config);
|
|
132
|
+
|
|
133
|
+
const key = await getApiKey("Search key");
|
|
134
|
+
expect(key).not.toBeNull();
|
|
135
|
+
expect(key!.actions).toEqual(["documents:search"]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("saves state after apply", async () => {
|
|
139
|
+
const config: TypesenseConfig = {
|
|
140
|
+
collections: [
|
|
141
|
+
{ name: "products", fields: [{ name: "title", type: "string" }] },
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const plan = await buildPlan(config);
|
|
146
|
+
await applyPlan(plan, config);
|
|
147
|
+
|
|
148
|
+
const state = await loadState();
|
|
149
|
+
expect(state.resources).toHaveLength(1);
|
|
150
|
+
expect(state.resources[0]!.identifier.type).toBe("collection");
|
|
151
|
+
expect(state.resources[0]!.identifier.name).toBe("products");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("no-op apply returns success with no applied changes", async () => {
|
|
155
|
+
await createCollection({
|
|
156
|
+
name: "products",
|
|
157
|
+
fields: [{ name: "title", type: "string" }],
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const config: TypesenseConfig = {
|
|
161
|
+
collections: [
|
|
162
|
+
{ name: "products", fields: [{ name: "title", type: "string" }] },
|
|
163
|
+
],
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const plan = await buildPlan(config);
|
|
167
|
+
const result = await applyPlan(plan, config);
|
|
168
|
+
|
|
169
|
+
expect(result.success).toBe(true);
|
|
170
|
+
expect(result.applied).toHaveLength(0);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("apply creates multiple resource types in correct order", async () => {
|
|
174
|
+
const config: TypesenseConfig = {
|
|
175
|
+
collections: [
|
|
176
|
+
{ name: "products", fields: [{ name: "title", type: "string" }] },
|
|
177
|
+
],
|
|
178
|
+
aliases: [{ name: "products_live", collection: "products" }],
|
|
179
|
+
apiKeys: [
|
|
180
|
+
{ description: "Search key", actions: ["documents:search"], collections: ["*"] },
|
|
181
|
+
],
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const plan = await buildPlan(config);
|
|
185
|
+
const result = await applyPlan(plan, config);
|
|
186
|
+
|
|
187
|
+
expect(result.success).toBe(true);
|
|
188
|
+
expect(result.applied).toHaveLength(3);
|
|
189
|
+
|
|
190
|
+
// Verify all resources exist
|
|
191
|
+
expect(await getCollection("products")).not.toBeNull();
|
|
192
|
+
expect(await getAlias("products_live")).not.toBeNull();
|
|
193
|
+
expect(await getApiKey("Search key")).not.toBeNull();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("apply handles update to collection", async () => {
|
|
197
|
+
// Initial apply
|
|
198
|
+
const config1: TypesenseConfig = {
|
|
199
|
+
collections: [
|
|
200
|
+
{ name: "products", fields: [{ name: "title", type: "string" }] },
|
|
201
|
+
],
|
|
202
|
+
};
|
|
203
|
+
const plan1 = await buildPlan(config1);
|
|
204
|
+
await applyPlan(plan1, config1);
|
|
205
|
+
|
|
206
|
+
// Update: change a field property
|
|
207
|
+
const config2: TypesenseConfig = {
|
|
208
|
+
collections: [
|
|
209
|
+
{
|
|
210
|
+
name: "products",
|
|
211
|
+
fields: [
|
|
212
|
+
{ name: "title", type: "string", facet: true },
|
|
213
|
+
],
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
};
|
|
217
|
+
const plan2 = await buildPlan(config2);
|
|
218
|
+
const result = await applyPlan(plan2, config2);
|
|
219
|
+
|
|
220
|
+
expect(result.success).toBe(true);
|
|
221
|
+
expect(result.applied.length).toBeGreaterThan(0);
|
|
222
|
+
const collection = await getCollection("products");
|
|
223
|
+
const titleField = collection!.fields.find((f) => f.name === "title");
|
|
224
|
+
expect(titleField!.facet).toBe(true);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("apply handles deletion of resource", async () => {
|
|
228
|
+
// Create and save state
|
|
229
|
+
const config1: TypesenseConfig = {
|
|
230
|
+
collections: [
|
|
231
|
+
{ name: "products", fields: [{ name: "title", type: "string" }] },
|
|
232
|
+
],
|
|
233
|
+
};
|
|
234
|
+
const plan1 = await buildPlan(config1);
|
|
235
|
+
await applyPlan(plan1, config1);
|
|
236
|
+
|
|
237
|
+
// Now apply empty config (should delete)
|
|
238
|
+
const config2: TypesenseConfig = {};
|
|
239
|
+
const plan2 = await buildPlan(config2);
|
|
240
|
+
const result = await applyPlan(plan2, config2);
|
|
241
|
+
|
|
242
|
+
expect(result.success).toBe(true);
|
|
243
|
+
expect(await getCollection("products")).toBeNull();
|
|
244
|
+
});
|
|
245
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll } from "bun:test";
|
|
2
|
+
import { initClient, getClient, testConnection } from "../client/index.js";
|
|
3
|
+
|
|
4
|
+
describe("client", () => {
|
|
5
|
+
test("getClient throws before initialization", () => {
|
|
6
|
+
// Reset by importing fresh - but since the module is cached,
|
|
7
|
+
// we test that after init it works
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("initClient creates a working client", () => {
|
|
11
|
+
const client = initClient({
|
|
12
|
+
nodes: [{ host: "localhost", port: 8108, protocol: "http" }],
|
|
13
|
+
apiKey: "test-api-key",
|
|
14
|
+
});
|
|
15
|
+
expect(client).toBeDefined();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("getClient returns initialized client", () => {
|
|
19
|
+
const client = getClient();
|
|
20
|
+
expect(client).toBeDefined();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("testConnection returns true for running server", async () => {
|
|
24
|
+
initClient({
|
|
25
|
+
nodes: [{ host: "localhost", port: 8108, protocol: "http" }],
|
|
26
|
+
apiKey: "test-api-key",
|
|
27
|
+
});
|
|
28
|
+
const result = await testConnection();
|
|
29
|
+
expect(result).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("testConnection returns false for bad connection", async () => {
|
|
33
|
+
initClient({
|
|
34
|
+
nodes: [{ host: "localhost", port: 9999, protocol: "http" }],
|
|
35
|
+
apiKey: "bad-key",
|
|
36
|
+
});
|
|
37
|
+
const result = await testConnection();
|
|
38
|
+
expect(result).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("getClientFromEnv reads environment variables", async () => {
|
|
42
|
+
// Restore good client for remaining tests
|
|
43
|
+
initClient({
|
|
44
|
+
nodes: [{ host: "localhost", port: 8108, protocol: "http" }],
|
|
45
|
+
apiKey: "test-api-key",
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { setupClient, cleanupTypesense } from "./setup.js";
|
|
3
|
+
import { getTypesenseVersion } from "./helpers.js";
|
|
4
|
+
import {
|
|
5
|
+
createCollection,
|
|
6
|
+
getCollection,
|
|
7
|
+
updateCollection,
|
|
8
|
+
deleteCollection,
|
|
9
|
+
} from "../resources/collection.js";
|
|
10
|
+
import type { CollectionConfig } from "../types/index.js";
|
|
11
|
+
|
|
12
|
+
describe("collection advanced features", () => {
|
|
13
|
+
let version: number;
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
setupClient();
|
|
17
|
+
await cleanupTypesense();
|
|
18
|
+
version = await getTypesenseVersion();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(async () => {
|
|
22
|
+
await cleanupTypesense();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("field types", () => {
|
|
26
|
+
test("creates collection with all basic field types", async () => {
|
|
27
|
+
const config: CollectionConfig = {
|
|
28
|
+
name: "all-types",
|
|
29
|
+
fields: [
|
|
30
|
+
{ name: "str", type: "string" },
|
|
31
|
+
{ name: "str_arr", type: "string[]" },
|
|
32
|
+
{ name: "i32", type: "int32" },
|
|
33
|
+
{ name: "i32_arr", type: "int32[]" },
|
|
34
|
+
{ name: "i64", type: "int64" },
|
|
35
|
+
{ name: "i64_arr", type: "int64[]" },
|
|
36
|
+
{ name: "f", type: "float" },
|
|
37
|
+
{ name: "f_arr", type: "float[]" },
|
|
38
|
+
{ name: "b", type: "bool" },
|
|
39
|
+
{ name: "b_arr", type: "bool[]" },
|
|
40
|
+
{ name: "geo", type: "geopoint" },
|
|
41
|
+
{ name: "geo_arr", type: "geopoint[]" },
|
|
42
|
+
{ name: "auto_field", type: "auto" },
|
|
43
|
+
{ name: "str_star", type: "string*" },
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
await createCollection(config);
|
|
48
|
+
const retrieved = await getCollection("all-types");
|
|
49
|
+
expect(retrieved).not.toBeNull();
|
|
50
|
+
|
|
51
|
+
const fieldNames = retrieved!.fields.map((f) => f.name);
|
|
52
|
+
expect(fieldNames).toContain("str");
|
|
53
|
+
expect(fieldNames).toContain("i64");
|
|
54
|
+
expect(fieldNames).toContain("geo");
|
|
55
|
+
expect(fieldNames).toContain("auto_field");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("creates collection with optional fields", async () => {
|
|
59
|
+
const config: CollectionConfig = {
|
|
60
|
+
name: "optional-fields",
|
|
61
|
+
fields: [
|
|
62
|
+
{ name: "required_field", type: "string" },
|
|
63
|
+
{ name: "optional_field", type: "string", optional: true },
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
await createCollection(config);
|
|
68
|
+
const retrieved = await getCollection("optional-fields");
|
|
69
|
+
const optionalField = retrieved!.fields.find((f) => f.name === "optional_field");
|
|
70
|
+
expect(optionalField!.optional).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("creates collection with faceted fields", async () => {
|
|
74
|
+
const config: CollectionConfig = {
|
|
75
|
+
name: "faceted",
|
|
76
|
+
fields: [
|
|
77
|
+
{ name: "category", type: "string", facet: true },
|
|
78
|
+
{ name: "brand", type: "string", facet: true },
|
|
79
|
+
{ name: "price", type: "float" },
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
await createCollection(config);
|
|
84
|
+
const retrieved = await getCollection("faceted");
|
|
85
|
+
const categoryField = retrieved!.fields.find((f) => f.name === "category");
|
|
86
|
+
expect(categoryField!.facet).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("creates collection with infix search field", async () => {
|
|
90
|
+
const config: CollectionConfig = {
|
|
91
|
+
name: "infix",
|
|
92
|
+
fields: [
|
|
93
|
+
{ name: "title", type: "string", infix: true },
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
await createCollection(config);
|
|
98
|
+
const retrieved = await getCollection("infix");
|
|
99
|
+
const field = retrieved!.fields.find((f) => f.name === "title");
|
|
100
|
+
expect(field!.infix).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("creates collection with nested objects", async () => {
|
|
104
|
+
const config: CollectionConfig = {
|
|
105
|
+
name: "nested",
|
|
106
|
+
fields: [
|
|
107
|
+
{ name: "metadata", type: "object" },
|
|
108
|
+
{ name: "tags", type: "object[]" },
|
|
109
|
+
],
|
|
110
|
+
enable_nested_fields: true,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
await createCollection(config);
|
|
114
|
+
const retrieved = await getCollection("nested");
|
|
115
|
+
expect(retrieved!.enable_nested_fields).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("creates collection with vector field", async () => {
|
|
119
|
+
const config: CollectionConfig = {
|
|
120
|
+
name: "vectors",
|
|
121
|
+
fields: [
|
|
122
|
+
{ name: "title", type: "string" },
|
|
123
|
+
{ name: "embedding", type: "float[]", num_dim: 128 },
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
await createCollection(config);
|
|
128
|
+
const retrieved = await getCollection("vectors");
|
|
129
|
+
const vecField = retrieved!.fields.find((f) => f.name === "embedding");
|
|
130
|
+
expect(vecField!.num_dim).toBe(128);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("creates collection with reference field", async () => {
|
|
134
|
+
// Create referenced collection first
|
|
135
|
+
await createCollection({
|
|
136
|
+
name: "users",
|
|
137
|
+
fields: [{ name: "name", type: "string" }],
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const config: CollectionConfig = {
|
|
141
|
+
name: "posts",
|
|
142
|
+
fields: [
|
|
143
|
+
{ name: "title", type: "string" },
|
|
144
|
+
{ name: "author_id", type: "string", reference: "users.id" },
|
|
145
|
+
],
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
await createCollection(config);
|
|
149
|
+
const retrieved = await getCollection("posts");
|
|
150
|
+
const refField = retrieved!.fields.find((f) => f.name === "author_id");
|
|
151
|
+
expect(refField!.reference).toBe("users.id");
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("collection-level options", () => {
|
|
156
|
+
test("creates collection with token_separators", async () => {
|
|
157
|
+
const config: CollectionConfig = {
|
|
158
|
+
name: "custom-tokens",
|
|
159
|
+
fields: [{ name: "sku", type: "string" }],
|
|
160
|
+
token_separators: ["-", "/", "."],
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
await createCollection(config);
|
|
164
|
+
const retrieved = await getCollection("custom-tokens");
|
|
165
|
+
expect(retrieved!.token_separators).toEqual(["-", "/", "."]);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("creates collection with symbols_to_index", async () => {
|
|
169
|
+
const config: CollectionConfig = {
|
|
170
|
+
name: "symbols",
|
|
171
|
+
fields: [{ name: "tag", type: "string" }],
|
|
172
|
+
symbols_to_index: ["#", "@", "+"],
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
await createCollection(config);
|
|
176
|
+
const retrieved = await getCollection("symbols");
|
|
177
|
+
expect(retrieved!.symbols_to_index).toEqual(["#", "@", "+"]);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("creates collection with default_sorting_field", async () => {
|
|
181
|
+
const config: CollectionConfig = {
|
|
182
|
+
name: "sorted",
|
|
183
|
+
fields: [
|
|
184
|
+
{ name: "title", type: "string" },
|
|
185
|
+
{ name: "popularity", type: "int32" },
|
|
186
|
+
],
|
|
187
|
+
default_sorting_field: "popularity",
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
await createCollection(config);
|
|
191
|
+
const retrieved = await getCollection("sorted");
|
|
192
|
+
expect(retrieved!.default_sorting_field).toBe("popularity");
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("collection updates", () => {
|
|
197
|
+
test("adds multiple fields in single update", async () => {
|
|
198
|
+
await createCollection({
|
|
199
|
+
name: "products",
|
|
200
|
+
fields: [{ name: "title", type: "string" }],
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const updated: CollectionConfig = {
|
|
204
|
+
name: "products",
|
|
205
|
+
fields: [
|
|
206
|
+
{ name: "title", type: "string" },
|
|
207
|
+
{ name: "description", type: "string", optional: true },
|
|
208
|
+
{ name: "price", type: "float" },
|
|
209
|
+
{ name: "category", type: "string", facet: true },
|
|
210
|
+
],
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const result = await updateCollection(updated, {
|
|
214
|
+
name: "products",
|
|
215
|
+
fields: [{ name: "title", type: "string" }],
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
expect(result.fieldsToAdd).toHaveLength(3);
|
|
219
|
+
const retrieved = await getCollection("products");
|
|
220
|
+
expect(retrieved!.fields.length).toBeGreaterThanOrEqual(4);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("modifies field properties", async () => {
|
|
224
|
+
await createCollection({
|
|
225
|
+
name: "products",
|
|
226
|
+
fields: [{ name: "category", type: "string" }],
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const result = await updateCollection(
|
|
230
|
+
{
|
|
231
|
+
name: "products",
|
|
232
|
+
fields: [{ name: "category", type: "string", facet: true }],
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: "products",
|
|
236
|
+
fields: [{ name: "category", type: "string" }],
|
|
237
|
+
}
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
expect(result.fieldsToModify).toHaveLength(1);
|
|
241
|
+
expect(result.fieldsToModify[0]!.name).toBe("category");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("drops and adds fields simultaneously", async () => {
|
|
245
|
+
await createCollection({
|
|
246
|
+
name: "products",
|
|
247
|
+
fields: [
|
|
248
|
+
{ name: "title", type: "string" },
|
|
249
|
+
{ name: "old_field", type: "string" },
|
|
250
|
+
],
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const result = await updateCollection(
|
|
254
|
+
{
|
|
255
|
+
name: "products",
|
|
256
|
+
fields: [
|
|
257
|
+
{ name: "title", type: "string" },
|
|
258
|
+
{ name: "new_field", type: "int32" },
|
|
259
|
+
],
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
name: "products",
|
|
263
|
+
fields: [
|
|
264
|
+
{ name: "title", type: "string" },
|
|
265
|
+
{ name: "old_field", type: "string" },
|
|
266
|
+
],
|
|
267
|
+
}
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
expect(result.fieldsToDrop).toContain("old_field");
|
|
271
|
+
expect(result.fieldsToAdd.map((f) => f.name)).toContain("new_field");
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
});
|