import { Dialect, Expression, ExpressionBuilder, Kysely, KyselyPlugin } from "kysely"; //#region src/extend/types.d.ts /** * Method bag keyed by table name. Methods receive `this` as the * extended client (the runtime calls them with `Function.prototype.call` * so the binding is correct). Adopter-side TS annotation: * `this: typeof dbX` — fine for v1; full self-typed inference is a * separate refactor. */ type ModelExtensions = { [Table in keyof DB & string]?: Record unknown> }; /** * One result extension — declares which columns the compute reads * (`needs`) and the function that produces the derived value * (`compute`). Sync only in v1; async opens up "runtime queries * inside compute" footguns. */ interface ResultExtension { /** Map from column name → `true`. Auto-injected into SELECT list. */ needs: Partial>; /** Receives the row's selected columns; return value lands on the row. */ compute: (row: Row) => unknown; } /** * Result extensions keyed by table name. Each entry is a record of * computed-field name → ResultExtension. */ type ResultExtensions = { [Table in keyof DB & string]?: Record> }; /** Top-level shape passed to `db.$extends(...)`. */ interface ExtensionDefinition { model?: ModelExtensions; result?: ResultExtensions; } /** * Fold result extensions into the DB row shape. For each table that * has computeds, every row gains the computed properties typed by the * compute function's return type. * * Implementation note: `compute(row: Row)` is contravariant in `Row`, * so the obvious-looking `R[T] extends Record>` * check fails — `ResultExtension` doesn't extend * `ResultExtension` under strictFunctionTypes. We use * `(row: never)` as the variance-neutral position: every adopter * compute extends `(row: never) => unknown`, so the check passes * uniformly. */ type DBWithResults = { [T in keyof DB]: T extends keyof R ? DB[T] & ComputedFields : DB[T] }; /** Map a computed-bag to a record of computed-field name → return type. */ type ComputedFields = Bag extends Record unknown; }> ? { [K in keyof Bag]: Bag[K] extends { compute: (row: never) => infer Out; } ? Out : unknown } : {}; /** * Result type of `db.$extends(...)` — the client typed against the * (possibly result-augmented) DB, intersected with the per-table * method bag from `model`. */ type ExtendedClient> = KickDbClient ? DBWithResults> : DB> & { [T in keyof NonNullable & keyof DB & string]: NonNullable[T]> }; //#endregion //#region src/client/register.d.ts /** * Module-augmentable registry for the typed KickJS-DB surface. Adopters * declare: * * declare module '@forinda/kickjs-db' { * interface KickDbRegister { * db: typeof appDbClient * } * } * * Once the augmentation is in scope, `KickDbClient` (with no explicit * generic) widens to the registered DB shape everywhere — * `@Inject(DB_PRIMARY) private db!: KickDbClient` produces the typed * client with no manual cast at the call site. * * `kick db typegen` (M2.B-T9) emits this declaration into * `.kickjs/types/kick__db.d.ts` for adopters who opt into codegen. Adopters * who prefer to declare it by hand do so once in any module that's reached * by their tsconfig. * * Named `KickDbRegister` rather than a generic `Register` to avoid * collisions with other libraries that augment a same-named interface. */ interface KickDbRegister {} /** * Pull the registered DB shape out of the augmentable KickDbRegister. * `unknown` when the adopter hasn't declared an augmentation — keeps the * M1-permissive fallback intact. */ type RegisteredDB = KickDbRegister extends { db: { qb: { __DB__?: never; }; }; } ? unknown : KickDbRegister extends { db: infer D; } ? D extends { qb: import('kysely').Kysely; } ? X : unknown : unknown; //#endregion //#region src/query/types.d.ts /** * Adopter-augmented at typegen time, mirroring `KickDbRegister`. The * augmentation slots a record keyed by table name; each value is a * record of `{ relationName: RelationMapEntry }`. The kick/db typegen * plugin emits this alongside the column-shape augmentation. * * declare module '@forinda/kickjs-db' { * interface KickDbRelationsRegister { * db: { * users: { posts: { kind: 'many'; target: 'posts' } } * posts: { author: { kind: 'one'; target: 'users' }, comments: { kind: 'many'; target: 'comments' } } * comments: { post: { kind: 'one'; target: 'posts' } } * } * } * } */ interface KickDbRelationsRegister {} /** * One relation entry in the registry — kind (`'one'` or `'many'`) + * the target table name. The target is a plain string key so the * registry stays decoupled from any particular `DB` generic; the * compile-time check that `target` lives in the local `DB` happens * inside `WithClause`. * * `relationName` (optional) is the pairing tag from * `relations()`'s helpers. When set, the resolver uses it to pair * `one` + `many` declarations across multi-FK schemas. See * docs/db/spec-relation-name.md (M4.B). */ interface RelationMapEntry { kind: 'one' | 'many'; target: string; relationName?: string; } /** * The full registered relation graph. Falls back to an open-shaped * record when the adopter hasn't augmented `KickDbRelationsRegister`, * preserving the M1-permissive baseline. */ type RegisteredRelations = KickDbRelationsRegister extends { db: infer R; } ? R extends Record> ? R : Record> : Record>; /** * Pull the relation map for `Table`. Returns an empty record when the * table has no declared relations (or no augmentation present at all), * which makes `with` resolve to `Record` — i.e. typing * a `with: { ... }` becomes a compile error per key. */ type TableRelations = Table extends keyof RegisteredRelations ? RegisteredRelations[Table] extends Record ? RegisteredRelations[Table] : Record : Record; /** * Kysely's `ExpressionBuilder` exposed verbatim as the second arg * to `where` / `orderBy` callbacks. Adopters use the callable form: * * where: (_u, eb) => eb('isActive', '=', true) * orderBy: (_u, eb) => eb.ref('createdAt') * * Helpers like `eb.and(...)`, `eb.or(...)`, `eb.ref('col')`, * `eb.fn.count(...)` are Kysely's standard surface. There is no * `eb.eq` — that's the callable form. There is no `eb.desc` — * order direction lands as a separate Kysely call (e.g. wrap the * ref expression in `sql\`... desc\``) when needed in v1. */ type QueryOps = ExpressionBuilder; /** * The table-bound shape passed as the first arg to callbacks. The * declared shape is `DB[Table]`, so `u.id` resolves to the row * field's TS type at compile time — handy for type-narrowing in * adapter-side helpers. At runtime, the same `u` is a Proxy whose * property reads return Kysely column refs (`eb.ref('users.id')`), * so `eb('id', '=', u.id)` Just Works for either layer-1 or * relational-query call sites. Most adopters keep the first arg * underscored and reach for `eb('col', op, value)` directly. */ type TableRefs = DB[Table]; /** * Options bag for `findMany` / `findFirst` / `findUnique`. * * `with` keys are constrained to the relations declared for `Table`; * nested `with` recursively constrains in the same way. Boolean * shorthand (`with: { posts: true }`) eager-loads with no * per-relation filtering; the object form takes a nested * `FindManyOptions` for the related table. */ interface FindManyOptions { where?: (table: TableRefs, ops: QueryOps) => Expression; orderBy?: (table: TableRefs, ops: QueryOps) => Expression | Array>; limit?: number; offset?: number; /** Override the spec's default depth guard (5). Throws `RelationalQueryDepthError` on excess. */ maxDepth?: number; with?: WithClause>; /** * Cancellation handle. When the signal aborts, the in-flight * query is cancelled at the dialect level (PG `pg_cancel_backend`, * SQLite synchronous abort, MySQL `KILL QUERY`) and the promise * rejects with `RelationalQueryCancelledError`. * * Bind to `RequestContext.signal` from kickjs-http to short-circuit * the query when the client disconnects or the request times out. * * Spec: docs/db/spec-abortsignal-threading.md. Per-relation * `signal` on a `with` value is intentionally not supported; * nested LATERAL/correlated subqueries inherit the parent signal. */ signal?: AbortSignal; } /** * Per-relation shape inside `with`. `true` for boolean shorthand; * a nested `FindManyOptions` for filtered eager loads. Only relations * whose `target` is also a key of the local `DB` are accepted — * cross-DB relations are unrepresentable at the call site. */ type WithClause> = { [K in keyof Rels]?: Rels[K]['target'] extends keyof DB & string ? true | FindManyOptions : never }; /** * Resolved row shape returned by `findMany`. Base table columns * intersected with the per-relation slot map produced by `with`. */ type FindManyRow> = DB[Table] & WithSlots; /** * Map each present `with` key to its resolved relation slot. Absent * keys do not appear in the result shape — adopters who omit `with` * get the bare row type back. */ type WithSlots = W extends Record ? { [K in keyof W & keyof TableRelations
]: ResolveRelationSlot[K], W[K]> } : {}; /** * Resolve one slot. `one` returns `Related | null`; `many` returns * `Related[]`. Nested options recurse through `FindManyRow`. */ type ResolveRelationSlot = R['target'] extends keyof DB & string ? V extends true ? R['kind'] extends 'one' ? DB[R['target']] | null : DB[R['target']][] : V extends FindManyOptions ? R['kind'] extends 'one' ? FindManyRow | null : FindManyRow[] : never : never; /** * Per-table query namespace exposed as `db.query.X`. v1 ships read * methods only — writes route through layers 1 + 2 (`selectFrom`, * `insertInto`, etc.). */ interface TableQueryNamespace { findMany>(options?: O): Promise>>; findFirst>(options?: O): Promise | null>; findUnique>(options: O): Promise | null>; } /** * Top-level `db.query` namespace. One slot per table in `DB`. Defaults * to `RegisteredDB` so adopters using the bare `KickDbClient` * (relying on the `KickDbRegister` augmentation) get the right shape * without an explicit generic. */ type QueryNamespace = { [Table in keyof DB & string]: TableQueryNamespace }; //#endregion //#region src/client/types.d.ts interface QueryEvent { sql: string; parameters: readonly unknown[]; durationMs: number; } interface QueryErrorEvent { sql: string; parameters: readonly unknown[]; error: unknown; } interface BeforeQueryEvent { /** Mutable — listeners may rewrite sql / parameters before execution. */ sql: string; parameters: unknown[]; } interface TransactionEvent { isolation?: 'serializable' | 'repeatable read' | 'read committed' | 'read uncommitted'; } interface TransactionRollbackEvent extends TransactionEvent { error: unknown; } /** * Fired when a query exceeds `createDbClient({ slowQueryThresholdMs })`. * The `query` event ALSO fires for the same query — `slowQuery` is a * separate channel so listeners can subscribe to slow ones only * without filtering every query themselves. */ interface SlowQueryEvent extends QueryEvent { /** The configured threshold the query exceeded. */ thresholdMs: number; } interface KickDbClientEvents { beforeQuery: BeforeQueryEvent; query: QueryEvent; queryError: QueryErrorEvent; slowQuery: SlowQueryEvent; transactionStart: TransactionEvent; transactionCommit: TransactionEvent; transactionRollback: TransactionRollbackEvent; } /** * KickDbClient wraps a Kysely instance with three additions: * * 1. Lifecycle events (`on('query', ...)` etc) for observability + RLS * rewriting via `beforeQuery`. * 2. transaction(fn) / transaction(opts, fn) — passes a fully-scoped child * client whose mutations are isolated. * 3. tx.savepoint(fn) — nested rollback boundary inside an outer transaction. * * The underlying query builder is exposed as `db.qb` for advanced cases * that need APIs not surfaced through the wrapper. Adopters typically * never reach for `qb` — `selectFrom` / `insertInto` / etc. cover the * common query surface. * * NB: Rather than re-typing every query method on this surface, we * directly expose `selectFrom`/`insertInto`/`updateTable`/`deleteFrom` * as bound functions of the underlying builder — keeps us in sync with * upstream type evolution without manual mirroring. */ interface KickDbClient { /** Underlying query builder — escape hatch for advanced cases. */ readonly qb: Kysely; readonly dialect: 'postgres' | 'sqlite' | 'mysql'; selectFrom: Kysely['selectFrom']; insertInto: Kysely['insertInto']; updateTable: Kysely['updateTable']; deleteFrom: Kysely['deleteFrom']; /** * Relational query namespace — `db.query.users.findMany({ with: { * posts: true } })`. PG-only in v1; SQLite + MySQL adopters get a * `RelationalQueryNotSupportedError` on first call. The shape is * inferred from the local `DB` generic (which itself defaults to * `RegisteredDB` so adopters using the bare `KickDbClient` still * see the right table set). * * Available `with` keys come from the `KickDbRelationsRegister` * augmentation emitted by the kick/db typegen plugin alongside * the column-shape augmentation. */ readonly query: QueryNamespace; on(event: E, listener: (e: KickDbClientEvents[E]) => void | Promise): this; off(event: E, listener: (e: KickDbClientEvents[E]) => void | Promise): this; transaction(fn: (tx: KickDbClient) => Promise): Promise; transaction(opts: TransactionEvent, fn: (tx: KickDbClient) => Promise): Promise; savepoint(fn: (sp: KickDbClient) => Promise): Promise; /** * Returns a wrapped client carrying adopter-defined per-table * methods. Inside each method, `this` is the extended client so * call-chains stay clean: * * const dbX = db.$extends({ * model: { * users: { * findByEmail(this: typeof dbX, email: string) { * return this.selectFrom('users') * .where('email', '=', email) * .executeTakeFirst() * }, * }, * }, * }) * * await dbX.users.findByEmail('a@b.com') * * Result extensions (`compute()` over selected rows) and the * insert-side toDriver pass land as follow-ups; v1 ships model * methods only. */ $extends>(ext: E): ExtendedClient; destroy(): Promise; } interface CreateDbClientOptions { /** Schema record — only used for type inference (M2-S1 tightens). */ schema: TSchema; /** A Kysely Dialect — typically PostgresDialect from db-pg. */ dialect: Dialect; /** * Enable lifecycle event emission for `query` / `queryError` / * `slowQuery` / `transactionStart` / `transactionCommit` / * `transactionRollback`. Default `false` — zero overhead path; the * Kysely log callback isn't even installed. */ events?: boolean; /** * Fire the `slowQuery` event for any query whose duration exceeds * this threshold (milliseconds). Default `null` — no slow-query * detection. Setting a value implies `events: true` so the listener * surface is attached. */ slowQueryThresholdMs?: number | null; /** * Optional KickEventBus instance the client republishes lifecycle * events to. When set, the bus receives: * * - `db:slow-query` — fired alongside the local `slowQuery` event; * payload `{ sql, parameters, durationMs, thresholdMs }`. * - `db:query-error` — fired alongside the local `queryError` * event; payload `{ sql, parameters, error }`. * * Setting a bus implies `events: true` (the publisher hangs off the * existing emitter). Wire it from `DEVTOOLS_BUS` if you want the * DevTools panel to pick the events up: * * import { DEVTOOLS_BUS } from '@forinda/kickjs-devtools-kit/bus/token' * // Resolve only when devtools is actually wired — adopters who * // skip @forinda/kickjs-devtools never register the token, and * // resolve() throws on missing tokens. * const db = createDbClient({ * ..., * bus: container.has(DEVTOOLS_BUS) ? container.resolve(DEVTOOLS_BUS) : undefined, * slowQueryThresholdMs: 100, * }) * * Type imported via `import type` so kickjs-db keeps devtools-kit * as an optional peer; adopters who skip devtools never load the * bus module. */ bus?: import('@forinda/kickjs-devtools-kit/bus').KickEventBus; /** * Extra Kysely plugins appended to the internal chain. Kickjs-db * still installs its own plugins (`CodecPlugin` for `customType` * mappers, `ParseJSONResultsPlugin` for SQLite + MySQL JSON * decoding); adopter plugins run after them in the order supplied. * * Use this for JSON column rewriters, soft-delete filters, * instrumentation, or any other `KyselyPlugin`. Empty / unset = * byte-identical chain to pre-M5.B clients (only the built-in * plugins run). * * Reach for `safeNullComparison()` here when you want * `eb('col', '=', null)` to compile to `IS NULL` instead of the * silently-false `= NULL`: * * ```ts * import { createDbClient, safeNullComparison } from '@forinda/kickjs-db' * * const db = createDbClient({ * schema, * dialect: pgDialect({ pool }), * plugins: [safeNullComparison()], * }) * ``` * * **Heads-up — pass `safeNullComparison()` from * `@forinda/kickjs-db`, NOT Kysely's `SafeNullComparisonPlugin`.** * Kysely 0.29's upstream version is broken on PG (rewrites the * operator but keeps the null operand parameterised, producing * `WHERE "col" IS $1` which PG rejects with `syntax error at or * near "$1"`). The kickjs version emits the literal `null` keyword * inline. Tracked upstream at . */ plugins?: KyselyPlugin[]; } //#endregion export { ModelExtensions as C, ExtensionDefinition as S, TableRelations as _, QueryErrorEvent as a, RegisteredDB as b, TransactionEvent as c, FindManyRow as d, KickDbRelationsRegister as f, TableQueryNamespace as g, RelationMapEntry as h, KickDbClientEvents as i, TransactionRollbackEvent as l, RegisteredRelations as m, CreateDbClientOptions as n, QueryEvent as o, QueryNamespace as p, KickDbClient as r, SlowQueryEvent as s, BeforeQueryEvent as t, FindManyOptions as u, WithClause as v, ExtendedClient as x, KickDbRegister as y }; //# sourceMappingURL=types-BZGFAXPN.d.mts.map