This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
// The collections: list them with their document counts, create one. A collection is a named
|
||||
// bucket; its name is the slug every procedure takes, so a page never handles an id.
|
||||
|
||||
import { conflict, notFound, validationFailed } from "@conusai-kit/core"
|
||||
import { type CollectionView, collections, documents } from "../contract/index.js"
|
||||
import type { KnowledgeDeps, KnowledgeScope } from "./ports.js"
|
||||
|
||||
const PAGE = 200
|
||||
|
||||
export async function listCollections(
|
||||
deps: KnowledgeDeps,
|
||||
scope: KnowledgeScope,
|
||||
): Promise<{ collections: CollectionView[] }> {
|
||||
const found = []
|
||||
let cursor: string | undefined
|
||||
for (;;) {
|
||||
const page = await deps.data
|
||||
.forScope(scope)
|
||||
.of(collections)
|
||||
.list({
|
||||
order: [{ field: "createdAt", direction: "asc" }],
|
||||
limit: PAGE,
|
||||
...(cursor === undefined ? {} : { cursor }),
|
||||
})
|
||||
found.push(...page.items)
|
||||
if (!page.hasMore) break
|
||||
cursor = page.cursor
|
||||
}
|
||||
const views: CollectionView[] = []
|
||||
for (const row of found) {
|
||||
const counts = await documentCounts(deps, scope, row.name)
|
||||
views.push({
|
||||
name: row.name,
|
||||
documentCount: counts.total,
|
||||
indexedCount: counts.indexed,
|
||||
createdBy: row.createdBy,
|
||||
})
|
||||
}
|
||||
return { collections: views }
|
||||
}
|
||||
|
||||
async function documentCounts(
|
||||
deps: KnowledgeDeps,
|
||||
scope: KnowledgeScope,
|
||||
collectionName: string,
|
||||
): Promise<{ total: number; indexed: number }> {
|
||||
let total = 0
|
||||
let indexed = 0
|
||||
let cursor: string | undefined
|
||||
for (;;) {
|
||||
const page = await deps.data
|
||||
.forScope(scope)
|
||||
.of(documents)
|
||||
.list({
|
||||
where: [{ field: "collectionName", op: "eq", value: collectionName }],
|
||||
limit: PAGE,
|
||||
...(cursor === undefined ? {} : { cursor }),
|
||||
})
|
||||
for (const row of page.items) {
|
||||
total += 1
|
||||
if (row.status === "indexed") indexed += 1
|
||||
}
|
||||
if (!page.hasMore) break
|
||||
cursor = page.cursor
|
||||
}
|
||||
return { total, indexed }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a collection, idempotently: a name that already exists answers with the existing
|
||||
* collection and writes nothing, so a retried request never audits twice.
|
||||
*/
|
||||
export async function createCollection(
|
||||
deps: KnowledgeDeps,
|
||||
scope: KnowledgeScope,
|
||||
name: string,
|
||||
): Promise<CollectionView> {
|
||||
const repos = deps.data.forScope(scope)
|
||||
const existing = (await repos.of(collections).list({ where: [{ field: "name", op: "eq", value: name }], limit: 1 }))
|
||||
.items[0]
|
||||
if (existing) {
|
||||
const counts = await documentCounts(deps, scope, name)
|
||||
return {
|
||||
name: existing.name,
|
||||
documentCount: counts.total,
|
||||
indexedCount: counts.indexed,
|
||||
createdBy: existing.createdBy,
|
||||
}
|
||||
}
|
||||
try {
|
||||
const row = await deps.data.unitOfWork.run(scope, async (tx) => {
|
||||
const inserted = await tx.of(collections).insert({ name, createdBy: scope.actor.userId })
|
||||
return inserted
|
||||
})
|
||||
return { name: row.name, documentCount: 0, indexedCount: 0, createdBy: row.createdBy }
|
||||
} catch (error) {
|
||||
// A concurrent create with the same name lost the race: answer the winner's row.
|
||||
if ((error as { status?: number }).status === 409) {
|
||||
const winner = (await repos.of(collections).list({ where: [{ field: "name", op: "eq", value: name }], limit: 1 }))
|
||||
.items[0]
|
||||
if (winner) {
|
||||
const counts = await documentCounts(deps, scope, name)
|
||||
return {
|
||||
name: winner.name,
|
||||
documentCount: counts.total,
|
||||
indexedCount: counts.indexed,
|
||||
createdBy: winner.createdBy,
|
||||
}
|
||||
}
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/** One collection by name, or NotFound: the resolver's own read is the membership check. */
|
||||
export async function requireCollection(deps: KnowledgeDeps, scope: KnowledgeScope, name: string) {
|
||||
const row = (
|
||||
await deps.data
|
||||
.forScope(scope)
|
||||
.of(collections)
|
||||
.list({ where: [{ field: "name", op: "eq", value: name }], limit: 1 })
|
||||
).items[0]
|
||||
if (!row) throw notFound("no such collection")
|
||||
return row
|
||||
}
|
||||
|
||||
export const unknownCollection = (name: string): never => {
|
||||
throw validationFailed([{ path: "collectionName", message: "no such collection" }], `no collection ${name}`)
|
||||
}
|
||||
export const takenCollection = (): never => {
|
||||
throw conflict("this collection name is taken")
|
||||
}
|
||||
Reference in New Issue
Block a user