import type { Field, Operator, SchemaBase } from './dsl.js'; import type { ValueExpr } from './expr.js'; import type { FrontAppSchema, ProjectApiSchema } from './project.js'; // Filter: the single declarative model for query conditions, shared by // dao_schema methods and curd page lists. Conditions are AND-combined and // may reference any table (cross-table filters render JOINs at the usage // site, which owns the main table). Pagination is NOT part of a filter — // it stays on the dao method (mode) and the curd list config. /** One AND-combined criterion: a column plus its comparison operator. */ export interface FilterCondition { /** Column to compare (any table — cross-table filters are allowed). */ field: Field; /** Comparison operator; defaults to 'eq'. */ op?: Operator; /** Right side of the comparison. Absent = the args parameter named after * the column (existing semantics); present = an explicit value expression * (column-vs-column, literal, computation). */ right?: ValueExpr; /** Required (default) or optional criterion. A filter has one semantics: * dao filters are required (missing criteria = error), page filters are * optional (missing criteria = no WHERE). Declared by the filter author. */ optional?: boolean; } /** Query filter: AND-combined conditions plus an optional keyword search. */ export interface FilterSchema extends SchemaBase { type: 'filter'; /** The backend api module this filter belongs to (shared instance from * project.config.ts apis). Filters are backend-side, so storage is * filter_schema/{api.name}/{app.name}/filter/ — app unset = the api-level * common domain layer, stored at filter_schema/{api.name}/common/filter/. */ api: ProjectApiSchema; /** The frontend app this filter belongs to (shared instance from project.config). * Unset = api-level common domain filter shared by all modules of the api. */ app?: FrontAppSchema; /** AND-combined conditions (may be empty when keyword is present). */ conditions: FilterCondition[]; /** Fuzzy keyword search: one input value matched against multiple columns * via OR-like. Presence drives the keyword query endpoint. */ keyword?: { columns: Field[] }; } export function defineFilter(options: { name: string; api: ProjectApiSchema; app?: FrontAppSchema; conditions?: FilterCondition[]; keyword?: { columns: Field[] }; description?: string; }): FilterSchema { if (options.app && !options.api.apps.includes(options.app)) { throw new Error(`filter ${options.name}: api '${options.api.name}' does not serve app '${options.app.name}'`); } const conditions = options.conditions ?? []; if (conditions.length === 0 && options.keyword === undefined) { throw new Error(`filter ${options.name}: conditions and keyword cannot both be empty`); } if (options.keyword !== undefined && options.keyword.columns.length === 0) { throw new Error(`filter ${options.name}: keyword columns must be non-empty`); } return { type: 'filter', name: options.name, description: options.description, api: options.api, app: options.app, conditions, keyword: options.keyword, }; }