From 38cbbe86407607a2223027f667075fc574f11cf9 Mon Sep 17 00:00:00 2001 From: callumalpass Date: Sat, 3 Oct 2026 13:24:20 +1000 Subject: [PATCH] Fix schema-invalid file metadata selection aliases --- apps/writer/src/backend/collection.test.ts | 14 ++++++++++++++ apps/writer/src/backend/collection.ts | 5 +++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/apps/writer/src/backend/collection.test.ts b/apps/writer/src/backend/collection.test.ts index b9f6dae..3027471 100644 --- a/apps/writer/src/backend/collection.test.ts +++ b/apps/writer/src/backend/collection.test.ts @@ -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) => ({ typeName: name, typeVersion: 1, digest: "test", fields }); const description: Pick = { @@ -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"]' })] }] }); diff --git a/apps/writer/src/backend/collection.ts b/apps/writer/src/backend/collection.ts index 5bfedad..ffe9de2 100644 --- a/apps/writer/src/backend/collection.ts +++ b/apps/writer/src/backend/collection.ts @@ -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> { @@ -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"] } : {}),