fix(schema): track client-side schema deviations, restore schema.ts fidelity
src/types/schema.ts had drifted from the official schema contract: a `clientOnly` field had been added to `FilterEnum` to support client-side log filtering. Restored it to match upstream exactly and moved the deviation-only type into a new intersection type in src/lib/schemaDeviationTypes.ts instead. Audited the codebase for every place the UI does something the official schema doesn't support and tagged each with `// SCHEMA-DEVIATION: <id>`, documented in the new SCHEMA_DEVIATIONS.md registry (what, why, and the ideal server-side fix): - log-client-filters: Level/Event filters on Log Entries, applied client-side because x:Log/query rejects them as JMAP filters. - account-quota-usage-column: synthetic quotaUsage column on the Accounts list. - mailbox-client-hierarchy-sort: full-fetch + client-side sort to reconstruct mailbox parent/child hierarchy. - webapp-enabled-column-fallback: synthetic "Enabled" column label for x:Application when the schema list doesn't define one. Other viewName/objectName special cases (x:OtpAuth, x:Expression, x:Rate, x:Action, x:Trace, CustomComponent/*) were checked against upstream and are pre-existing architecture, not fork deviations.
This commit is contained in:
@@ -76,6 +76,7 @@ import {
|
||||
import type { Schema, Field, MassAction, ItemAction, Filter as FilterDef } from '@/types/schema';
|
||||
import type { JmapSetResponse, JmapSetError } from '@/types/jmap';
|
||||
import type { ResolvedSchema } from '@/lib/schemaResolver';
|
||||
import { isClientOnlyFilterEnum } from '@/lib/schemaDeviationTypes';
|
||||
|
||||
const PAGE_SIZE = 25;
|
||||
const MAX_REPORTED_ERRORS = 3;
|
||||
@@ -87,7 +88,7 @@ const ENUM_COMBOBOX_THRESHOLD = 15;
|
||||
const REFRESH_COOLDOWN_MS = 5000;
|
||||
|
||||
function isClientOnlyFilter(f: FilterDef): boolean {
|
||||
return f.type === 'enum' && f.clientOnly === true;
|
||||
return f.type === 'enum' && isClientOnlyFilterEnum(f);
|
||||
}
|
||||
|
||||
function parseSetResponse(raw: [string, Record<string, unknown>, string][]): JmapSetResponse | null {
|
||||
@@ -187,6 +188,8 @@ function formatNumber(value: unknown): string {
|
||||
return value.toLocaleString();
|
||||
}
|
||||
|
||||
// SCHEMA-DEVIATION: mailbox-client-hierarchy-sort (see SCHEMA_DEVIATIONS.md)
|
||||
//
|
||||
// Orders mailboxes so each parent is immediately followed by its
|
||||
// descendants (siblings alphabetical), and records each row's depth.
|
||||
// Requires the full set (not just one page) since a mailbox's parent
|
||||
@@ -502,8 +505,10 @@ export function DynamicList({ viewName }: DynamicListProps) {
|
||||
if (!isWebApplications) return columns;
|
||||
|
||||
// For Web Applications, present Description first and Enabled second
|
||||
// to match the layout of other tables such as Domains. If the schema
|
||||
// does not expose an Enabled column, add a synthetic one.
|
||||
// to match the layout of other tables such as Domains. Reordering real
|
||||
// schema columns is fine, but the synthetic fallback below (when the
|
||||
// schema doesn't list an Enabled column at all) is a tracked deviation.
|
||||
// SCHEMA-DEVIATION: webapp-enabled-column-fallback (see SCHEMA_DEVIATIONS.md)
|
||||
const ordered = ['description', 'enabled'];
|
||||
const rest = columns.filter((c) => !ordered.includes(c.name));
|
||||
const descriptionCol = columns.find((c) => c.name === 'description');
|
||||
@@ -670,11 +675,13 @@ export function DynamicList({ viewName }: DynamicListProps) {
|
||||
|
||||
if (activeClientFilters.length > 0 || isMailboxList) {
|
||||
// No server-side pagination possible once a client-only filter is
|
||||
// active: fetch every server-matching row up front, narrow it in
|
||||
// the browser, then paginate the in-memory result locally. Mailbox
|
||||
// hierarchy needs this too — a mailbox's parent can land on a
|
||||
// different server page than the mailbox itself, so the full set
|
||||
// is required to place each row under its parent correctly.
|
||||
// active (SCHEMA-DEVIATION: log-client-filters): fetch every
|
||||
// server-matching row up front, narrow it in the browser, then
|
||||
// paginate the in-memory result locally. Mailbox hierarchy needs
|
||||
// this too (SCHEMA-DEVIATION: mailbox-client-hierarchy-sort) — a
|
||||
// mailbox's parent can land on a different server page than the
|
||||
// mailbox itself, so the full set is required to place each row
|
||||
// under its parent correctly.
|
||||
const { list: fullList } = await jmapQueryAllAndGet(
|
||||
obj.objectName,
|
||||
accountId,
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
import type { Schema } from '@/types/schema';
|
||||
|
||||
/**
|
||||
* SCHEMA-DEVIATION: account-quota-usage-column (see SCHEMA_DEVIATIONS.md)
|
||||
*
|
||||
* The Accounts list only shows Email/Full Name/Created At, hiding role and
|
||||
* storage usage that otherwise require opening each account individually.
|
||||
* `createdAt` is dropped to make room; `roles` is a real property so it
|
||||
|
||||
+19
-5
@@ -5,8 +5,11 @@
|
||||
*/
|
||||
|
||||
import type { Schema } from '@/types/schema';
|
||||
import type { ClientOnlyFilterEnum } from './schemaDeviationTypes';
|
||||
|
||||
/**
|
||||
* SCHEMA-DEVIATION: log-client-filters (see SCHEMA_DEVIATIONS.md)
|
||||
*
|
||||
* The Stalwart JMAP backend rejects `level`/`event` as filter conditions on
|
||||
* `x:Log/query` (`unsupportedFilter`), even though both properties are
|
||||
* already returned per row. Until the backend adds real support, these two
|
||||
@@ -18,17 +21,28 @@ export function withClientLogFilters(schema: Schema): Schema {
|
||||
const logList = schema.lists['x:Log'];
|
||||
if (!logList || !schema.enums['TracingLevel'] || !schema.enums['EventType']) return schema;
|
||||
|
||||
const levelFilter: ClientOnlyFilterEnum = {
|
||||
type: 'enum',
|
||||
field: 'level',
|
||||
enumName: 'TracingLevel',
|
||||
label: 'Level',
|
||||
clientOnly: true,
|
||||
};
|
||||
const eventFilter: ClientOnlyFilterEnum = {
|
||||
type: 'enum',
|
||||
field: 'event',
|
||||
enumName: 'EventType',
|
||||
label: 'Event',
|
||||
clientOnly: true,
|
||||
};
|
||||
|
||||
return {
|
||||
...schema,
|
||||
lists: {
|
||||
...schema.lists,
|
||||
'x:Log': {
|
||||
...logList,
|
||||
filters: [
|
||||
...(logList.filters ?? []),
|
||||
{ type: 'enum', field: 'level', enumName: 'TracingLevel', label: 'Level', clientOnly: true },
|
||||
{ type: 'enum', field: 'event', enumName: 'EventType', label: 'Event', clientOnly: true },
|
||||
],
|
||||
filters: [...(logList.filters ?? []), levelFilter, eventFilter],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
import type { FilterEnum } from '@/types/schema';
|
||||
|
||||
/**
|
||||
* Type augmentations for tracked schema deviations (see SCHEMA_DEVIATIONS.md).
|
||||
*
|
||||
* `src/types/schema.ts` mirrors the server's schema contract and must stay
|
||||
* aligned with the official webui/server types — it is never edited to
|
||||
* accommodate a deviation. When a deviation needs to carry extra data on an
|
||||
* otherwise-official schema shape, the augmented type lives here instead,
|
||||
* as an intersection with the official type, and is imported only by the
|
||||
* deviation's own code.
|
||||
*/
|
||||
|
||||
// SCHEMA-DEVIATION: log-client-filters (see SCHEMA_DEVIATIONS.md)
|
||||
export type ClientOnlyFilterEnum = FilterEnum & { clientOnly?: boolean };
|
||||
|
||||
export function isClientOnlyFilterEnum(f: FilterEnum): f is ClientOnlyFilterEnum {
|
||||
return (f as ClientOnlyFilterEnum).clientOnly === true;
|
||||
}
|
||||
@@ -303,10 +303,6 @@ export interface FilterEnum {
|
||||
field: string;
|
||||
enumName: string;
|
||||
label: string;
|
||||
/** Applied client-side after fetch instead of sent to the server as a JMAP
|
||||
* filter condition. Used for properties the backend query engine doesn't
|
||||
* (yet) support filtering on, even though it returns them per row. */
|
||||
clientOnly?: boolean;
|
||||
}
|
||||
|
||||
export interface FilterInteger {
|
||||
|
||||
Reference in New Issue
Block a user