import { PgTable, AnyPgColumn } from "drizzle-orm/pg-core"; import { CollectionConfig, ResolvedHasMany, ResolvedHasOne } from "@rebasepro/types"; import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry"; import { ApiError } from "@rebasepro/server"; export { buildCompositeId, parseIdValues, isAddressableId, COMPOSITE_ID_SEPARATOR } from "@rebasepro/common"; export type { PrimaryKeyInfo } from "@rebasepro/common"; import type { PrimaryKeyInfo } from "@rebasepro/common"; /** * Shared helper functions for row operations. * These are used by FetchService, PersistService, and RelationService. * * All functions that need collection/table lookups require an explicit * `PostgresCollectionRegistry` instance — there is no global singleton. */ /** * Interface for Drizzle column metadata introspection. * Replaces unsafe `as Record` double-cast chains. */ export interface DrizzleColumnMeta { columnType?: string; dataType?: string; primary?: boolean; } /** Safely extract Drizzle column metadata from a column object. */ export declare function getColumnMeta(col: AnyPgColumn): DrizzleColumnMeta; /** * Whether an address could name a row in this table, judged by the columns. * * {@link getPrimaryKeys} lets a config's `isId: "uuid"` win over the schema, so * its `isUUID` is a claim rather than a fact — right for deriving addresses, * wrong for refusing a query. Here the Drizzle column type decides, because it * is what Postgres will enforce: a `uuid` column meets `/c/products/new` with * `22P02`, which aborts the surrounding transaction and turns every later * statement into an unrelated-looking `25P02`. */ export declare function idCanAddressTable(id: string | number, table: PgTable, idInfoArray: PrimaryKeyInfo[]): boolean; export declare function getCollectionByPath(collectionPath: string, registry: PostgresCollectionRegistry): CollectionConfig; /** * Reject a write naming something that is not a column of the table. * * Drizzle builds INSERT from `Object.entries(table[Symbol.Columns])` and UPDATE * from `Object.keys(tableColumns)`, so a key the table does not carry is not * rejected by anything — it is *left out of the statement*. The insert answers * 201 having stored nothing under that name; the update, if the key was the * only one, builds `update "posts" set where …` and Postgres raises a syntax * error (SQLSTATE 42601), which is neither class 22 nor 23 and so surfaces as a * 500 for what is a caller's typo. * * That makes this the last honest place to check, and the only one every write * passes through. `assertKnownWriteFields` in the REST layer checks the same * thing against the *config* and is skipped on four paths — `strictWrites: * false`, a collection declaring no properties, an auth adapter that owns the * body's shape, and a nested route whose target cannot be walked — and it never * sees an in-process `rebase.data` write at all. * * It also gives `strictWrites: false` a truthful implementation. The flag is * documented for "a column that really does exist which the config never * declared", and skipping the config check alone could not deliver that: the * value was dropped a layer later regardless. Skipping the config check and * keeping this one does exactly what the flag says — the column must exist, * the property need not. */ export declare function assertWritableColumns(values: Record, table: PgTable, collectionPath: string): void; /** * A relation whose names do not resolve against the registered schema. * * Every one of these used to be a `logger.warn` followed by `continue`, so a * save reported success for a relation it had not written and a read answered * `[]` for one it could not resolve. `assertRelationsResolve` (validate-relations) * fails boot on the same defects, which is where they belong — a server that * refuses to start is recoverable in a minute. This is the second line, for the * paths that assemble a registry by hand, and it exists so that "cannot resolve" * is never again reported as "done". * * @param label `.` * @param detail what does not resolve, in terms of the schema */ export declare function relationMisconfigured(label: string, detail: string): ApiError; export declare function getTableForCollection(collection: CollectionConfig, registry: PostgresCollectionRegistry): PgTable; /** * The key columns a collection's rows are addressed by. * * Three tiers, in order: properties marked `isId`, the primary keys of the * drizzle schema, and finally a column literally named `id`. Only the first is * visible to the browser, which is why a key known only to drizzle is reported * at boot — see {@link warnOnKeysTheAdminCannotResolve}. * * Returns `[]` when nothing resolves, rather than throwing. It used to open by * resolving the table, which throws when there is none — so the `isId` tier, * which needs no table at all, was unreachable for exactly the collections * most likely to have no table registered. Every caller that wanted "no keys" * to mean "no keys" had to spell that out in a try/catch. * * Callers that cannot proceed without a key must say so themselves, naming the * collection: an empty array here means "this collection has no address", which * is a different answer in a notification (broadcast a wildcard) than in a save * (fail). */ export declare function getPrimaryKeys(collection: CollectionConfig, registry: PostgresCollectionRegistry): PrimaryKeyInfo[]; /** * The key columns, for callers that cannot do their job without one. * * {@link getPrimaryKeys} answers "what keys, if any" and returns `[]` for a * collection with no address. Most of this driver, though, is building a WHERE * clause and has no meaning without a key — for those, an empty array is not an * answer, and indexing `[0]` into it produces `Cannot read properties of * undefined` three frames from where the real problem is. This says what is * wrong and which collection it is wrong about. */ export declare function requirePrimaryKeys(collection: CollectionConfig, registry: PostgresCollectionRegistry): PrimaryKeyInfo[]; /** * The column on the *source* table that a `hasOne`/`hasMany` link points at. * * `sourceKey` is authored when the two sides join on a natural key — an * external identity id, a SKU — and left off when they join on the row id, * which is the overwhelming majority. That makes `undefined` the only optional * field on a resolved relation, so it gets exactly one reader: this function. * Every consumer that needs the column asks here, and none of them re-derives * "or else the primary key" for itself. That is the whole point — the fallback * chains this codebase removed from relation resolution were dangerous because * they were *duplicated* and could disagree, not because they existed. */ export declare function sourceKeyField(relation: ResolvedHasOne | ResolvedHasMany, sourceCollection: CollectionConfig, registry: PostgresCollectionRegistry): string; /** * Whether this link joins on something other than the source's primary key. * * Callers that hold a parent *id* — which is most of them, since an id is what * a URL carries — must translate it to the source key's value before it can be * compared with the target's foreign key. Those that hold the parent *row*, or * that build a correlated subquery over the source table, can read the column * directly and skip the lookup. */ export declare function joinsOnNaturalKey(relation: ResolvedHasOne | ResolvedHasMany, sourceCollection: CollectionConfig, registry: PostgresCollectionRegistry): boolean; /** * Collections whose key the *browser* cannot resolve, and what it will do * instead. * * The two sides resolve keys from different evidence. This driver reads, in * order: properties marked `isId`, the primary keys of the Drizzle schema, then * a column literally named `id`. The admin shares the `CollectionConfig` — it * compiles the same collection files into its bundle — but never the Drizzle * schema, so the middle tier is invisible to it. * * Nothing can normalize this at runtime: the server does not serve the admin * its collections, so a key resolved here cannot be handed over there. The * config files are the only thing both sides read, so the fix is an edit to * them, and the most this can do is say exactly which edit. * * Two shapes, and the second is the dangerous one: * * - No `isId`, no `id` property → the admin resolves no address, warns in the * console, and rows cannot be opened or linked. * - No `isId`, but an `id` property that is *not* the key → the admin addresses * rows by `id` while this driver reads the address as the real key. Nothing * errors: the addresses look right and route wrong. */ export declare function findUnresolvableKeyCollections(collections: CollectionConfig[], registry: PostgresCollectionRegistry): { collection: CollectionConfig; keys: PrimaryKeyInfo[]; shadowedByIdProperty: boolean; }[]; /** * Report the collections from {@link findUnresolvableKeyCollections} at boot, * with the edit that fixes each one. * * Grouped by failure, not by collection: the shadowed case is a routing bug and * the silent case is a missing feature, and they deserve different urgency. */ export declare function warnOnKeysTheAdminCannotResolve(collections: CollectionConfig[], registry: PostgresCollectionRegistry): void; /** * The address of a row: derived from the collection's primary keys, because a * row does not carry one — it is exactly its columns. * * Falls back to a literal `id` column, for a row that reached us from somewhere * other than this driver. Returns `""` when there is no key and no `id` — * callers decide what that means, since "unaddressable" is a different answer * in a notification (broadcast a wildcard) than in a save (fail). */ export declare function deriveRowAddress(row: Record, collection: CollectionConfig, registry: PostgresCollectionRegistry): string;