rastack 0.0.58 → 0.0.59
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/CHANGELOG.md +9 -0
- package/README.md +190 -0
- package/dist/deploy/ci.js +2 -2
- package/dist/deploy/io.js +2 -2
- package/dist/dev/harness.d.ts +1 -1
- package/dist/dev/harness.js +1 -2
- package/dist/external/cache.d.ts +28 -0
- package/dist/external/cache.js +35 -0
- package/dist/external/envelope.d.ts +35 -0
- package/dist/external/envelope.js +48 -0
- package/dist/external/graphql.d.ts +25 -0
- package/dist/external/graphql.js +78 -0
- package/dist/external/handler.d.ts +52 -0
- package/dist/external/handler.js +139 -0
- package/dist/external/index.d.ts +19 -0
- package/dist/external/index.js +38 -0
- package/dist/external/lambda.d.ts +49 -0
- package/dist/external/lambda.js +86 -0
- package/dist/external/rest.d.ts +17 -0
- package/dist/external/rest.js +46 -0
- package/dist/external/route.d.ts +12 -0
- package/dist/external/route.js +33 -0
- package/dist/external/service.d.ts +50 -0
- package/dist/external/service.js +59 -0
- package/dist/external/types.d.ts +107 -0
- package/dist/external/types.js +22 -0
- package/dist/external/util.d.ts +16 -0
- package/dist/external/util.js +58 -0
- package/dist/rastack-dev.js +10 -0
- package/dist/rastack-wasm-build.d.ts +1 -1
- package/dist/rastack-wasm-build.js +6 -6
- package/dist/wasm/rastack_wasm_bg.wasm +0 -0
- package/external.ts +10 -0
- package/hooks/query/delete.ts +12 -11
- package/hooks/query/fetch.ts +6 -8
- package/hooks/query/interfaces.ts +1 -4
- package/hooks/query/list.ts +16 -9
- package/hooks/query/update.ts +8 -6
- package/package.json +24 -2
- package/scripts/copy-templates.js +1 -1
- package/scripts/copy-wasm.js +1 -1
- package/scripts/dev-install.mjs +63 -0
- package/src/deploy/ci.ts +2 -2
- package/src/deploy/io.ts +2 -2
- package/src/dev/harness.ts +1 -2
- package/src/external/cache.ts +53 -0
- package/src/external/envelope.ts +78 -0
- package/src/external/graphql.ts +98 -0
- package/src/external/handler.ts +207 -0
- package/src/external/index.ts +25 -0
- package/src/external/lambda.ts +108 -0
- package/src/external/rest.ts +60 -0
- package/src/external/route.ts +33 -0
- package/src/external/service.ts +86 -0
- package/src/external/types.ts +121 -0
- package/src/external/util.ts +57 -0
- package/src/rastack-dev.ts +11 -0
- package/src/rastack-wasm-build.ts +6 -6
- package/jest.config.cjs +0 -12
- package/test/auth.spec.ts +0 -207
- package/test/cache.spec.ts +0 -263
- package/test/cognito.spec.ts +0 -281
- package/test/compile.spec.ts +0 -240
- package/test/components.spec.ts +0 -524
- package/test/csv-schema.spec.ts +0 -143
- package/test/deploy.spec.ts +0 -571
- package/test/dev.spec.ts +0 -414
- package/test/entities.spec.ts +0 -597
- package/test/import.spec.ts +0 -241
- package/test/plugin.spec.ts +0 -315
- package/test/runtime-manifest.spec.ts +0 -309
- package/test/schema-entities.spec.ts +0 -688
- package/test/tokens.spec.ts +0 -302
- package/test/transitions.spec.ts +0 -372
- package/test/typed-hooks.spec.ts +0 -412
- package/test/update.spec.ts +0 -152
- package/test/validate.spec.ts +0 -319
- package/tsconfig.json +0 -27
- package/wasm/package.json +0 -4
- package/wasm/rastack_wasm.d.ts +0 -100
- package/wasm/rastack_wasm.js +0 -745
- package/wasm/rastack_wasm_bg.wasm +0 -0
- package/wasm/rastack_wasm_bg.wasm.d.ts +0 -28
package/test/dev.spec.ts
DELETED
|
@@ -1,414 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ADMIN_DEFAULTS,
|
|
3
|
-
APP_ENTRY,
|
|
4
|
-
DEV_DEFAULTS,
|
|
5
|
-
WASM_ENTRY,
|
|
6
|
-
appRootCandidates,
|
|
7
|
-
contentType,
|
|
8
|
-
parseAdminArgs,
|
|
9
|
-
parseDevArgs,
|
|
10
|
-
renderShell,
|
|
11
|
-
renderWelcome,
|
|
12
|
-
wasmBundleCandidates,
|
|
13
|
-
} from "../src/dev/harness";
|
|
14
|
-
import {
|
|
15
|
-
boolFilterOptions,
|
|
16
|
-
buildWriteBody,
|
|
17
|
-
changelistHeaders,
|
|
18
|
-
coerceFormValue,
|
|
19
|
-
distinctValueOptions,
|
|
20
|
-
errorDetail,
|
|
21
|
-
fieldControlType,
|
|
22
|
-
filterableFields,
|
|
23
|
-
findResource,
|
|
24
|
-
fkFilterOptions,
|
|
25
|
-
formFields,
|
|
26
|
-
initialFormValues,
|
|
27
|
-
listPath,
|
|
28
|
-
detailPath,
|
|
29
|
-
optionLabel,
|
|
30
|
-
optionsFromRows,
|
|
31
|
-
pageCount,
|
|
32
|
-
parseManifest,
|
|
33
|
-
readListEnvelope,
|
|
34
|
-
withFilters,
|
|
35
|
-
PAGE_SIZE,
|
|
36
|
-
type AdminResource,
|
|
37
|
-
} from "../admin/adapters";
|
|
38
|
-
|
|
39
|
-
// -- harness core -----------------------------------------------------------
|
|
40
|
-
|
|
41
|
-
describe("rastack dev — arg parsing", () => {
|
|
42
|
-
it("defaults resources/out/warehouse/port and opens the browser", () => {
|
|
43
|
-
expect(parseDevArgs([])).toEqual(DEV_DEFAULTS);
|
|
44
|
-
expect(DEV_DEFAULTS.port).toBe(4321);
|
|
45
|
-
expect(DEV_DEFAULTS.warehouseDir).toBe("data/warehouse");
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("reads --port, --warehouse, --out, --no-open and a positional dir", () => {
|
|
49
|
-
const args = parseDevArgs([
|
|
50
|
-
"app/resources",
|
|
51
|
-
"--port",
|
|
52
|
-
"5000",
|
|
53
|
-
"--warehouse",
|
|
54
|
-
"data/wh",
|
|
55
|
-
"--out",
|
|
56
|
-
"build",
|
|
57
|
-
"--no-open",
|
|
58
|
-
]);
|
|
59
|
-
expect(args).toEqual({
|
|
60
|
-
resourcesDir: "app/resources",
|
|
61
|
-
outDir: "build",
|
|
62
|
-
warehouseDir: "data/wh",
|
|
63
|
-
appDir: "",
|
|
64
|
-
port: 5000,
|
|
65
|
-
open: false,
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("ignores a non-numeric --port (keeps the default)", () => {
|
|
70
|
-
expect(parseDevArgs(["--port", "abc"]).port).toBe(4321);
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
describe("rastack admin — arg parsing", () => {
|
|
75
|
-
it("defaults to the server's port 8000 and the same dirs as dev", () => {
|
|
76
|
-
expect(parseAdminArgs([])).toEqual(ADMIN_DEFAULTS);
|
|
77
|
-
expect(ADMIN_DEFAULTS.port).toBe(8000);
|
|
78
|
-
expect(ADMIN_DEFAULTS.warehouseDir).toBe("data/warehouse");
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("reads --port, --warehouse, --out, --no-open and a positional dir", () => {
|
|
82
|
-
expect(
|
|
83
|
-
parseAdminArgs(["res", "--port", "9100", "--warehouse", "wh", "--no-open"]),
|
|
84
|
-
).toEqual({
|
|
85
|
-
resourcesDir: "res",
|
|
86
|
-
outDir: ".rastack",
|
|
87
|
-
warehouseDir: "wh",
|
|
88
|
-
port: 9100,
|
|
89
|
-
open: false,
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
describe("rastack dev — content types", () => {
|
|
95
|
-
it("maps wasm to application/wasm (required for streaming instantiation)", () => {
|
|
96
|
-
expect(contentType("/wasm/rastack_wasm_bg.wasm")).toBe("application/wasm");
|
|
97
|
-
});
|
|
98
|
-
it("maps the common asset types", () => {
|
|
99
|
-
expect(contentType("/dev/admin.js")).toBe("text/javascript; charset=utf-8");
|
|
100
|
-
expect(contentType("/schema.rastack.json")).toBe(
|
|
101
|
-
"application/json; charset=utf-8",
|
|
102
|
-
);
|
|
103
|
-
expect(contentType("a/version-hint.text")).toBe("text/plain; charset=utf-8");
|
|
104
|
-
expect(contentType("data/v3.parquet")).toBe("application/octet-stream");
|
|
105
|
-
});
|
|
106
|
-
it("falls back to octet-stream for unknown extensions", () => {
|
|
107
|
-
expect(contentType("mystery.bin")).toBe("application/octet-stream");
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
describe("rastack dev — wasm bundle resolution", () => {
|
|
112
|
-
it("includes the installed dist/wasm and repo tools/wasm locations", () => {
|
|
113
|
-
const dirs = wasmBundleCandidates("/opt/rastack/dist", "/repo");
|
|
114
|
-
expect(dirs).toContain("/opt/rastack/dist/wasm");
|
|
115
|
-
expect(dirs).toContain("/repo/tools/wasm");
|
|
116
|
-
// The entry filename the resolver probes for.
|
|
117
|
-
expect(WASM_ENTRY).toBe("rastack_wasm.js");
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
describe("rastack admin — HTML shell", () => {
|
|
122
|
-
it("mounts #root and loads the bundled admin", () => {
|
|
123
|
-
const html = renderShell();
|
|
124
|
-
expect(html).toContain('<div id="root">');
|
|
125
|
-
expect(html).toContain('<script src="/dev/admin.js">');
|
|
126
|
-
expect(html.startsWith("<!doctype html>")).toBe(true);
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
describe("rastack dev — app resolution", () => {
|
|
131
|
-
it("defaults appDir to empty (probe the conventional roots)", () => {
|
|
132
|
-
expect(DEV_DEFAULTS.appDir).toBe("");
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it("reads --app as an explicit app root", () => {
|
|
136
|
-
expect(parseDevArgs(["--app", "site"]).appDir).toBe("site");
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it("probes public/www/app/root when no --app is given", () => {
|
|
140
|
-
const dirs = appRootCandidates("", "/repo");
|
|
141
|
-
expect(dirs).toEqual([
|
|
142
|
-
"/repo/public",
|
|
143
|
-
"/repo/www",
|
|
144
|
-
"/repo/app",
|
|
145
|
-
"/repo",
|
|
146
|
-
]);
|
|
147
|
-
expect(APP_ENTRY).toBe("index.html");
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it("uses only the explicit --app dir when set", () => {
|
|
151
|
-
expect(appRootCandidates("site", "/repo")).toEqual(["/repo/site"]);
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
describe("rastack dev — welcome page (no app)", () => {
|
|
156
|
-
it("is a self-contained page pointing at the backend and admin", () => {
|
|
157
|
-
const html = renderWelcome();
|
|
158
|
-
expect(html.startsWith("<!doctype html>")).toBe(true);
|
|
159
|
-
// Django-style "it worked" confirmation, not the admin bundle.
|
|
160
|
-
expect(html).not.toContain("/dev/admin.js");
|
|
161
|
-
expect(html).toContain("dev server is running");
|
|
162
|
-
expect(html).toContain("/schema.rastack.json");
|
|
163
|
-
expect(html).toContain("rastack admin");
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
// -- admin adapters ---------------------------------------------------------
|
|
168
|
-
|
|
169
|
-
/** The in-repo airline fixtures, in manifest JSON shape. */
|
|
170
|
-
const MANIFEST = {
|
|
171
|
-
version: 1,
|
|
172
|
-
resources: [
|
|
173
|
-
{
|
|
174
|
-
app: "airports",
|
|
175
|
-
model: "airport",
|
|
176
|
-
fields: [
|
|
177
|
-
{ name: "code", type: "string", maxLength: 3, unique: true },
|
|
178
|
-
{ name: "name", type: "string", maxLength: 120 },
|
|
179
|
-
],
|
|
180
|
-
search: ["code", "name"],
|
|
181
|
-
admin: { listDisplay: ["code", "name"] },
|
|
182
|
-
},
|
|
183
|
-
{
|
|
184
|
-
app: "airports",
|
|
185
|
-
model: "terminal",
|
|
186
|
-
fields: [
|
|
187
|
-
{
|
|
188
|
-
name: "airport",
|
|
189
|
-
type: "fk",
|
|
190
|
-
relation: { app: "airports", model: "airport" },
|
|
191
|
-
},
|
|
192
|
-
{ name: "label", type: "string", maxLength: 20 },
|
|
193
|
-
{ name: "active", type: "bool", null: true },
|
|
194
|
-
],
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
app: "airports",
|
|
198
|
-
model: "secret",
|
|
199
|
-
fields: [{ name: "value", type: "string" }],
|
|
200
|
-
admin: false,
|
|
201
|
-
},
|
|
202
|
-
],
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
describe("admin adapters — manifest parsing", () => {
|
|
206
|
-
const resources = parseManifest(MANIFEST);
|
|
207
|
-
|
|
208
|
-
it("parses resources, fields, search, listDisplay and admin visibility", () => {
|
|
209
|
-
const airport = findResource(resources, "airports", "airport")!;
|
|
210
|
-
expect(airport.key).toBe("airports.airport");
|
|
211
|
-
expect(airport.label).toBe("Airport");
|
|
212
|
-
expect(airport.search).toEqual(["code", "name"]);
|
|
213
|
-
expect(airport.listDisplay).toEqual(["code", "name"]);
|
|
214
|
-
expect(airport.adminEnabled).toBe(true);
|
|
215
|
-
|
|
216
|
-
const secret = findResource(resources, "airports", "secret")!;
|
|
217
|
-
expect(secret.adminEnabled).toBe(false);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it("accepts a JSON string as well as an object", () => {
|
|
221
|
-
expect(parseManifest(JSON.stringify(MANIFEST)).length).toBe(3);
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it("carries FK relation + nullability on fields", () => {
|
|
225
|
-
const terminal = findResource(resources, "airports", "terminal")!;
|
|
226
|
-
const fk = terminal.fields.find((f) => f.name === "airport")!;
|
|
227
|
-
expect(fk.type).toBe("fk");
|
|
228
|
-
expect(fk.relation).toEqual({ app: "airports", model: "airport" });
|
|
229
|
-
const active = terminal.fields.find((f) => f.name === "active")!;
|
|
230
|
-
expect(active.nullable).toBe(true);
|
|
231
|
-
});
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
describe("admin adapters — changelist", () => {
|
|
235
|
-
const resources = parseManifest(MANIFEST);
|
|
236
|
-
const airport = findResource(resources, "airports", "airport")!;
|
|
237
|
-
const terminal = findResource(resources, "airports", "terminal")!;
|
|
238
|
-
|
|
239
|
-
it("prepends an id column, then listDisplay (else all fields)", () => {
|
|
240
|
-
expect(changelistHeaders(airport).map((h) => h.key)).toEqual([
|
|
241
|
-
"id",
|
|
242
|
-
"code",
|
|
243
|
-
"name",
|
|
244
|
-
]);
|
|
245
|
-
// terminal has no listDisplay → all fields after id
|
|
246
|
-
expect(changelistHeaders(terminal).map((h) => h.key)).toEqual([
|
|
247
|
-
"id",
|
|
248
|
-
"airport",
|
|
249
|
-
"label",
|
|
250
|
-
"active",
|
|
251
|
-
]);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it("uses the admin's fixed page size and correct page count", () => {
|
|
255
|
-
expect(PAGE_SIZE).toBe(25);
|
|
256
|
-
expect(pageCount(0, 25)).toBe(1);
|
|
257
|
-
expect(pageCount(25, 25)).toBe(1);
|
|
258
|
-
expect(pageCount(26, 25)).toBe(2);
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
it("reads the DRF list envelope defensively", () => {
|
|
262
|
-
expect(readListEnvelope({ count: 2, results: [{ id: 1 }] })).toEqual({
|
|
263
|
-
count: 2,
|
|
264
|
-
results: [{ id: 1 }],
|
|
265
|
-
});
|
|
266
|
-
expect(readListEnvelope(undefined)).toEqual({ count: 0, results: [] });
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
it("builds the REST paths the WASM engine answers", () => {
|
|
270
|
-
expect(listPath("airports", "airport")).toBe("/api/airports/v1/airport/");
|
|
271
|
-
expect(detailPath("airports", "airport", 7)).toBe(
|
|
272
|
-
"/api/airports/v1/airport/7/",
|
|
273
|
-
);
|
|
274
|
-
});
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
describe("admin adapters — forms", () => {
|
|
278
|
-
const resources = parseManifest(MANIFEST);
|
|
279
|
-
const airport = findResource(resources, "airports", "airport")!;
|
|
280
|
-
const terminal = findResource(resources, "airports", "terminal")!;
|
|
281
|
-
|
|
282
|
-
it("chooses controls from the field datatype (FK → select)", () => {
|
|
283
|
-
const fk = terminal.fields.find((f) => f.name === "airport")!;
|
|
284
|
-
const label = terminal.fields.find((f) => f.name === "label")!;
|
|
285
|
-
const active = terminal.fields.find((f) => f.name === "active")!;
|
|
286
|
-
expect(fieldControlType(fk)).toBe("select");
|
|
287
|
-
expect(fieldControlType(label)).toBe("text");
|
|
288
|
-
expect(fieldControlType(active)).toBe("boolean");
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
it("seeds initial values (blank strings, false for booleans)", () => {
|
|
292
|
-
expect(initialFormValues(terminal)).toEqual({
|
|
293
|
-
airport: "",
|
|
294
|
-
label: "",
|
|
295
|
-
active: false,
|
|
296
|
-
});
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
it("coerces values like the Rust admin (numbers/bools parsed, empties omitted)", () => {
|
|
300
|
-
const fk = terminal.fields.find((f) => f.name === "airport")!;
|
|
301
|
-
const active = terminal.fields.find((f) => f.name === "active")!;
|
|
302
|
-
const label = terminal.fields.find((f) => f.name === "label")!;
|
|
303
|
-
expect(coerceFormValue(fk, "12")).toBe(12);
|
|
304
|
-
expect(coerceFormValue(active, "true")).toBe(true);
|
|
305
|
-
expect(coerceFormValue(active, false)).toBe(false);
|
|
306
|
-
expect(coerceFormValue(label, "")).toBeUndefined(); // omitted
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
it("builds a write body, omitting empty non-bool fields", () => {
|
|
310
|
-
const body = buildWriteBody(terminal, {
|
|
311
|
-
airport: "3",
|
|
312
|
-
label: "",
|
|
313
|
-
active: true,
|
|
314
|
-
});
|
|
315
|
-
expect(body).toEqual({ airport: 3, active: true });
|
|
316
|
-
expect("label" in body).toBe(false);
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
it("labels FK options by the first string field, else #id", () => {
|
|
320
|
-
expect(optionLabel({ id: 5, code: "JFK", name: "Kennedy" })).toBe(
|
|
321
|
-
"JFK (#5)",
|
|
322
|
-
);
|
|
323
|
-
expect(optionLabel({ id: 9 })).toBe("#9");
|
|
324
|
-
expect(optionsFromRows([{ id: 1, code: "LAX" }])).toEqual([
|
|
325
|
-
{ label: "LAX (#1)", value: 1 },
|
|
326
|
-
]);
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
it("form fields exclude the primary key", () => {
|
|
330
|
-
const withPk: AdminResource = {
|
|
331
|
-
...airport,
|
|
332
|
-
fields: [
|
|
333
|
-
{ name: "id", type: "int", primaryKey: true, nullable: false },
|
|
334
|
-
...airport.fields,
|
|
335
|
-
],
|
|
336
|
-
};
|
|
337
|
-
expect(formFields(withPk).map((f) => f.name)).toEqual(["code", "name"]);
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
it("extracts a {detail} error message", () => {
|
|
341
|
-
expect(errorDetail({ detail: "nope" })).toBe("nope");
|
|
342
|
-
expect(errorDetail("raw string")).toBe("raw string");
|
|
343
|
-
expect(errorDetail({})).toBe("Request failed");
|
|
344
|
-
});
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
describe("admin adapters — Linear-style filters", () => {
|
|
348
|
-
const resources = parseManifest(MANIFEST);
|
|
349
|
-
const airport = findResource(resources, "airports", "airport")!;
|
|
350
|
-
const terminal = findResource(resources, "airports", "terminal")!;
|
|
351
|
-
|
|
352
|
-
it("offers FK, boolean and non-unique string fields as facets", () => {
|
|
353
|
-
// `airport`: `code` is unique (an identifier, not a facet), `name` is a
|
|
354
|
-
// plain string → a single `choice` facet.
|
|
355
|
-
expect(filterableFields(airport)).toEqual([
|
|
356
|
-
{ name: "name", label: "Name", kind: "choice" },
|
|
357
|
-
]);
|
|
358
|
-
// `terminal`: FK + string + bool all filterable.
|
|
359
|
-
expect(filterableFields(terminal)).toEqual([
|
|
360
|
-
{
|
|
361
|
-
name: "airport",
|
|
362
|
-
label: "Airport",
|
|
363
|
-
kind: "fk",
|
|
364
|
-
relation: { app: "airports", model: "airport" },
|
|
365
|
-
},
|
|
366
|
-
{ name: "label", label: "Label", kind: "choice" },
|
|
367
|
-
{ name: "active", label: "Active", kind: "bool" },
|
|
368
|
-
]);
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
it("booleans are True/False", () => {
|
|
372
|
-
expect(boolFilterOptions()).toEqual([
|
|
373
|
-
{ value: "true", label: "True" },
|
|
374
|
-
{ value: "false", label: "False" },
|
|
375
|
-
]);
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
it("distinct values become sorted options, dropping empties", () => {
|
|
379
|
-
const rows = [
|
|
380
|
-
{ label: "B" },
|
|
381
|
-
{ label: "A" },
|
|
382
|
-
{ label: "B" },
|
|
383
|
-
{ label: "" },
|
|
384
|
-
{ label: null },
|
|
385
|
-
];
|
|
386
|
-
expect(distinctValueOptions(rows, "label")).toEqual([
|
|
387
|
-
{ value: "A", label: "A" },
|
|
388
|
-
{ value: "B", label: "B" },
|
|
389
|
-
]);
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
it("drops a high-cardinality field (no useful facet)", () => {
|
|
393
|
-
const rows = Array.from({ length: 5 }, (_, i) => ({ v: `x${i}` }));
|
|
394
|
-
expect(distinctValueOptions(rows, "v", 3)).toEqual([]);
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
it("FK options reuse the option-label heuristic against the target rows", () => {
|
|
398
|
-
expect(
|
|
399
|
-
fkFilterOptions([
|
|
400
|
-
{ id: 1, code: "JFK" },
|
|
401
|
-
{ id: 2, code: "LHR" },
|
|
402
|
-
]),
|
|
403
|
-
).toEqual([
|
|
404
|
-
{ value: "1", label: "JFK (#1)" },
|
|
405
|
-
{ value: "2", label: "LHR (#2)" },
|
|
406
|
-
]);
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
it("merges filters under the reserved keys (page/search always win)", () => {
|
|
410
|
-
expect(
|
|
411
|
-
withFilters({ page: "2", search: "x" }, { status: "active", page: "9" }),
|
|
412
|
-
).toEqual({ status: "active", page: "2", search: "x" });
|
|
413
|
-
});
|
|
414
|
-
});
|