fix(web): support PostgREST not(col, is) so archived session lists load
Local Postgres not() only handled eq/cs, so archived=1 threw and the sidebar archive view returned 500. Compile is not null/true/false instead of inequality.
This commit is contained in:
@@ -20,6 +20,7 @@ type Filter =
|
||||
| Readonly<{ kind: "like"; column: string; value: unknown }>
|
||||
| Readonly<{ kind: "in"; column: string; value: readonly unknown[] }>
|
||||
| Readonly<{ kind: "is"; column: string; value: unknown }>
|
||||
| Readonly<{ kind: "isNot"; column: string; value: unknown }>
|
||||
| Readonly<{ kind: "notContains"; column: string; value: unknown }>
|
||||
| Readonly<{ kind: "or"; expression: string }>;
|
||||
|
||||
@@ -46,6 +47,19 @@ function identifier(value: string): string {
|
||||
return `"${normalized}"`;
|
||||
}
|
||||
|
||||
export function compileUnaryIsClause(
|
||||
column: string,
|
||||
value: unknown,
|
||||
negated: boolean,
|
||||
): string {
|
||||
const quoted = identifier(column);
|
||||
const target = value === null ? "null" : value === true ? "true" : value === false ? "false" : null;
|
||||
if (target === null) {
|
||||
throw new Error(negated ? "unsupported not filter" : "unsupported is filter");
|
||||
}
|
||||
return `${quoted} is${negated ? " not" : ""} ${target}`;
|
||||
}
|
||||
|
||||
export function formatOrderClause(
|
||||
ordering: readonly { column: string; ascending: boolean }[],
|
||||
): string {
|
||||
@@ -388,6 +402,11 @@ class LocalPostgresQueryBuilder implements PromiseLike<QueryResult> {
|
||||
this.filters.push({ kind: "neq", column, value });
|
||||
return this;
|
||||
}
|
||||
if (operator === "is") {
|
||||
compileUnaryIsClause(column, value, true);
|
||||
this.filters.push({ kind: "isNot", column, value });
|
||||
return this;
|
||||
}
|
||||
if (operator !== "cs") throw new Error("unsupported not filter");
|
||||
this.filters.push({ kind: "notContains", column, value });
|
||||
return this;
|
||||
@@ -451,10 +470,10 @@ class LocalPostgresQueryBuilder implements PromiseLike<QueryResult> {
|
||||
}
|
||||
const column = identifier(filter.column);
|
||||
if (filter.kind === "is") {
|
||||
if (filter.value === null) return `${column} is null`;
|
||||
if (filter.value === true) return `${column} is true`;
|
||||
if (filter.value === false) return `${column} is false`;
|
||||
throw new Error("unsupported is filter");
|
||||
return compileUnaryIsClause(filter.column, filter.value, false);
|
||||
}
|
||||
if (filter.kind === "isNot") {
|
||||
return compileUnaryIsClause(filter.column, filter.value, true);
|
||||
}
|
||||
if (filter.kind === "in") {
|
||||
if (filter.value.length === 0) return "false";
|
||||
|
||||
@@ -7,7 +7,11 @@ import {
|
||||
closeLocalPostgresDataPools,
|
||||
createLocalPostgresDataClient,
|
||||
} from "../src/lib/db/local-postgres-client-core.ts";
|
||||
import { applyArchiveFilter, excludeEmptyConsultations } from "../src/lib/session-list-filter.ts";
|
||||
import {
|
||||
applyArchiveFilter,
|
||||
cloudListIncludesSession,
|
||||
excludeEmptyConsultations,
|
||||
} from "../src/lib/session-list-filter.ts";
|
||||
import { startPostgresFixture } from "./helpers/postgres-fixture.ts";
|
||||
|
||||
const runner = fileURLToPath(new URL("../scripts/db-migrate.mjs", import.meta.url));
|
||||
@@ -17,6 +21,7 @@ const emptyConsultation = "11111111-1111-4111-8111-111111111111";
|
||||
const emptyRectification = "22222222-2222-4222-8222-222222222222";
|
||||
const filledConsultation = "33333333-3333-4333-8333-333333333333";
|
||||
const archivedRectification = "44444444-4444-4444-8444-444444444444";
|
||||
const archivedEmptyConsultation = "55555555-5555-4555-8555-555555555555";
|
||||
|
||||
test("session list query keeps empty rectification rows and drops empty consultations", {
|
||||
skip: docker ? false : "docker unavailable",
|
||||
@@ -43,7 +48,8 @@ test("session list query keeps empty rectification rows and drops empty consulta
|
||||
('${emptyConsultation}', '${userId}', 'Empty consult', 'general', 'test-model', '[]', 'consultation', false, null, now()),
|
||||
('${emptyRectification}', '${userId}', 'Empty rectification', 'general', 'test-model', '[]', 'birth_time_rectification', false, null, now()),
|
||||
('${filledConsultation}', '${userId}', 'Filled consult', 'general', 'test-model', '[{"role":"user","text":"问一句"}]', 'consultation', false, null, now()),
|
||||
('${archivedRectification}', '${userId}', 'Archived rectification', 'general', 'test-model', '[]', 'birth_time_rectification', false, '2026-09-08T00:00:00Z', now());
|
||||
('${archivedRectification}', '${userId}', 'Archived rectification', 'general', 'test-model', '[]', 'birth_time_rectification', false, '2026-09-08T00:00:00Z', now()),
|
||||
('${archivedEmptyConsultation}', '${userId}', 'Archived empty consult', 'general', 'test-model', '[]', 'consultation', false, '2026-09-08T00:00:00Z', now());
|
||||
`);
|
||||
const local = createLocalPostgresDataClient(
|
||||
fixture.connectionUrl("app_runtime", "app-runtime-test-password"),
|
||||
@@ -65,9 +71,24 @@ test("session list query keeps empty rectification rows and drops empty consulta
|
||||
),
|
||||
);
|
||||
assert.equal(archived.error, null, archived.error?.message);
|
||||
const archivedIds = (archived.data as { id: string }[]).map((row) => row.id).sort();
|
||||
assert.deepEqual(archivedIds, [archivedRectification]);
|
||||
assert.equal(archivedIds.includes(archivedEmptyConsultation), false);
|
||||
assert.equal(liveIds.some((id) => archivedIds.includes(id)), false);
|
||||
const rows = [
|
||||
{ id: emptyConsultation, sessionType: "consultation" as const, messagesEmpty: true, archived: false },
|
||||
{ id: emptyRectification, sessionType: "birth_time_rectification" as const, messagesEmpty: true, archived: false },
|
||||
{ id: filledConsultation, sessionType: "consultation" as const, messagesEmpty: false, archived: false },
|
||||
{ id: archivedRectification, sessionType: "birth_time_rectification" as const, messagesEmpty: true, archived: true },
|
||||
{ id: archivedEmptyConsultation, sessionType: "consultation" as const, messagesEmpty: true, archived: true },
|
||||
];
|
||||
assert.deepEqual(
|
||||
(archived.data as { id: string }[]).map((row) => row.id),
|
||||
[archivedRectification],
|
||||
rows.filter((row) => cloudListIncludesSession({ ...row, archivedView: false })).map((row) => row.id).sort(),
|
||||
liveIds,
|
||||
);
|
||||
assert.deepEqual(
|
||||
rows.filter((row) => cloudListIncludesSession({ ...row, archivedView: true })).map((row) => row.id).sort(),
|
||||
archivedIds,
|
||||
);
|
||||
} finally {
|
||||
await closeLocalPostgresDataPools();
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
closeLocalPostgresDataPools,
|
||||
compileUnaryIsClause,
|
||||
createLocalPostgresDataClient,
|
||||
} from "../src/lib/db/local-postgres-client-core.ts";
|
||||
|
||||
test("not(col, is, null) compiles to IS NOT NULL rather than inequality", () => {
|
||||
const sql = compileUnaryIsClause("archived_at", null, true);
|
||||
assert.equal(sql, '"archived_at" is not null');
|
||||
assert.doesNotMatch(sql, /<>/);
|
||||
assert.equal(compileUnaryIsClause("archived_at", true, true), '"archived_at" is not true');
|
||||
assert.equal(compileUnaryIsClause("archived_at", false, true), '"archived_at" is not false');
|
||||
assert.equal(compileUnaryIsClause("archived_at", null, false), '"archived_at" is null');
|
||||
});
|
||||
|
||||
test("not(col, is, undefined) still throws", async () => {
|
||||
assert.throws(() => compileUnaryIsClause("archived_at", undefined, true), /unsupported not filter/);
|
||||
const local = createLocalPostgresDataClient(
|
||||
"postgresql://unused:unused@127.0.0.1:9/unused",
|
||||
{ id: "11111111-1111-4111-8111-111111111111", email: "unused@example.com" },
|
||||
);
|
||||
try {
|
||||
assert.throws(
|
||||
() => local.from("chat_sessions").select("id").not("archived_at", "is", undefined),
|
||||
/unsupported not filter/,
|
||||
);
|
||||
} finally {
|
||||
await closeLocalPostgresDataPools();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user