/** * ORM-agnostic where-clause AST. `listQuery.ts#buildWhere` produces this; * each adapter's own `filterCompiler` (see `adapters/prisma/filterCompiler.ts`) * turns it into that ORM's native query shape. Never expose an ORM-specific * operator here (no `mode: 'insensitive'`, no Prisma `not`) — those are * compiler-side decisions made from `LeafFilter.op`, not carried in the AST. */ export type Filter = CompositeFilter | LeafFilter; export interface CompositeFilter { op: 'and' | 'or'; clauses: Filter[]; } export interface LeafFilter { op: 'eq' | 'contains' | 'containsExact' | 'startsWith' | 'gte' | 'lte' | 'lt' | 'in' | 'isNull' | 'isNotNull'; field: string; value?: unknown; } import type { Schema, Model } from '../types/schema.js'; import type { RelationEdge } from '../introspection/relations.js'; /** Boot-time schema source. One call per handler lifetime — no per-request cost. */ export interface SchemaIntrospector { introspect(): Schema | Promise; } export interface TargetGuard { targetModel: Model; targetPk: string | number; filter?: Filter; } /** Tri demandé par `?sort=`, déjà validé contre les colonnes que la vue rend. */ export interface ListOrder { field: string; dir: 'asc' | 'desc'; } /** * Per-request CRUD + relation-read surface `handler.ts` talks to instead of * a raw ORM client. See docs/superpowers/specs/2026-08-13-db-adapter-abstraction-design.md * for the rationale behind each method's shape. */ export interface DataAdapter { /** * Vue liste paginée : toujours count + fetch ensemble. * * `orderBy` absent = clé primaire décroissante, l'ordre historique. Présent, * il est TOUJOURS départagé par la clé primaire décroissante, sauf quand * c'est elle qu'on trie : sans ce départage, deux lignes de même valeur * peuvent changer de page d'une requête à l'autre, et une fenêtre * `skip`/`take` posée par-dessus perd son sens (une ligne vue deux fois, une * autre jamais). C'est à l'adapter de le composer — lui seul sait nommer la * clé primaire dans le langage de son moteur. * * `field` n'est jamais une chaîne libre : `sortQuery.ts` ne le laisse sortir * que s'il appartient aux colonnes réellement rendues par la liste. */ listRecords(model: Model, opts: { filter?: Filter; skip: number; take: number; orderBy?: ListOrder; }): Promise<{ rows: Record[]; total: number; }>; /** * Lecture générale sans pagination forcée : options de relation FK/m2m, * options de filtre FK sidebar, endpoint `_search`. `orderBy` est le * `Record` déjà exposé tel quel côté config * publique (`AdminHandlerConfig.models[].relations[field].orderBy`) — * transmis de façon opaque, sans traduction. */ findMany(model: Model, opts: { filter?: Filter; orderBy?: Record; skip?: number; take?: number; }): Promise[]>; getRecord(model: Model, id: string | number): Promise | null>; findFirst(model: Model, filter: Filter): Promise | null>; countRecords(model: Model, filter?: Filter): Promise; /** * `m2m`'s value carries the TARGET model's PK field name alongside the raw * ids, not just the ids: this adapter has no `Schema`/`RelationGraph` of * its own to resolve a target model from an edge, and `handler.ts` (the * only caller) already resolves the target model before building this * payload, at zero extra cost to it. */ createRecord(model: Model, input: { scalars: Record; m2m?: Record; }>; targetGuards?: TargetGuard[]; }): Promise>; updateRecord(model: Model, id: string | number, input: { scalars: Record; m2m?: Record; }>; targetGuards?: TargetGuard[]; }, authorizationFilter?: Filter): Promise>; deleteRecord(model: Model, id: string | number, authorizationFilter?: Filter): Promise; /** * Suppression en masse, en UNE opération et non une boucle de * `deleteRecord` : une boucle qui casse au septième id sur une contrainte de * clé étrangère laisse six lignes supprimées et rien pour revenir en * arrière. Ici les deux seules réponses possibles sont « tout est supprimé » * et « rien ne l'est, parce que telle ligne est encore référencée ». * * `authorizationFilter` est composé avec les ids DANS le `where` plutôt que * vérifié à part : un id hors portée ne matche simplement pas, sans erreur, * donc rien ne distingue « n'existe pas » de « appartient à un autre * tenant ». Le compte renvoyé est celui des lignes réellement supprimées. */ deleteMany(model: Model, ids: Array, authorizationFilter?: Filter): Promise; /** `targetModel` est fourni par l'appelant : chaque site d'appel actuel l'a déjà résolu. */ getM2mSelectedIds(model: Model, edge: RelationEdge, targetModel: Model, recordId: string | number): Promise>; }