@convex-dev/rag 0.3.0 → 0.3.2
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 +463 -121
- package/dist/client/defaultChunker.d.ts.map +1 -1
- package/dist/client/defaultChunker.js +47 -16
- package/dist/client/defaultChunker.js.map +1 -1
- package/dist/client/fileUtils.d.ts +4 -2
- package/dist/client/fileUtils.d.ts.map +1 -1
- package/dist/client/fileUtils.js +5 -3
- package/dist/client/fileUtils.js.map +1 -1
- package/dist/client/index.d.ts +19 -15
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +15 -11
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +11 -4
- package/dist/component/chunks.d.ts +1 -0
- package/dist/component/chunks.d.ts.map +1 -1
- package/dist/component/chunks.js +2 -1
- package/dist/component/chunks.js.map +1 -1
- package/dist/component/entries.d.ts +8 -8
- package/dist/component/entries.d.ts.map +1 -1
- package/dist/component/entries.js +30 -17
- package/dist/component/entries.js.map +1 -1
- package/dist/component/namespaces.d.ts +3 -3
- package/dist/component/namespaces.js +4 -4
- package/dist/component/namespaces.js.map +1 -1
- package/dist/component/schema.d.ts +31 -31
- package/dist/shared.d.ts +28 -11
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +2 -1
- package/dist/shared.js.map +1 -1
- package/package.json +1 -6
- package/src/client/defaultChunker.test.ts +1 -1
- package/src/client/defaultChunker.ts +73 -17
- package/src/client/fileUtils.ts +8 -4
- package/src/client/index.test.ts +28 -24
- package/src/client/index.ts +30 -24
- package/src/component/_generated/api.d.ts +11 -4
- package/src/component/chunks.test.ts +2 -0
- package/src/component/chunks.ts +2 -1
- package/src/component/entries.test.ts +16 -16
- package/src/component/entries.ts +31 -19
- package/src/component/namespaces.ts +4 -4
- package/src/shared.ts +15 -7
package/src/client/fileUtils.ts
CHANGED
|
@@ -133,13 +133,17 @@ export function guessMimeTypeFromContents(buf: ArrayBuffer | string): string {
|
|
|
133
133
|
/**
|
|
134
134
|
* Make a contentHash of a Blob that matches the File Storage metadata, allowing
|
|
135
135
|
* identifying when content is identical.
|
|
136
|
+
* By default, uses SHA-256 (which is what Convex File Storage tracks).
|
|
137
|
+
* Git / GitHub use SHA-1.
|
|
136
138
|
* @param blob The contents to hash
|
|
137
|
-
* @returns
|
|
139
|
+
* @returns hash of the contents
|
|
138
140
|
*/
|
|
139
|
-
|
|
140
|
-
|
|
141
|
+
export async function contentHashFromArrayBuffer(
|
|
142
|
+
buffer: ArrayBuffer,
|
|
143
|
+
algorithm: "SHA-256" | "SHA-1" = "SHA-256"
|
|
144
|
+
) {
|
|
141
145
|
return Array.from(
|
|
142
|
-
new Uint8Array(await crypto.subtle.digest(
|
|
146
|
+
new Uint8Array(await crypto.subtle.digest(algorithm, buffer))
|
|
143
147
|
)
|
|
144
148
|
.map((b) => b.toString(16).padStart(2, "0"))
|
|
145
149
|
.join("");
|
package/src/client/index.test.ts
CHANGED
|
@@ -32,10 +32,10 @@ const rag = new RAG(components.rag, {
|
|
|
32
32
|
filterNames: ["simpleString", "arrayOfStrings", "customObject"],
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
-
export const
|
|
35
|
+
export const findEntryByContentHash = query({
|
|
36
36
|
args: { namespace: v.string(), key: v.string(), contentHash: v.string() },
|
|
37
37
|
handler: async (ctx, args) => {
|
|
38
|
-
return rag.
|
|
38
|
+
return rag.findEntryByContentHash(ctx, {
|
|
39
39
|
namespace: args.namespace,
|
|
40
40
|
key: args.key,
|
|
41
41
|
contentHash: args.contentHash,
|
|
@@ -95,7 +95,7 @@ export const search = action({
|
|
|
95
95
|
},
|
|
96
96
|
handler: async (ctx, args) => {
|
|
97
97
|
const { results, entries, text } = await rag.search(ctx, {
|
|
98
|
-
|
|
98
|
+
query: args.embedding,
|
|
99
99
|
namespace: args.namespace,
|
|
100
100
|
limit: args.limit ?? 10,
|
|
101
101
|
chunkContext: args.chunkContext ?? { before: 0, after: 0 },
|
|
@@ -111,7 +111,7 @@ export const search = action({
|
|
|
111
111
|
|
|
112
112
|
const testApi: ApiFromModules<{
|
|
113
113
|
fns: {
|
|
114
|
-
|
|
114
|
+
findEntryByContentHash: typeof findEntryByContentHash;
|
|
115
115
|
add: typeof add;
|
|
116
116
|
search: typeof search;
|
|
117
117
|
};
|
|
@@ -200,16 +200,16 @@ describe("RAG thick client", () => {
|
|
|
200
200
|
limit: 10,
|
|
201
201
|
});
|
|
202
202
|
|
|
203
|
-
// Should match README format: "
|
|
204
|
-
expect(text).toContain("
|
|
203
|
+
// Should match README format: "## Title:\n{entry.text}"
|
|
204
|
+
expect(text).toContain("## Test Document:");
|
|
205
205
|
expect(entries).toHaveLength(1);
|
|
206
206
|
expect(entries[0].text).toBe(
|
|
207
207
|
"Chunk 1 content\nChunk 2 content\nChunk 3 content"
|
|
208
208
|
);
|
|
209
209
|
|
|
210
|
-
// Overall text should be: "
|
|
210
|
+
// Overall text should be: "## Test Document:\nChunk 1 content\nChunk 2 content\nChunk 3 content"
|
|
211
211
|
expect(text).toBe(
|
|
212
|
-
"
|
|
212
|
+
"## Test Document:\n\nChunk 1 content\nChunk 2 content\nChunk 3 content"
|
|
213
213
|
);
|
|
214
214
|
});
|
|
215
215
|
|
|
@@ -235,8 +235,8 @@ describe("RAG thick client", () => {
|
|
|
235
235
|
limit: 10,
|
|
236
236
|
});
|
|
237
237
|
|
|
238
|
-
// Should not have "
|
|
239
|
-
expect(text).not.toContain("
|
|
238
|
+
// Should not have "## " prefix since no title
|
|
239
|
+
expect(text).not.toContain("## ");
|
|
240
240
|
expect(entries).toHaveLength(1);
|
|
241
241
|
expect(entries[0].text).toBe("Content without title");
|
|
242
242
|
expect(text).toBe("Content without title");
|
|
@@ -337,11 +337,11 @@ describe("RAG thick client", () => {
|
|
|
337
337
|
|
|
338
338
|
// Should have entries separated by "\n---\n" as per README
|
|
339
339
|
expect(text).toContain("---");
|
|
340
|
-
expect(text).toMatch(
|
|
340
|
+
expect(text).toMatch(/## .+:\n\n.+\n\n---\n\n## .+:\n\n.+/);
|
|
341
341
|
|
|
342
|
-
// Should have both titles prefixed with "
|
|
343
|
-
expect(text).toContain("
|
|
344
|
-
expect(text).toContain("
|
|
342
|
+
// Should have both titles prefixed with "## "
|
|
343
|
+
expect(text).toContain("## First Document:");
|
|
344
|
+
expect(text).toContain("## Second Document:");
|
|
345
345
|
|
|
346
346
|
expect(entries).toHaveLength(2);
|
|
347
347
|
});
|
|
@@ -384,14 +384,14 @@ describe("RAG thick client", () => {
|
|
|
384
384
|
|
|
385
385
|
// Should properly handle mixed formatting
|
|
386
386
|
expect(text).toContain("---"); // Entries should be separated
|
|
387
|
-
expect(text).toContain("
|
|
387
|
+
expect(text).toContain("## Titled Document:"); // Titled entry should have prefix
|
|
388
388
|
|
|
389
389
|
// One entry should have title format, one should not
|
|
390
390
|
const parts = text.split("\n---\n");
|
|
391
391
|
expect(parts).toHaveLength(2);
|
|
392
392
|
|
|
393
|
-
const hasTitle = parts.some((part) => part.startsWith("
|
|
394
|
-
const hasNoTitle = parts.some((part) => !part.startsWith("
|
|
393
|
+
const hasTitle = parts.some((part) => part.startsWith("## "));
|
|
394
|
+
const hasNoTitle = parts.some((part) => !part.startsWith("## "));
|
|
395
395
|
expect(hasTitle).toBe(true);
|
|
396
396
|
expect(hasNoTitle).toBe(true);
|
|
397
397
|
|
|
@@ -445,28 +445,32 @@ describe("RAG thick client", () => {
|
|
|
445
445
|
});
|
|
446
446
|
|
|
447
447
|
// Verify basic structure matches README
|
|
448
|
-
expect(text).toContain("
|
|
449
|
-
expect(text).toContain("
|
|
448
|
+
expect(text).toContain("## Title 1:");
|
|
449
|
+
expect(text).toContain("## Title 2:");
|
|
450
450
|
expect(text).toContain("---");
|
|
451
451
|
|
|
452
452
|
// Should have proper entry separation
|
|
453
|
-
const parts = text.split("\n---\n");
|
|
453
|
+
const parts = text.split("\n\n---\n\n");
|
|
454
454
|
expect(parts).toHaveLength(2);
|
|
455
455
|
|
|
456
|
-
// Each part should start with "
|
|
456
|
+
// Each part should start with "## Title X:"
|
|
457
457
|
parts.forEach((part) => {
|
|
458
|
-
expect(part).toMatch(
|
|
458
|
+
expect(part).toMatch(/^## Title \d+:/);
|
|
459
459
|
});
|
|
460
460
|
|
|
461
461
|
expect(entries).toHaveLength(2);
|
|
462
462
|
|
|
463
463
|
// Individual entry texts should follow the pattern
|
|
464
464
|
expect(text).toBe(
|
|
465
|
-
|
|
465
|
+
`## Title 1:
|
|
466
|
+
|
|
466
467
|
Chunk 1 contents
|
|
467
468
|
Chunk 2 contents
|
|
469
|
+
|
|
468
470
|
---
|
|
469
|
-
|
|
471
|
+
|
|
472
|
+
## Title 2:
|
|
473
|
+
|
|
470
474
|
Chunk 3 contents
|
|
471
475
|
Chunk 4 contents`
|
|
472
476
|
);
|
package/src/client/index.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
type Chunk,
|
|
28
28
|
type CreateChunkArgs,
|
|
29
29
|
type Entry,
|
|
30
|
-
type
|
|
30
|
+
type EntryFilter,
|
|
31
31
|
type EntryId,
|
|
32
32
|
type Namespace,
|
|
33
33
|
type NamespaceId,
|
|
@@ -66,6 +66,7 @@ export type {
|
|
|
66
66
|
export {
|
|
67
67
|
type VEntry,
|
|
68
68
|
type VSearchEntry,
|
|
69
|
+
type EntryFilter,
|
|
69
70
|
vEntry,
|
|
70
71
|
vSearchEntry,
|
|
71
72
|
vSearchResult,
|
|
@@ -155,7 +156,7 @@ export class RAG<
|
|
|
155
156
|
entryId: EntryId;
|
|
156
157
|
status: Status;
|
|
157
158
|
created: boolean;
|
|
158
|
-
|
|
159
|
+
replacedEntry: Entry<FitlerSchemas, EntryMetadata> | null;
|
|
159
160
|
}> {
|
|
160
161
|
let namespaceId: NamespaceId;
|
|
161
162
|
if ("namespaceId" in args) {
|
|
@@ -182,7 +183,7 @@ export class RAG<
|
|
|
182
183
|
const onComplete =
|
|
183
184
|
args.onComplete && (await createFunctionHandle(args.onComplete));
|
|
184
185
|
|
|
185
|
-
const { entryId, status, created,
|
|
186
|
+
const { entryId, status, created, replacedEntry } = await ctx.runMutation(
|
|
186
187
|
this.component.entries.add,
|
|
187
188
|
{
|
|
188
189
|
entry: {
|
|
@@ -203,7 +204,7 @@ export class RAG<
|
|
|
203
204
|
entryId: entryId as EntryId,
|
|
204
205
|
status,
|
|
205
206
|
created,
|
|
206
|
-
|
|
207
|
+
replacedEntry: replacedEntry as Entry<
|
|
207
208
|
FitlerSchemas,
|
|
208
209
|
EntryMetadata
|
|
209
210
|
> | null,
|
|
@@ -243,7 +244,7 @@ export class RAG<
|
|
|
243
244
|
entryId: entryId as EntryId,
|
|
244
245
|
status: "replaced" as const,
|
|
245
246
|
created: false,
|
|
246
|
-
|
|
247
|
+
replacedEntry: null,
|
|
247
248
|
};
|
|
248
249
|
}
|
|
249
250
|
startOrder = nextStartOrder;
|
|
@@ -256,7 +257,7 @@ export class RAG<
|
|
|
256
257
|
return {
|
|
257
258
|
entryId: entryId as EntryId,
|
|
258
259
|
status: "ready" as const,
|
|
259
|
-
|
|
260
|
+
replacedEntry: promoted.replacedEntry as Entry<
|
|
260
261
|
FitlerSchemas,
|
|
261
262
|
EntryMetadata
|
|
262
263
|
> | null,
|
|
@@ -362,7 +363,7 @@ export class RAG<
|
|
|
362
363
|
/**
|
|
363
364
|
* The query to search for. Optional if embedding is provided.
|
|
364
365
|
*/
|
|
365
|
-
query
|
|
366
|
+
query: string | Array<number>;
|
|
366
367
|
} & SearchOptions<FitlerSchemas>
|
|
367
368
|
): Promise<{
|
|
368
369
|
results: SearchResult[];
|
|
@@ -376,7 +377,7 @@ export class RAG<
|
|
|
376
377
|
chunkContext = { before: 0, after: 0 },
|
|
377
378
|
vectorScoreThreshold,
|
|
378
379
|
} = args;
|
|
379
|
-
let embedding = args.
|
|
380
|
+
let embedding = Array.isArray(args.query) ? args.query : undefined;
|
|
380
381
|
if (!embedding) {
|
|
381
382
|
const embedResult = await embed({
|
|
382
383
|
model: this.options.textEmbeddingModel,
|
|
@@ -405,7 +406,7 @@ export class RAG<
|
|
|
405
406
|
for (const range of ranges) {
|
|
406
407
|
if (previousEnd !== 0) {
|
|
407
408
|
if (range.startOrder !== previousEnd) {
|
|
408
|
-
text += "\n...\n";
|
|
409
|
+
text += "\n\n...\n\n";
|
|
409
410
|
} else {
|
|
410
411
|
text += "\n";
|
|
411
412
|
}
|
|
@@ -419,8 +420,8 @@ export class RAG<
|
|
|
419
420
|
return {
|
|
420
421
|
results: results as SearchResult[],
|
|
421
422
|
text: entriesWithTexts
|
|
422
|
-
.map((e) => (e.title ?
|
|
423
|
-
.join(`\n---\n`),
|
|
423
|
+
.map((e) => (e.title ? `## ${e.title}:\n\n${e.text}` : e.text))
|
|
424
|
+
.join(`\n\n---\n\n`),
|
|
424
425
|
entries: entriesWithTexts,
|
|
425
426
|
};
|
|
426
427
|
}
|
|
@@ -446,6 +447,11 @@ export class RAG<
|
|
|
446
447
|
* The namespace to search in. e.g. a userId if entries are per-user.
|
|
447
448
|
*/
|
|
448
449
|
namespace: string;
|
|
450
|
+
/**
|
|
451
|
+
* The text or embedding to search for. If provided, it will be used
|
|
452
|
+
* instead of the prompt for vector search.
|
|
453
|
+
*/
|
|
454
|
+
query?: string | Array<number>;
|
|
449
455
|
};
|
|
450
456
|
/**
|
|
451
457
|
* Required. The prompt to use for context search, as well as the final
|
|
@@ -551,15 +557,18 @@ export class RAG<
|
|
|
551
557
|
async list(
|
|
552
558
|
ctx: RunQueryCtx,
|
|
553
559
|
args: {
|
|
554
|
-
namespaceId
|
|
555
|
-
paginationOpts: PaginationOptions;
|
|
560
|
+
namespaceId?: NamespaceId;
|
|
556
561
|
order?: "desc" | "asc";
|
|
557
562
|
status?: Status;
|
|
558
|
-
}
|
|
563
|
+
} & ({ paginationOpts: PaginationOptions } | { limit: number })
|
|
559
564
|
): Promise<PaginationResult<Entry<FitlerSchemas, EntryMetadata>>> {
|
|
565
|
+
const paginationOpts =
|
|
566
|
+
"paginationOpts" in args
|
|
567
|
+
? args.paginationOpts
|
|
568
|
+
: { cursor: null, numItems: args.limit };
|
|
560
569
|
const results = await ctx.runQuery(this.component.entries.list, {
|
|
561
570
|
namespaceId: args.namespaceId,
|
|
562
|
-
paginationOpts
|
|
571
|
+
paginationOpts,
|
|
563
572
|
order: args.order ?? "asc",
|
|
564
573
|
status: args.status ?? "ready",
|
|
565
574
|
});
|
|
@@ -586,7 +595,7 @@ export class RAG<
|
|
|
586
595
|
* new results into a new entry when migrating, or avoiding duplicating work
|
|
587
596
|
* when updating content.
|
|
588
597
|
*/
|
|
589
|
-
async
|
|
598
|
+
async findEntryByContentHash(
|
|
590
599
|
ctx: RunQueryCtx,
|
|
591
600
|
args: {
|
|
592
601
|
namespace: string;
|
|
@@ -679,11 +688,13 @@ export class RAG<
|
|
|
679
688
|
args: {
|
|
680
689
|
paginationOpts: PaginationOptions;
|
|
681
690
|
entryId: EntryId;
|
|
691
|
+
order?: "desc" | "asc";
|
|
682
692
|
}
|
|
683
693
|
): Promise<PaginationResult<Chunk>> {
|
|
684
694
|
return ctx.runQuery(this.component.chunks.list, {
|
|
685
695
|
entryId: args.entryId,
|
|
686
696
|
paginationOpts: args.paginationOpts,
|
|
697
|
+
order: args.order ?? "asc",
|
|
687
698
|
});
|
|
688
699
|
}
|
|
689
700
|
|
|
@@ -927,7 +938,7 @@ async function createChunkArgsBatch(
|
|
|
927
938
|
for (const batch of makeBatches(missingEmbeddingsWithIndex, 100)) {
|
|
928
939
|
const { embeddings } = await embedMany({
|
|
929
940
|
model: embedModel,
|
|
930
|
-
values: batch.map((b) => b.text),
|
|
941
|
+
values: batch.map((b) => b.text.trim() || "<empty>"),
|
|
931
942
|
});
|
|
932
943
|
for (const [index, embedding] of embeddings.entries()) {
|
|
933
944
|
argsMaybeMissingEmbeddings[batch[index].index].embedding = embedding;
|
|
@@ -1060,7 +1071,7 @@ type EntryArgs<
|
|
|
1060
1071
|
* and searching with the same value will return entries that match that
|
|
1061
1072
|
* value exactly.
|
|
1062
1073
|
*/
|
|
1063
|
-
filterValues?:
|
|
1074
|
+
filterValues?: EntryFilter<FitlerSchemas>[];
|
|
1064
1075
|
/**
|
|
1065
1076
|
* The importance of the entry. This is used to scale the vector search
|
|
1066
1077
|
* score of each chunk.
|
|
@@ -1080,11 +1091,6 @@ type EntryArgs<
|
|
|
1080
1091
|
};
|
|
1081
1092
|
|
|
1082
1093
|
type SearchOptions<FitlerSchemas extends Record<string, Value>> = {
|
|
1083
|
-
/**
|
|
1084
|
-
* The embedding to search for. If provided, it will be used instead
|
|
1085
|
-
* of the query for vector search.
|
|
1086
|
-
*/
|
|
1087
|
-
embedding?: Array<number>;
|
|
1088
1094
|
/**
|
|
1089
1095
|
* Filters to apply to the search. These are OR'd together. To represent
|
|
1090
1096
|
* AND logic, your filter can be an object or array with multiple values.
|
|
@@ -1097,7 +1103,7 @@ type SearchOptions<FitlerSchemas extends Record<string, Value>> = {
|
|
|
1097
1103
|
* `{ team_user: { team: "team1", user: "user1" } }`, it will not match
|
|
1098
1104
|
* `{ team_user: { team: "team1" } }` but it will match
|
|
1099
1105
|
*/
|
|
1100
|
-
filters?:
|
|
1106
|
+
filters?: EntryFilter<FitlerSchemas>[];
|
|
1101
1107
|
/**
|
|
1102
1108
|
* The maximum number of messages to fetch. Default is 10.
|
|
1103
1109
|
* This is the number *before* the chunkContext is applied.
|
|
@@ -62,6 +62,7 @@ export type Mounts = {
|
|
|
62
62
|
"public",
|
|
63
63
|
{
|
|
64
64
|
entryId: string;
|
|
65
|
+
order: "desc" | "asc";
|
|
65
66
|
paginationOpts: {
|
|
66
67
|
cursor: string | null;
|
|
67
68
|
endCursor?: string | null;
|
|
@@ -115,13 +116,14 @@ export type Mounts = {
|
|
|
115
116
|
{
|
|
116
117
|
created: boolean;
|
|
117
118
|
entryId: string;
|
|
118
|
-
|
|
119
|
+
replacedEntry: {
|
|
119
120
|
contentHash?: string;
|
|
120
121
|
entryId: string;
|
|
121
122
|
filterValues: Array<{ name: string; value: any }>;
|
|
122
123
|
importance: number;
|
|
123
124
|
key?: string;
|
|
124
125
|
metadata?: Record<string, any>;
|
|
126
|
+
replacedAt?: number;
|
|
125
127
|
status: "pending" | "ready" | "replaced";
|
|
126
128
|
title?: string;
|
|
127
129
|
} | null;
|
|
@@ -170,6 +172,7 @@ export type Mounts = {
|
|
|
170
172
|
importance: number;
|
|
171
173
|
key?: string;
|
|
172
174
|
metadata?: Record<string, any>;
|
|
175
|
+
replacedAt?: number;
|
|
173
176
|
status: "pending" | "ready" | "replaced";
|
|
174
177
|
title?: string;
|
|
175
178
|
} | null
|
|
@@ -185,6 +188,7 @@ export type Mounts = {
|
|
|
185
188
|
importance: number;
|
|
186
189
|
key?: string;
|
|
187
190
|
metadata?: Record<string, any>;
|
|
191
|
+
replacedAt?: number;
|
|
188
192
|
status: "pending" | "ready" | "replaced";
|
|
189
193
|
title?: string;
|
|
190
194
|
} | null
|
|
@@ -193,7 +197,7 @@ export type Mounts = {
|
|
|
193
197
|
"query",
|
|
194
198
|
"public",
|
|
195
199
|
{
|
|
196
|
-
namespaceId
|
|
200
|
+
namespaceId?: string;
|
|
197
201
|
order?: "desc" | "asc";
|
|
198
202
|
paginationOpts: {
|
|
199
203
|
cursor: string | null;
|
|
@@ -215,6 +219,7 @@ export type Mounts = {
|
|
|
215
219
|
importance: number;
|
|
216
220
|
key?: string;
|
|
217
221
|
metadata?: Record<string, any>;
|
|
222
|
+
replacedAt?: number;
|
|
218
223
|
status: "pending" | "ready" | "replaced";
|
|
219
224
|
title?: string;
|
|
220
225
|
}>;
|
|
@@ -227,13 +232,14 @@ export type Mounts = {
|
|
|
227
232
|
"public",
|
|
228
233
|
{ entryId: string },
|
|
229
234
|
{
|
|
230
|
-
|
|
235
|
+
replacedEntry: {
|
|
231
236
|
contentHash?: string;
|
|
232
237
|
entryId: string;
|
|
233
238
|
filterValues: Array<{ name: string; value: any }>;
|
|
234
239
|
importance: number;
|
|
235
240
|
key?: string;
|
|
236
241
|
metadata?: Record<string, any>;
|
|
242
|
+
replacedAt?: number;
|
|
237
243
|
status: "pending" | "ready" | "replaced";
|
|
238
244
|
title?: string;
|
|
239
245
|
} | null;
|
|
@@ -321,7 +327,7 @@ export type Mounts = {
|
|
|
321
327
|
"public",
|
|
322
328
|
{ namespaceId: string },
|
|
323
329
|
{
|
|
324
|
-
|
|
330
|
+
replacedNamespace: null | {
|
|
325
331
|
createdAt: number;
|
|
326
332
|
dimension: number;
|
|
327
333
|
filterNames: Array<string>;
|
|
@@ -355,6 +361,7 @@ export type Mounts = {
|
|
|
355
361
|
importance: number;
|
|
356
362
|
key?: string;
|
|
357
363
|
metadata?: Record<string, any>;
|
|
364
|
+
replacedAt?: number;
|
|
358
365
|
status: "pending" | "ready" | "replaced";
|
|
359
366
|
title?: string;
|
|
360
367
|
}>;
|
|
@@ -397,6 +397,7 @@ describe("chunks", () => {
|
|
|
397
397
|
// Test listing with pagination
|
|
398
398
|
const result = await t.query(api.chunks.list, {
|
|
399
399
|
entryId,
|
|
400
|
+
order: "asc",
|
|
400
401
|
paginationOpts: { numItems: 3, cursor: null },
|
|
401
402
|
});
|
|
402
403
|
|
|
@@ -417,6 +418,7 @@ describe("chunks", () => {
|
|
|
417
418
|
// Get next page
|
|
418
419
|
const nextResult = await t.query(api.chunks.list, {
|
|
419
420
|
entryId,
|
|
421
|
+
order: "asc",
|
|
420
422
|
paginationOpts: { numItems: 3, cursor: result.continueCursor },
|
|
421
423
|
});
|
|
422
424
|
|
package/src/component/chunks.ts
CHANGED
|
@@ -463,6 +463,7 @@ export const list = query({
|
|
|
463
463
|
args: v.object({
|
|
464
464
|
entryId: v.id("entries"),
|
|
465
465
|
paginationOpts: paginationOptsValidator,
|
|
466
|
+
order: v.union(v.literal("desc"), v.literal("asc")),
|
|
466
467
|
}),
|
|
467
468
|
returns: vPaginationResult(vChunk),
|
|
468
469
|
handler: async (ctx, args) => {
|
|
@@ -470,7 +471,7 @@ export const list = query({
|
|
|
470
471
|
const chunks = await paginator(ctx.db, schema)
|
|
471
472
|
.query("chunks")
|
|
472
473
|
.withIndex("entryId_order", (q) => q.eq("entryId", entryId))
|
|
473
|
-
.order(
|
|
474
|
+
.order(args.order)
|
|
474
475
|
.paginate(paginationOpts);
|
|
475
476
|
return {
|
|
476
477
|
...chunks,
|
|
@@ -46,7 +46,7 @@ describe("entries", () => {
|
|
|
46
46
|
expect(result.created).toBe(true);
|
|
47
47
|
expect(result.status).toBe("ready");
|
|
48
48
|
expect(result.entryId).toBeDefined();
|
|
49
|
-
expect(result.
|
|
49
|
+
expect(result.replacedEntry).toBeNull();
|
|
50
50
|
|
|
51
51
|
// Verify the entry was actually created
|
|
52
52
|
const createdDoc = await t.run(async (ctx) => {
|
|
@@ -73,7 +73,7 @@ describe("entries", () => {
|
|
|
73
73
|
|
|
74
74
|
expect(firstResult.created).toBe(true);
|
|
75
75
|
expect(firstResult.status).toBe("ready");
|
|
76
|
-
expect(firstResult.
|
|
76
|
+
expect(firstResult.replacedEntry).toBeNull();
|
|
77
77
|
|
|
78
78
|
// Second add with identical content
|
|
79
79
|
const secondResult = await t.mutation(api.entries.add, {
|
|
@@ -84,7 +84,7 @@ describe("entries", () => {
|
|
|
84
84
|
expect(secondResult.created).toBe(false);
|
|
85
85
|
expect(secondResult.status).toBe("ready");
|
|
86
86
|
expect(secondResult.entryId).toBe(firstResult.entryId);
|
|
87
|
-
expect(secondResult.
|
|
87
|
+
expect(secondResult.replacedEntry).toBeNull();
|
|
88
88
|
|
|
89
89
|
// Verify no new entry was created
|
|
90
90
|
const allDocs = await t.run(async (ctx) => {
|
|
@@ -116,7 +116,7 @@ describe("entries", () => {
|
|
|
116
116
|
});
|
|
117
117
|
|
|
118
118
|
expect(firstResult.created).toBe(true);
|
|
119
|
-
expect(firstResult.
|
|
119
|
+
expect(firstResult.replacedEntry).toBeNull();
|
|
120
120
|
|
|
121
121
|
// Second add with different content hash
|
|
122
122
|
const modifiedEntry = {
|
|
@@ -131,9 +131,9 @@ describe("entries", () => {
|
|
|
131
131
|
|
|
132
132
|
expect(secondResult.created).toBe(true);
|
|
133
133
|
expect(secondResult.entryId).not.toBe(firstResult.entryId);
|
|
134
|
-
// When creating a entry as "ready" initially,
|
|
134
|
+
// When creating a entry as "ready" initially, replacedEntry is null
|
|
135
135
|
// Replacement only happens during pending -> ready transitions
|
|
136
|
-
expect(secondResult.
|
|
136
|
+
expect(secondResult.replacedEntry).toMatchObject({
|
|
137
137
|
entryId: firstResult.entryId,
|
|
138
138
|
});
|
|
139
139
|
|
|
@@ -181,7 +181,7 @@ describe("entries", () => {
|
|
|
181
181
|
|
|
182
182
|
expect(secondResult.created).toBe(true);
|
|
183
183
|
expect(secondResult.entryId).not.toBe(firstResult.entryId);
|
|
184
|
-
expect(secondResult.
|
|
184
|
+
expect(secondResult.replacedEntry).toMatchObject({
|
|
185
185
|
entryId: firstResult.entryId,
|
|
186
186
|
});
|
|
187
187
|
|
|
@@ -219,7 +219,7 @@ describe("entries", () => {
|
|
|
219
219
|
|
|
220
220
|
expect(secondResult.created).toBe(true);
|
|
221
221
|
expect(secondResult.entryId).not.toBe(firstResult.entryId);
|
|
222
|
-
expect(secondResult.
|
|
222
|
+
expect(secondResult.replacedEntry).toMatchObject({
|
|
223
223
|
entryId: firstResult.entryId,
|
|
224
224
|
});
|
|
225
225
|
|
|
@@ -247,7 +247,7 @@ describe("entries", () => {
|
|
|
247
247
|
|
|
248
248
|
expect(result.created).toBe(true);
|
|
249
249
|
expect(result.status).toBe("pending");
|
|
250
|
-
expect(result.
|
|
250
|
+
expect(result.replacedEntry).toBeNull();
|
|
251
251
|
|
|
252
252
|
// Verify the entry was created with pending status
|
|
253
253
|
const createdDoc = await t.run(async (ctx) => {
|
|
@@ -277,8 +277,8 @@ describe("entries", () => {
|
|
|
277
277
|
expect(result1.created).toBe(true);
|
|
278
278
|
expect(result2.created).toBe(true);
|
|
279
279
|
expect(result1.entryId).not.toBe(result2.entryId);
|
|
280
|
-
expect(result1.
|
|
281
|
-
expect(result2.
|
|
280
|
+
expect(result1.replacedEntry).toBeNull();
|
|
281
|
+
expect(result2.replacedEntry).toBeNull();
|
|
282
282
|
|
|
283
283
|
// Verify both entries exist
|
|
284
284
|
const allDocs = await t.run(async (ctx) => {
|
|
@@ -293,7 +293,7 @@ describe("entries", () => {
|
|
|
293
293
|
expect(keys).toEqual(["doc1", "doc2"]);
|
|
294
294
|
});
|
|
295
295
|
|
|
296
|
-
test("pending to ready transition populates
|
|
296
|
+
test("pending to ready transition populates replacedEntry", async () => {
|
|
297
297
|
const t = convexTest(schema, modules);
|
|
298
298
|
const namespaceId = await setupTestNamespace(t);
|
|
299
299
|
|
|
@@ -307,7 +307,7 @@ describe("entries", () => {
|
|
|
307
307
|
|
|
308
308
|
expect(firstResult.created).toBe(true);
|
|
309
309
|
expect(firstResult.status).toBe("ready");
|
|
310
|
-
expect(firstResult.
|
|
310
|
+
expect(firstResult.replacedEntry).toBeNull();
|
|
311
311
|
|
|
312
312
|
// Second add - create as pending (no allChunks)
|
|
313
313
|
const modifiedEntry = {
|
|
@@ -322,15 +322,15 @@ describe("entries", () => {
|
|
|
322
322
|
|
|
323
323
|
expect(pendingResult.created).toBe(true);
|
|
324
324
|
expect(pendingResult.status).toBe("pending");
|
|
325
|
-
expect(pendingResult.
|
|
325
|
+
expect(pendingResult.replacedEntry).toBeNull();
|
|
326
326
|
|
|
327
327
|
// Promote to ready - this should replace the first entry
|
|
328
328
|
const promoteResult = await t.mutation(api.entries.promoteToReady, {
|
|
329
329
|
entryId: pendingResult.entryId,
|
|
330
330
|
});
|
|
331
331
|
|
|
332
|
-
expect(promoteResult.
|
|
333
|
-
expect(promoteResult.
|
|
332
|
+
expect(promoteResult.replacedEntry).not.toBeNull();
|
|
333
|
+
expect(promoteResult.replacedEntry!.entryId).toBe(firstResult.entryId);
|
|
334
334
|
|
|
335
335
|
// Verify the first entry is now replaced
|
|
336
336
|
const firstDoc = await t.run(async (ctx) => {
|