Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions apps/writer/src/backend/collection.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { CollectionDescription, JsonObject } from "@mdbase-dev/connect";
import { describe, expect, it } from "vitest";
import { validateCanonicalQueryInput } from "../../node_modules/@callumalpass/mdbase/dist/operations/canonical-query.js";
import { annotationContract, commentContract, manuscriptContract, sourceContract, CollectionSchema, CollectionStore, starterSchema, type CollectionRow } from "./collection.js";
const impl = (name: string, fields: Record<string, string>) => ({ typeName: name, typeVersion: 1, digest: "test", fields });
const description: Pick<CollectionDescription, "types" | "contracts" | "configuration"> = {
Expand Down Expand Up @@ -61,6 +62,19 @@ describe("collection semantics", () => {
expect([...narrow.commentScope(["mixed.md"])]).toEqual(["comments/root.md", "comments/reply.md"]);
expect(narrow.manuscripts[0]?.modified).toBe("2026-01-01");
});
it.each([
["configured collection", new CollectionSchema(description)],
["starter collection", starterSchema()],
["unconfigured collection", new CollectionSchema({ types: [], contracts: [] })],
])("uses schema-valid discovery selection names for a %s", (_name, schema) => {
const select = schema.discoverySelect().map(({ name, expression }) => ({ name, expr: expression }));
expect(validateCanonicalQueryInput({ select })).toEqual([]);
});
it("reads manuscript modification time from the named metadata selection", () => {
const store = fresh();
store.upsert([{ path: "paper.md", types: ["paper"], values: { heading: "Paper", file_mtime: "2026-01-02" } }], true);
expect(store.manuscripts).toEqual([{ path: "paper.md", title: "Paper", modified: "2026-01-02" }]);
});
it("escapes literal mapped field names rather than interpreting dots or CEL syntax", () => {
const schema = new CollectionSchema({ types: [], contracts: [{ ...annotationContract, contractType: "record", digest: "test", schema: {},
implementations: [impl("highlight", { source: 'a.b["source"]' })] }] });
Expand Down
5 changes: 3 additions & 2 deletions apps/writer/src/backend/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ export class CollectionSchema {
}
return [
...[...fields].map((field) => ({ name: field, expression: `record[${JSON.stringify(field)}]` })),
...(modified ? [{ name: "file.mtime", expression: "file.mtime" }] : fields.size ? [] : [{ name: "file.path", expression: "file.path" }]),
// Output aliases are field names: dots are valid in expressions, not names.
...(modified ? [{ name: "file_mtime", expression: "file.mtime" }] : fields.size ? [] : [{ name: "file_path", expression: "file.path" }]),
];
}
annotationFields(types: readonly string[]): Readonly<Record<string, string>> {
Expand Down Expand Up @@ -177,7 +178,7 @@ export class CollectionStore {
}
case "manuscript": {
const fields = fieldsOf(effective, binding);
const modified = row.values?.["file.mtime"] ?? row.file?.mtime;
const modified = row.values?.["file_mtime"] ?? row.file?.mtime;
const manuscript = retainEffective && previous.manuscript ? previous.manuscript : {
path: row.path, title: typeof fields["title"] === "string" ? fields["title"] : row.path,
...(typeof fields["template"] === "string" ? { template: fields["template"] } : {}),
Expand Down