import { RestError, type Dialect, type SqlRenderer } from '@crudy/restloom'; import mssqlDriver, { type ConnectionPool } from 'mssql'; import { introspect } from './introspect.js'; import { MssqlRenderer } from './renderer.js'; export { MssqlRenderer } from './renderer.js'; const CODES: Record = { 2627: [409, 'duplicate'], 2601: [409, 'duplicate'], 515: [400, 'value_required'], 245: [400, 'invalid_value'], 241: [400, 'invalid_value'], 242: [400, 'invalid_value'], 8114: [400, 'invalid_value'], 8115: [400, 'invalid_value'], 2628: [400, 'invalid_value'], 229: [403, 'forbidden'], 230: [403, 'forbidden'], }; /** Error 547 covers both foreign keys and checks; its message tells which. */ export function mapError(err: unknown): RestError | null { const e = err as { number?: unknown; code?: unknown; message?: unknown; originalError?: { info?: { number?: unknown; message?: unknown } } }; if (e?.code === 'ETIMEOUT') return new RestError(503, 'timeout'); const number = e?.number ?? e?.originalError?.info?.number; if (number === 547) { const text = String(e.message ?? e.originalError?.info?.message ?? ''); return /CHECK constraint/i.test(text) ? new RestError(400, 'value_rejected') : new RestError(409, 'reference_missing'); } const hit = typeof number === 'number' ? CODES[number] : undefined; return hit ? new RestError(hit[0], hit[1]) : null; } export interface MssqlOptions { renderer?: SqlRenderer; } /** * The SQL Server 2022 dialect over an mssql ConnectionPool. Each request is one * transaction; the statement timeout is the pool's `requestTimeout`, since the * driver sets it per pool. Row security is SQL Server's security policies, * which a `transaction` hook feeds through sp_set_session_context. */ export function mssql(pool: ConnectionPool, options: MssqlOptions = {}): Dialect { return { name: 'mssql', capabilities: { returning: true, estimatedCount: false, rowSecurity: true, procedures: true }, renderer: options.renderer ?? new MssqlRenderer(), decode: (value) => (typeof value === 'string' ? JSON.parse(value) : value), introspect: (scope) => introspect(pool, scope), async transaction(fn) { const tx = new mssqlDriver.Transaction(pool); await tx.begin(); try { const out = await fn({ async query(sql, params) { const req = new mssqlDriver.Request(tx); params.forEach((v, i) => { // Whole numbers beyond 32 bits go as BigInt, or the driver overflows them as Int. if (typeof v === 'number' && Number.isInteger(v) && Math.abs(v) > 2147483647) req.input(`p${i + 1}`, mssqlDriver.BigInt, v); else req.input(`p${i + 1}`, v); }); return ((await req.query(sql)).recordset ?? []) as Record[]; }, }); await tx.commit(); return out; } catch (err) { await tx.rollback().catch(() => undefined); throw err; } }, mapError, }; }