contentbase 0.0.4 → 0.0.6
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/package.json +3 -2
- package/.mcp.json +0 -9
- package/MCP-DESCRIPTIONS-REVIEW.md +0 -122
- package/bun.lock +0 -512
- package/dist/cnotes +0 -0
- package/examples/sdlc-queries.ts +0 -161
- package/public/web-demo/index.html +0 -813
- package/scripts/examples/01-collection-setup.ts +0 -46
- package/scripts/examples/02-querying.ts +0 -67
- package/scripts/examples/03-sections.ts +0 -36
- package/scripts/examples/04-relationships.ts +0 -54
- package/scripts/examples/05-document-api.ts +0 -54
- package/scripts/examples/06-extract-sections.ts +0 -55
- package/scripts/examples/07-validation.ts +0 -46
- package/scripts/examples/08-serialization.ts +0 -51
- package/scripts/examples/lib/format.ts +0 -87
- package/scripts/examples/lib/setup.ts +0 -21
- package/scripts/examples/run-all.ts +0 -43
- package/showcases/national-parks/models.ts +0 -74
- package/showcases/national-parks/parks/acadia.mdx +0 -40
- package/showcases/national-parks/parks/yosemite.mdx +0 -44
- package/showcases/national-parks/parks/zion.mdx +0 -44
- package/showcases/national-parks/queries.ts +0 -103
- package/showcases/national-parks/trails/angels-landing.mdx +0 -19
- package/showcases/national-parks/trails/cathedral-lakes.mdx +0 -19
- package/showcases/national-parks/trails/half-dome.mdx +0 -19
- package/showcases/national-parks/trails/jordan-pond-path.mdx +0 -19
- package/showcases/national-parks/trails/mist-trail.mdx +0 -19
- package/showcases/national-parks/trails/observation-point.mdx +0 -19
- package/showcases/national-parks/trails/precipice-trail.mdx +0 -19
- package/showcases/national-parks/trails/the-narrows.mdx +0 -19
- package/showcases/node_modules/.cache/.repl_history +0 -3
- package/showcases/recipes/cuisines/chinese.mdx +0 -28
- package/showcases/recipes/cuisines/italian.mdx +0 -32
- package/showcases/recipes/cuisines/mexican.mdx +0 -28
- package/showcases/recipes/models.ts +0 -77
- package/showcases/recipes/queries.ts +0 -89
- package/showcases/recipes/recipes/chinese/egg-fried-rice.mdx +0 -43
- package/showcases/recipes/recipes/chinese/mapo-tofu.mdx +0 -47
- package/showcases/recipes/recipes/italian/bruschetta.mdx +0 -38
- package/showcases/recipes/recipes/italian/cacio-e-pepe.mdx +0 -39
- package/showcases/recipes/recipes/italian/tiramisu.mdx +0 -43
- package/showcases/recipes/recipes/mexican/chicken-tinga.mdx +0 -44
- package/showcases/recipes/recipes/mexican/guacamole.mdx +0 -39
- package/showcases/vinyl-collection/albums/bitches-brew.mdx +0 -36
- package/showcases/vinyl-collection/albums/i-put-a-spell-on-you.mdx +0 -35
- package/showcases/vinyl-collection/albums/in-rainbows.mdx +0 -35
- package/showcases/vinyl-collection/albums/kind-of-blue.mdx +0 -32
- package/showcases/vinyl-collection/albums/ok-computer.mdx +0 -37
- package/showcases/vinyl-collection/albums/wild-is-the-wind.mdx +0 -35
- package/showcases/vinyl-collection/artists/miles-davis.mdx +0 -27
- package/showcases/vinyl-collection/artists/nina-simone.mdx +0 -26
- package/showcases/vinyl-collection/artists/radiohead.mdx +0 -27
- package/showcases/vinyl-collection/models.ts +0 -73
- package/showcases/vinyl-collection/queries.ts +0 -87
- package/src/__tests__/semantic-search.integration.test.ts +0 -284
- package/test/ast-query.test.ts +0 -128
- package/test/collection.test.ts +0 -215
- package/test/define-model.test.ts +0 -78
- package/test/document.test.ts +0 -225
- package/test/extract-sections.test.ts +0 -356
- package/test/fixtures/sdlc/MODELS.md +0 -106
- package/test/fixtures/sdlc/SKILL.md +0 -404
- package/test/fixtures/sdlc/epics/authentication.mdx +0 -42
- package/test/fixtures/sdlc/epics/searching-and-browsing.mdx +0 -21
- package/test/fixtures/sdlc/index.ts +0 -9
- package/test/fixtures/sdlc/models.ts +0 -91
- package/test/fixtures/sdlc/stories/authentication/a-user-should-be-able-to-login.mdx +0 -20
- package/test/fixtures/sdlc/stories/authentication/a-user-should-be-able-to-register.mdx +0 -20
- package/test/fixtures/sdlc/templates/epic.md +0 -23
- package/test/fixtures/sdlc/templates/story.md +0 -19
- package/test/helpers.ts +0 -21
- package/test/model-instance.test.ts +0 -197
- package/test/pattern.test.ts +0 -191
- package/test/query-dsl.test.ts +0 -431
- package/test/query.test.ts +0 -196
- package/test/relationships.test.ts +0 -214
- package/test/section.test.ts +0 -160
- package/test/table-of-contents.test.ts +0 -135
- package/test/validator.test.ts +0 -62
- package/tsconfig.json +0 -18
- package/vitest.config.ts +0 -11
package/test/query-dsl.test.ts
DELETED
|
@@ -1,431 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
queryDSLSchema,
|
|
4
|
-
parseWhereClause,
|
|
5
|
-
parseSortClause,
|
|
6
|
-
executeQueryDSL,
|
|
7
|
-
} from "../src/query/query-dsl";
|
|
8
|
-
import { Collection } from "../src/collection";
|
|
9
|
-
import { createTestCollection } from "./helpers";
|
|
10
|
-
import { Epic } from "./fixtures/sdlc/models";
|
|
11
|
-
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
// Schema validation
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
|
|
16
|
-
describe("queryDSLSchema", () => {
|
|
17
|
-
it("validates a minimal query", () => {
|
|
18
|
-
const result = queryDSLSchema.safeParse({ model: "Epic" });
|
|
19
|
-
expect(result.success).toBe(true);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("validates a full query", () => {
|
|
23
|
-
const result = queryDSLSchema.safeParse({
|
|
24
|
-
model: "Epic",
|
|
25
|
-
where: { "meta.status": "created", "meta.priority": { $gt: 3 } },
|
|
26
|
-
sort: { "meta.priority": "desc" },
|
|
27
|
-
select: ["id", "title"],
|
|
28
|
-
limit: 10,
|
|
29
|
-
offset: 5,
|
|
30
|
-
method: "fetchAll",
|
|
31
|
-
});
|
|
32
|
-
expect(result.success).toBe(true);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("rejects missing model", () => {
|
|
36
|
-
const result = queryDSLSchema.safeParse({ where: {} });
|
|
37
|
-
expect(result.success).toBe(false);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("rejects negative limit", () => {
|
|
41
|
-
const result = queryDSLSchema.safeParse({ model: "Epic", limit: -1 });
|
|
42
|
-
expect(result.success).toBe(false);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("rejects negative offset", () => {
|
|
46
|
-
const result = queryDSLSchema.safeParse({ model: "Epic", offset: -1 });
|
|
47
|
-
expect(result.success).toBe(false);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("rejects non-integer limit", () => {
|
|
51
|
-
const result = queryDSLSchema.safeParse({ model: "Epic", limit: 1.5 });
|
|
52
|
-
expect(result.success).toBe(false);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("rejects invalid method", () => {
|
|
56
|
-
const result = queryDSLSchema.safeParse({
|
|
57
|
-
model: "Epic",
|
|
58
|
-
method: "deleteAll",
|
|
59
|
-
});
|
|
60
|
-
expect(result.success).toBe(false);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("defaults method to fetchAll", () => {
|
|
64
|
-
const result = queryDSLSchema.parse({ model: "Epic" });
|
|
65
|
-
expect(result.method).toBe("fetchAll");
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
// parseWhereClause
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
|
|
73
|
-
describe("parseWhereClause", () => {
|
|
74
|
-
it("converts literal string to eq condition", () => {
|
|
75
|
-
const conditions = parseWhereClause({ "meta.status": "active" });
|
|
76
|
-
expect(conditions).toEqual([
|
|
77
|
-
{ path: "meta.status", operator: "eq", value: "active" },
|
|
78
|
-
]);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("converts literal number to eq condition", () => {
|
|
82
|
-
const conditions = parseWhereClause({ "meta.count": 5 });
|
|
83
|
-
expect(conditions).toEqual([
|
|
84
|
-
{ path: "meta.count", operator: "eq", value: 5 },
|
|
85
|
-
]);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("converts literal boolean to eq condition", () => {
|
|
89
|
-
const conditions = parseWhereClause({ "meta.active": true });
|
|
90
|
-
expect(conditions).toEqual([
|
|
91
|
-
{ path: "meta.active", operator: "eq", value: true },
|
|
92
|
-
]);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("converts null to eq condition", () => {
|
|
96
|
-
const conditions = parseWhereClause({ "meta.field": null });
|
|
97
|
-
expect(conditions).toEqual([
|
|
98
|
-
{ path: "meta.field", operator: "eq", value: null },
|
|
99
|
-
]);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("converts array to in condition", () => {
|
|
103
|
-
const conditions = parseWhereClause({
|
|
104
|
-
"meta.status": ["active", "draft"],
|
|
105
|
-
});
|
|
106
|
-
expect(conditions).toEqual([
|
|
107
|
-
{ path: "meta.status", operator: "in", value: ["active", "draft"] },
|
|
108
|
-
]);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("converts operator object to condition", () => {
|
|
112
|
-
const conditions = parseWhereClause({
|
|
113
|
-
"meta.priority": { $gt: 5 },
|
|
114
|
-
});
|
|
115
|
-
expect(conditions).toEqual([
|
|
116
|
-
{ path: "meta.priority", operator: "gt", value: 5 },
|
|
117
|
-
]);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("handles multiple operators on the same path", () => {
|
|
121
|
-
const conditions = parseWhereClause({
|
|
122
|
-
"meta.priority": { $gte: 3, $lte: 8 },
|
|
123
|
-
});
|
|
124
|
-
expect(conditions).toEqual([
|
|
125
|
-
{ path: "meta.priority", operator: "gte", value: 3 },
|
|
126
|
-
{ path: "meta.priority", operator: "lte", value: 8 },
|
|
127
|
-
]);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it("handles mixed literal and operator values", () => {
|
|
131
|
-
const conditions = parseWhereClause({
|
|
132
|
-
"meta.status": "active",
|
|
133
|
-
"meta.priority": { $gt: 5 },
|
|
134
|
-
title: { $contains: "Auth" },
|
|
135
|
-
});
|
|
136
|
-
expect(conditions.length).toBe(3);
|
|
137
|
-
expect(conditions[0].operator).toBe("eq");
|
|
138
|
-
expect(conditions[1].operator).toBe("gt");
|
|
139
|
-
expect(conditions[2].operator).toBe("contains");
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it("supports all operators", () => {
|
|
143
|
-
const ops = [
|
|
144
|
-
"$eq",
|
|
145
|
-
"$neq",
|
|
146
|
-
"$in",
|
|
147
|
-
"$notIn",
|
|
148
|
-
"$gt",
|
|
149
|
-
"$lt",
|
|
150
|
-
"$gte",
|
|
151
|
-
"$lte",
|
|
152
|
-
"$contains",
|
|
153
|
-
"$startsWith",
|
|
154
|
-
"$endsWith",
|
|
155
|
-
"$regex",
|
|
156
|
-
"$exists",
|
|
157
|
-
];
|
|
158
|
-
for (const op of ops) {
|
|
159
|
-
const conditions = parseWhereClause({
|
|
160
|
-
field: { [op]: "value" },
|
|
161
|
-
});
|
|
162
|
-
expect(conditions.length).toBe(1);
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it("throws on unknown operator", () => {
|
|
167
|
-
expect(() =>
|
|
168
|
-
parseWhereClause({ field: { $unknown: "value" } }),
|
|
169
|
-
).toThrow("Unknown operator: $unknown");
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it("throws on regex pattern exceeding max length", () => {
|
|
173
|
-
const longPattern = "a".repeat(201);
|
|
174
|
-
expect(() =>
|
|
175
|
-
parseWhereClause({ field: { $regex: longPattern } }),
|
|
176
|
-
).toThrow("Regex pattern exceeds maximum length");
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it("rejects __proto__ path segment", () => {
|
|
180
|
-
expect(() =>
|
|
181
|
-
parseWhereClause({ "__proto__.polluted": "value" }),
|
|
182
|
-
).toThrow('Forbidden path segment "__proto__"');
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it("rejects constructor path segment", () => {
|
|
186
|
-
expect(() =>
|
|
187
|
-
parseWhereClause({ "meta.constructor.name": "value" }),
|
|
188
|
-
).toThrow('Forbidden path segment "constructor"');
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it("rejects prototype path segment", () => {
|
|
192
|
-
expect(() =>
|
|
193
|
-
parseWhereClause({ "prototype.method": "value" }),
|
|
194
|
-
).toThrow('Forbidden path segment "prototype"');
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
// ---------------------------------------------------------------------------
|
|
199
|
-
// parseSortClause
|
|
200
|
-
// ---------------------------------------------------------------------------
|
|
201
|
-
|
|
202
|
-
describe("parseSortClause", () => {
|
|
203
|
-
it("parses object form", () => {
|
|
204
|
-
const sorts = parseSortClause({ "meta.priority": "desc", title: "asc" });
|
|
205
|
-
expect(sorts).toEqual([
|
|
206
|
-
{ path: "meta.priority", direction: "desc" },
|
|
207
|
-
{ path: "title", direction: "asc" },
|
|
208
|
-
]);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it("passes through array form", () => {
|
|
212
|
-
const input = [{ path: "title", direction: "asc" as const }];
|
|
213
|
-
const sorts = parseSortClause(input);
|
|
214
|
-
expect(sorts).toEqual(input);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it("returns empty array for undefined", () => {
|
|
218
|
-
expect(parseSortClause(undefined)).toEqual([]);
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it("returns empty array for null", () => {
|
|
222
|
-
expect(parseSortClause(null)).toEqual([]);
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
// ---------------------------------------------------------------------------
|
|
227
|
-
// executeQueryDSL — integration tests
|
|
228
|
-
// ---------------------------------------------------------------------------
|
|
229
|
-
|
|
230
|
-
describe("executeQueryDSL", () => {
|
|
231
|
-
let collection: Collection;
|
|
232
|
-
|
|
233
|
-
beforeEach(async () => {
|
|
234
|
-
collection = await createTestCollection();
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it("returns all instances of a model", async () => {
|
|
238
|
-
const results = await executeQueryDSL(collection, {
|
|
239
|
-
model: "Epic",
|
|
240
|
-
method: "fetchAll",
|
|
241
|
-
});
|
|
242
|
-
expect(Array.isArray(results)).toBe(true);
|
|
243
|
-
expect((results as any[]).length).toBe(2);
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it("filters with implicit $eq", async () => {
|
|
247
|
-
const results = await executeQueryDSL(collection, {
|
|
248
|
-
model: "Epic",
|
|
249
|
-
where: { "meta.priority": "high" },
|
|
250
|
-
method: "fetchAll",
|
|
251
|
-
});
|
|
252
|
-
expect((results as any[]).length).toBe(1);
|
|
253
|
-
expect((results as any[])[0].meta.priority).toBe("high");
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
it("filters with $exists", async () => {
|
|
257
|
-
const results = await executeQueryDSL(collection, {
|
|
258
|
-
model: "Epic",
|
|
259
|
-
where: { "meta.priority": { $exists: true } },
|
|
260
|
-
method: "fetchAll",
|
|
261
|
-
});
|
|
262
|
-
expect((results as any[]).length).toBe(1);
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
it("filters with $contains on title", async () => {
|
|
266
|
-
const results = await executeQueryDSL(collection, {
|
|
267
|
-
model: "Epic",
|
|
268
|
-
where: { title: { $contains: "Authentication" } },
|
|
269
|
-
method: "fetchAll",
|
|
270
|
-
});
|
|
271
|
-
expect((results as any[]).length).toBe(1);
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
it("sorts results", async () => {
|
|
275
|
-
const results = await executeQueryDSL(collection, {
|
|
276
|
-
model: "Epic",
|
|
277
|
-
sort: { title: "desc" },
|
|
278
|
-
method: "fetchAll",
|
|
279
|
-
}) as any[];
|
|
280
|
-
expect(results[0].title.localeCompare(results[1].title)).toBeGreaterThan(0);
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
it("applies limit", async () => {
|
|
284
|
-
const results = await executeQueryDSL(collection, {
|
|
285
|
-
model: "Epic",
|
|
286
|
-
limit: 1,
|
|
287
|
-
method: "fetchAll",
|
|
288
|
-
});
|
|
289
|
-
expect((results as any[]).length).toBe(1);
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
it("applies offset", async () => {
|
|
293
|
-
const all = await executeQueryDSL(collection, {
|
|
294
|
-
model: "Epic",
|
|
295
|
-
sort: { title: "asc" },
|
|
296
|
-
method: "fetchAll",
|
|
297
|
-
}) as any[];
|
|
298
|
-
|
|
299
|
-
const offsetResults = await executeQueryDSL(collection, {
|
|
300
|
-
model: "Epic",
|
|
301
|
-
sort: { title: "asc" },
|
|
302
|
-
offset: 1,
|
|
303
|
-
method: "fetchAll",
|
|
304
|
-
}) as any[];
|
|
305
|
-
|
|
306
|
-
expect(offsetResults.length).toBe(1);
|
|
307
|
-
expect(offsetResults[0].title).toBe(all[1].title);
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
it("applies limit + offset for pagination", async () => {
|
|
311
|
-
const all = await executeQueryDSL(collection, {
|
|
312
|
-
model: "Epic",
|
|
313
|
-
sort: { title: "asc" },
|
|
314
|
-
method: "fetchAll",
|
|
315
|
-
}) as any[];
|
|
316
|
-
|
|
317
|
-
const page = await executeQueryDSL(collection, {
|
|
318
|
-
model: "Epic",
|
|
319
|
-
sort: { title: "asc" },
|
|
320
|
-
limit: 1,
|
|
321
|
-
offset: 1,
|
|
322
|
-
method: "fetchAll",
|
|
323
|
-
}) as any[];
|
|
324
|
-
|
|
325
|
-
expect(page.length).toBe(1);
|
|
326
|
-
expect(page[0].title).toBe(all[1].title);
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
it("method count returns count object", async () => {
|
|
330
|
-
const result = await executeQueryDSL(collection, {
|
|
331
|
-
model: "Epic",
|
|
332
|
-
method: "count",
|
|
333
|
-
});
|
|
334
|
-
expect(result).toEqual({ count: 2 });
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
it("method first returns single result", async () => {
|
|
338
|
-
const result = await executeQueryDSL(collection, {
|
|
339
|
-
model: "Epic",
|
|
340
|
-
method: "first",
|
|
341
|
-
});
|
|
342
|
-
expect(result).toBeDefined();
|
|
343
|
-
expect((result as any).title).toBeDefined();
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
it("method last returns single result", async () => {
|
|
347
|
-
const result = await executeQueryDSL(collection, {
|
|
348
|
-
model: "Epic",
|
|
349
|
-
method: "last",
|
|
350
|
-
});
|
|
351
|
-
expect(result).toBeDefined();
|
|
352
|
-
expect((result as any).title).toBeDefined();
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
it("select filters output fields", async () => {
|
|
356
|
-
const results = await executeQueryDSL(collection, {
|
|
357
|
-
model: "Epic",
|
|
358
|
-
select: ["id", "title"],
|
|
359
|
-
method: "fetchAll",
|
|
360
|
-
}) as any[];
|
|
361
|
-
|
|
362
|
-
for (const r of results) {
|
|
363
|
-
expect(Object.keys(r)).toEqual(["id", "title"]);
|
|
364
|
-
}
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
it("throws on unknown model", async () => {
|
|
368
|
-
await expect(
|
|
369
|
-
executeQueryDSL(collection, {
|
|
370
|
-
model: "NonExistent",
|
|
371
|
-
method: "fetchAll",
|
|
372
|
-
}),
|
|
373
|
-
).rejects.toThrow("Unknown model: NonExistent");
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
it("resolves model by prefix", async () => {
|
|
377
|
-
const results = await executeQueryDSL(collection, {
|
|
378
|
-
model: "epics",
|
|
379
|
-
method: "fetchAll",
|
|
380
|
-
});
|
|
381
|
-
expect((results as any[]).length).toBe(2);
|
|
382
|
-
});
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
// ---------------------------------------------------------------------------
|
|
386
|
-
// CollectionQuery limit/offset
|
|
387
|
-
// ---------------------------------------------------------------------------
|
|
388
|
-
|
|
389
|
-
describe("CollectionQuery limit/offset", () => {
|
|
390
|
-
let collection: Collection;
|
|
391
|
-
|
|
392
|
-
beforeEach(async () => {
|
|
393
|
-
collection = await createTestCollection();
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
it("limit restricts results", async () => {
|
|
397
|
-
const results = await collection
|
|
398
|
-
.query(Epic)
|
|
399
|
-
.limit(1)
|
|
400
|
-
.fetchAll();
|
|
401
|
-
expect(results.length).toBe(1);
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
it("offset skips results", async () => {
|
|
405
|
-
const all = await collection.query(Epic).sort("title").fetchAll();
|
|
406
|
-
const offset = await collection
|
|
407
|
-
.query(Epic)
|
|
408
|
-
.sort("title")
|
|
409
|
-
.offset(1)
|
|
410
|
-
.fetchAll();
|
|
411
|
-
expect(offset.length).toBe(1);
|
|
412
|
-
expect(offset[0].title).toBe(all[1].title);
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
it("limit(0) returns empty array", async () => {
|
|
416
|
-
const results = await collection.query(Epic).limit(0).fetchAll();
|
|
417
|
-
expect(results.length).toBe(0);
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
it("limit + offset chain together", async () => {
|
|
421
|
-
const all = await collection.query(Epic).sort("title").fetchAll();
|
|
422
|
-
const page = await collection
|
|
423
|
-
.query(Epic)
|
|
424
|
-
.sort("title")
|
|
425
|
-
.offset(1)
|
|
426
|
-
.limit(1)
|
|
427
|
-
.fetchAll();
|
|
428
|
-
expect(page.length).toBe(1);
|
|
429
|
-
expect(page[0].title).toBe(all[1].title);
|
|
430
|
-
});
|
|
431
|
-
});
|
package/test/query.test.ts
DELETED
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
-
import { QueryBuilder } from "../src/query/query-builder";
|
|
3
|
-
import { operators } from "../src/query/operators";
|
|
4
|
-
import { Collection } from "../src/collection";
|
|
5
|
-
import { createTestCollection } from "./helpers";
|
|
6
|
-
import { Epic } from "./fixtures/sdlc/models";
|
|
7
|
-
|
|
8
|
-
describe("QueryBuilder", () => {
|
|
9
|
-
it("builds eq conditions with two args", () => {
|
|
10
|
-
const qb = new QueryBuilder();
|
|
11
|
-
qb.where("meta.status", "active");
|
|
12
|
-
expect(qb.conditions).toEqual([
|
|
13
|
-
{ path: "meta.status", operator: "eq", value: "active" },
|
|
14
|
-
]);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("builds conditions with explicit operator", () => {
|
|
18
|
-
const qb = new QueryBuilder();
|
|
19
|
-
qb.where("meta.count", "gt", 5);
|
|
20
|
-
expect(qb.conditions[0].operator).toBe("gt");
|
|
21
|
-
expect(qb.conditions[0].value).toBe(5);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("builds conditions from object shorthand", () => {
|
|
25
|
-
const qb = new QueryBuilder();
|
|
26
|
-
qb.where({ "meta.status": "active", "meta.type": "post" });
|
|
27
|
-
expect(qb.conditions.length).toBe(2);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("whereIn adds in condition", () => {
|
|
31
|
-
const qb = new QueryBuilder();
|
|
32
|
-
qb.whereIn("meta.tags", ["a", "b"]);
|
|
33
|
-
expect(qb.conditions[0].operator).toBe("in");
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("whereNotIn adds notIn condition", () => {
|
|
37
|
-
const qb = new QueryBuilder();
|
|
38
|
-
qb.whereNotIn("meta.status", ["archived"]);
|
|
39
|
-
expect(qb.conditions[0].operator).toBe("notIn");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("supports all chainable methods", () => {
|
|
43
|
-
const qb = new QueryBuilder();
|
|
44
|
-
const result = qb
|
|
45
|
-
.where("a", "1")
|
|
46
|
-
.whereGt("b", 2)
|
|
47
|
-
.whereLt("c", 3)
|
|
48
|
-
.whereGte("d", 4)
|
|
49
|
-
.whereLte("e", 5)
|
|
50
|
-
.whereContains("f", "hello")
|
|
51
|
-
.whereStartsWith("g", "pre")
|
|
52
|
-
.whereEndsWith("h", "suf")
|
|
53
|
-
.whereRegex("i", /test/)
|
|
54
|
-
.whereExists("j")
|
|
55
|
-
.whereNotExists("k");
|
|
56
|
-
|
|
57
|
-
expect(result).toBe(qb); // chaining returns this
|
|
58
|
-
expect(qb.conditions.length).toBe(11);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("where with three args returns this (bug fix)", () => {
|
|
62
|
-
const qb = new QueryBuilder();
|
|
63
|
-
const result = qb.where("path", "neq", "value");
|
|
64
|
-
expect(result).toBe(qb);
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
describe("operators", () => {
|
|
69
|
-
it("eq handles primitive equality", () => {
|
|
70
|
-
expect(operators.eq("a", "a")).toBe(true);
|
|
71
|
-
expect(operators.eq("a", "b")).toBe(false);
|
|
72
|
-
expect(operators.eq(1, 1)).toBe(true);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("eq handles deep equality", () => {
|
|
76
|
-
expect(operators.eq({ a: 1 }, { a: 1 })).toBe(true);
|
|
77
|
-
expect(operators.eq({ a: 1 }, { a: 2 })).toBe(false);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("neq is negation of eq", () => {
|
|
81
|
-
expect(operators.neq("a", "b")).toBe(true);
|
|
82
|
-
expect(operators.neq("a", "a")).toBe(false);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("in checks membership", () => {
|
|
86
|
-
expect(operators.in("a", ["a", "b"])).toBe(true);
|
|
87
|
-
expect(operators.in("c", ["a", "b"])).toBe(false);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("notIn checks non-membership", () => {
|
|
91
|
-
expect(operators.notIn("c", ["a", "b"])).toBe(true);
|
|
92
|
-
expect(operators.notIn("a", ["a", "b"])).toBe(false);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("gt/lt/gte/lte compare values", () => {
|
|
96
|
-
expect(operators.gt(5, 3)).toBe(true);
|
|
97
|
-
expect(operators.lt(3, 5)).toBe(true);
|
|
98
|
-
expect(operators.gte(5, 5)).toBe(true);
|
|
99
|
-
expect(operators.lte(5, 5)).toBe(true);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("contains checks string inclusion", () => {
|
|
103
|
-
expect(operators.contains("hello world", "world")).toBe(true);
|
|
104
|
-
expect(operators.contains("hello", "world")).toBe(false);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("startsWith/endsWith check string prefixes/suffixes", () => {
|
|
108
|
-
expect(operators.startsWith("hello", "hel")).toBe(true);
|
|
109
|
-
expect(operators.endsWith("hello", "llo")).toBe(true);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("regex tests patterns", () => {
|
|
113
|
-
expect(operators.regex("hello123", /\d+/)).toBe(true);
|
|
114
|
-
expect(operators.regex("hello123", "\\d+")).toBe(true);
|
|
115
|
-
expect(operators.regex("hello", /\d+/)).toBe(false);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("exists checks for defined values", () => {
|
|
119
|
-
expect(operators.exists("value", true)).toBe(true);
|
|
120
|
-
expect(operators.exists(null, true)).toBe(false);
|
|
121
|
-
expect(operators.exists(undefined, true)).toBe(false);
|
|
122
|
-
expect(operators.exists(null, false)).toBe(true);
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
describe("CollectionQuery", () => {
|
|
127
|
-
let collection: Collection;
|
|
128
|
-
|
|
129
|
-
beforeEach(async () => {
|
|
130
|
-
collection = await createTestCollection();
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("fetchAll returns matching model instances", async () => {
|
|
134
|
-
const epics = await collection.query(Epic).fetchAll();
|
|
135
|
-
expect(epics.length).toBe(2);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it("where filters results", async () => {
|
|
139
|
-
const epics = await collection
|
|
140
|
-
.query(Epic)
|
|
141
|
-
.where("meta.priority", "high")
|
|
142
|
-
.fetchAll();
|
|
143
|
-
expect(epics.length).toBe(1);
|
|
144
|
-
expect(epics[0].meta.priority).toBe("high");
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("first returns first result", async () => {
|
|
148
|
-
const first = await collection.query(Epic).first();
|
|
149
|
-
expect(first).toBeDefined();
|
|
150
|
-
expect(first!.title).toBeDefined();
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("last returns last result", async () => {
|
|
154
|
-
const last = await collection.query(Epic).last();
|
|
155
|
-
expect(last).toBeDefined();
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it("count returns correct count", async () => {
|
|
159
|
-
const count = await collection.query(Epic).count();
|
|
160
|
-
expect(count).toBe(2);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("empty conditions returns all of model type", async () => {
|
|
164
|
-
const all = await collection.query(Epic).fetchAll();
|
|
165
|
-
expect(all.length).toBe(2);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it("sort orders results ascending by default", async () => {
|
|
169
|
-
const epics = await collection.query(Epic).sort("title").fetchAll();
|
|
170
|
-
expect(epics.length).toBe(2);
|
|
171
|
-
expect(epics[0].title.localeCompare(epics[1].title)).toBeLessThan(0);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it("sort orders results descending", async () => {
|
|
175
|
-
const epics = await collection.query(Epic).sort("title", "desc").fetchAll();
|
|
176
|
-
expect(epics.length).toBe(2);
|
|
177
|
-
expect(epics[0].title.localeCompare(epics[1].title)).toBeGreaterThan(0);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it("sort handles null values (pushed to end in asc)", async () => {
|
|
181
|
-
// authentication has priority "high", searching-and-browsing has no priority
|
|
182
|
-
const epics = await collection.query(Epic).sort("meta.priority").fetchAll();
|
|
183
|
-
expect(epics[0].meta.priority).toBe("high");
|
|
184
|
-
expect(epics[1].meta.priority).toBeUndefined();
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it("sort chains with where", async () => {
|
|
188
|
-
const epics = await collection
|
|
189
|
-
.query(Epic)
|
|
190
|
-
.where("meta.status", "created")
|
|
191
|
-
.sort("title", "desc")
|
|
192
|
-
.fetchAll();
|
|
193
|
-
expect(epics.length).toBe(2);
|
|
194
|
-
expect(epics[0].title.localeCompare(epics[1].title)).toBeGreaterThan(0);
|
|
195
|
-
});
|
|
196
|
-
});
|