mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 14:36:31 +00:00
d82395d0ae
OauthClientsRepository/OauthTokensRepository/OauthConsentsRepository replace OauthService's raw SQL (OA1-OA33). revokeChain's recursive CTE and the oauth_clients join (its FK actually targets client_id, not the row id the generated relation assumes) go through em.getKysely(); OA19's chain revoke is one nativeUpdate($in); OA12's consent grant is em.upsert on the (client, user) unique key. revokeAllForUser built now for Task 5. Every row-out read is disableIdentityMap: true; inserts go through em.insert.
126 lines
6.1 KiB
TypeScript
126 lines
6.1 KiB
TypeScript
import { raw, type Platform, type RawQueryFragment } from '@mikro-orm/core';
|
|
import { SqlitePlatform } from '@mikro-orm/sql';
|
|
import { sql, type Expression } from 'kysely';
|
|
|
|
/**
|
|
* The only place a repository may spell a database function.
|
|
*
|
|
* Every helper takes the MikroORM `Platform` the caller's EntityManager is
|
|
* running on (`em.getPlatform()`) plus an already-qualified column reference
|
|
* (`'t.created_at'`), and dispatches on that platform at runtime. TREK ships
|
|
* one binary and the database is chosen by configuration, so the dialect is a
|
|
* runtime property of the live connection, not a build-time swap: when the
|
|
* Postgres driver arrives this file grows a second branch and no repository
|
|
* changes. An unknown platform fails closed rather than guessing a spelling.
|
|
*
|
|
* Values are never interpolated — the day count in `dateAdd` is checked to be
|
|
* an integer before it is spelled.
|
|
*/
|
|
|
|
function column(ref: string): string {
|
|
if (!/^[a-z_][a-z0-9_]*(\.[a-z_][a-z0-9_]*)?$/i.test(ref)) {
|
|
throw new Error(`sql-functions: not a column reference: ${ref}`);
|
|
}
|
|
return ref;
|
|
}
|
|
|
|
function unsupported(platform: Platform): never {
|
|
throw new Error(`sql-functions: no implementation for platform ${platform.constructor.name}`);
|
|
}
|
|
|
|
/** The calendar date (`YYYY-MM-DD`) of a timestamp column. */
|
|
export function dateOf(platform: Platform, ref: string): RawQueryFragment {
|
|
if (platform instanceof SqlitePlatform) return raw(`date(${column(ref)})`);
|
|
return unsupported(platform);
|
|
}
|
|
|
|
/** The timestamp column shifted by whole days, as a calendar date. */
|
|
export function dateAdd(platform: Platform, ref: string, days: number): RawQueryFragment {
|
|
if (!Number.isInteger(days)) throw new Error(`sql-functions: dateAdd needs an integer day count, got ${days}`);
|
|
if (platform instanceof SqlitePlatform) {
|
|
const sign = days < 0 ? '-' : '+';
|
|
return raw(`date(${column(ref)}, '${sign}${Math.abs(days)} days')`);
|
|
}
|
|
return unsupported(platform);
|
|
}
|
|
|
|
/** The database clock, in the same text the column defaults produce. */
|
|
export function currentTimestamp(platform: Platform): RawQueryFragment {
|
|
if (platform instanceof SqlitePlatform) return raw('CURRENT_TIMESTAMP');
|
|
return unsupported(platform);
|
|
}
|
|
|
|
/**
|
|
* A bare reference to another column of the same row, for a filter or update
|
|
* value that must compare against or combine with a sibling column instead
|
|
* of a bound parameter (e.g. `used_count < max_uses` — `InviteTokensRepository
|
|
* .incrementUsedCount`'s capacity guard). Portable across dialects on its own,
|
|
* but still routed through this file (not spelled with `raw()` in a
|
|
* repository) so every escape hatch stays in one place ESLint's
|
|
* `no-restricted-syntax` rule for `src/db/repositories/**` can enforce.
|
|
*/
|
|
export function columnRef(platform: Platform, ref: string): RawQueryFragment {
|
|
if (platform instanceof SqlitePlatform) return raw(column(ref));
|
|
return unsupported(platform);
|
|
}
|
|
|
|
/** A column shifted by a signed integer amount: `<col> + n` / `<col> - n`. */
|
|
export function columnIncrementedBy(platform: Platform, ref: string, amount: number): RawQueryFragment {
|
|
if (!Number.isInteger(amount)) {
|
|
throw new Error(`sql-functions: columnIncrementedBy needs an integer amount, got ${amount}`);
|
|
}
|
|
if (platform instanceof SqlitePlatform) {
|
|
const sign = amount < 0 ? '-' : '+';
|
|
return raw(`${column(ref)} ${sign} ${Math.abs(amount)}`);
|
|
}
|
|
return unsupported(platform);
|
|
}
|
|
|
|
/**
|
|
* A case-insensitive column reference, for use as a filter's object key:
|
|
* `{ [lower(platform, 'email')]: value.toLowerCase() }` produces
|
|
* `WHERE LOWER(email) = ?`. This is the parity-preserving equivalent of the
|
|
* legacy `LOWER(col) = LOWER(?)` sites (`AuthService`/`OidcService`/
|
|
* `UserProfileService`'s username/email CI lookups): the caller lowercases
|
|
* the bound value itself (every one of this migration's call sites already
|
|
* does, via `String.prototype.toLowerCase`), so the comparison is between
|
|
* two already-lowercased values either way — SQLite's `LOWER()` is ASCII-only
|
|
* exactly like `toLowerCase()` is for the identifier characters these
|
|
* columns hold (usernames/emails), so the two spellings agree on every
|
|
* input this codebase stores.
|
|
*
|
|
* Typed `RawQueryFragment & symbol` (not the bare `RawQueryFragment` the
|
|
* other helpers in this file return): TypeScript only accepts a
|
|
* `string | number | symbol` as a computed property name, and a raw
|
|
* fragment is only usable as an object key because `raw()`'s default
|
|
* generic already brands it with a `[Symbol.toPrimitive]`. The other
|
|
* helpers here are only ever used as VALUES, where that brand isn't needed.
|
|
*/
|
|
export function lower(platform: Platform, ref: string): RawQueryFragment & symbol {
|
|
if (platform instanceof SqlitePlatform) return raw(`LOWER(${column(ref)})`);
|
|
return unsupported(platform);
|
|
}
|
|
|
|
/**
|
|
* `CURRENT_TIMESTAMP`, for a query built through `em.getKysely()` — the
|
|
* escape hatch for a recursive CTE (`OauthTokensRepository.collectChainIds`)
|
|
* or a join the QueryBuilder's relation metadata cannot express correctly
|
|
* (`OauthTokensRepository.listActiveByUser`/`listAllActiveWithClientAndUser`
|
|
* — `oauth_tokens.client_id` references `oauth_clients.client_id`, not its
|
|
* primary key, and the generated entity's `client` relation has no
|
|
* `referencedColumnNames` override, so a QueryBuilder `.innerJoin('ot.client',
|
|
* …)` silently joins on the wrong column; see those methods' docstrings).
|
|
* Kysely has its own expression system, entirely separate from MikroORM's
|
|
* `raw()` — `currentTimestamp` above returns a value this file's OTHER
|
|
* consumers (the QueryBuilder, `nativeUpdate`) accept, but a Kysely query
|
|
* cannot use a `RawQueryFragment` as an operand. This is the one place a
|
|
* repository may reach for the Kysely spelling of the same literal, kept
|
|
* here rather than in a repository file so `no-restricted-syntax`'s ban on
|
|
* spelling raw SQL directly in `src/db/repositories/**` still holds — this
|
|
* file is exempt from that rule by design (it IS the one approved place).
|
|
*/
|
|
export function currentTimestampKysely(platform: Platform): Expression<string> {
|
|
if (platform instanceof SqlitePlatform) return sql<string>`CURRENT_TIMESTAMP`;
|
|
return unsupported(platform);
|
|
}
|