mirror of
https://github.com/BetterCorp/BetterFrame.git
synced 2026-05-26 20:16:35 +00:00
fix(db): use native booleans instead of B() integer coercion
PG rejects integer 0/1 for BOOLEAN columns. Replaced all B() calls with native JS booleans — works for both SQLite (coerces true→1, false→0) and PG (native BOOLEAN). Removed B() import and PG adapter coercion hack. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5cefa04a45
commit
6e3a893421
2 changed files with 19 additions and 34 deletions
|
|
@ -59,19 +59,6 @@ export class PgAdapter implements DbAdapter {
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
private coerceParams(params: ReadonlyArray<SqlValue>): unknown[] {
|
|
||||||
return params.map((v) => {
|
|
||||||
if (v === 0 || v === 1) {
|
|
||||||
// Could be integer or boolean. PG is strict about boolean columns
|
|
||||||
// receiving integer values. We can't know the column type here, but
|
|
||||||
// the `pg` driver accepts JS booleans for both INTEGER and BOOLEAN
|
|
||||||
// columns, so converting 0/1 to false/true is always safe.
|
|
||||||
return v === 1;
|
|
||||||
}
|
|
||||||
return v;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async runner<T>(fn: (c: PoolClient) => Promise<T>): Promise<T> {
|
private async runner<T>(fn: (c: PoolClient) => Promise<T>): Promise<T> {
|
||||||
if (this.currentTxClient) return fn(this.currentTxClient);
|
if (this.currentTxClient) return fn(this.currentTxClient);
|
||||||
const client = await this.pool.connect();
|
const client = await this.pool.connect();
|
||||||
|
|
@ -81,9 +68,8 @@ export class PgAdapter implements DbAdapter {
|
||||||
|
|
||||||
async run(sql: string, params: ReadonlyArray<SqlValue> = []): Promise<RunResult> {
|
async run(sql: string, params: ReadonlyArray<SqlValue> = []): Promise<RunResult> {
|
||||||
const pgSql = this.rewriteSql(sql);
|
const pgSql = this.rewriteSql(sql);
|
||||||
const pgParams = this.coerceParams(params);
|
|
||||||
return this.runner(async (c) => {
|
return this.runner(async (c) => {
|
||||||
const res = await c.query(pgSql, pgParams);
|
const res = await c.query(pgSql, params as unknown[]);
|
||||||
let lastInsertRowid = 0n;
|
let lastInsertRowid = 0n;
|
||||||
// If the caller added RETURNING id, pluck it.
|
// If the caller added RETURNING id, pluck it.
|
||||||
if (res.rows.length > 0 && res.rows[0] && "id" in res.rows[0]) {
|
if (res.rows.length > 0 && res.rows[0] && "id" in res.rows[0]) {
|
||||||
|
|
@ -98,18 +84,16 @@ export class PgAdapter implements DbAdapter {
|
||||||
|
|
||||||
async get<T = Row>(sql: string, params: ReadonlyArray<SqlValue> = []): Promise<T | undefined> {
|
async get<T = Row>(sql: string, params: ReadonlyArray<SqlValue> = []): Promise<T | undefined> {
|
||||||
const pgSql = this.rewriteSql(sql);
|
const pgSql = this.rewriteSql(sql);
|
||||||
const pgParams = this.coerceParams(params);
|
|
||||||
return this.runner(async (c) => {
|
return this.runner(async (c) => {
|
||||||
const res = await c.query(pgSql, pgParams);
|
const res = await c.query(pgSql, params as unknown[]);
|
||||||
return (res.rows[0] as T | undefined);
|
return (res.rows[0] as T | undefined);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async all<T = Row>(sql: string, params: ReadonlyArray<SqlValue> = []): Promise<T[]> {
|
async all<T = Row>(sql: string, params: ReadonlyArray<SqlValue> = []): Promise<T[]> {
|
||||||
const pgSql = this.rewriteSql(sql);
|
const pgSql = this.rewriteSql(sql);
|
||||||
const pgParams = this.coerceParams(params);
|
|
||||||
return this.runner(async (c) => {
|
return this.runner(async (c) => {
|
||||||
const res = await c.query(pgSql, pgParams);
|
const res = await c.query(pgSql, params as unknown[]);
|
||||||
return res.rows as T[];
|
return res.rows as T[];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ import {
|
||||||
rowToSetupState,
|
rowToSetupState,
|
||||||
rowToUser,
|
rowToUser,
|
||||||
} from "./mappers.js";
|
} from "./mappers.js";
|
||||||
import { B, J, isoIn, isoNow, j } from "./util.js";
|
import { J, isoIn, isoNow, j } from "./util.js";
|
||||||
|
|
||||||
type NotifyFn = (
|
type NotifyFn = (
|
||||||
table: string,
|
table: string,
|
||||||
|
|
@ -189,12 +189,13 @@ export class Repository {
|
||||||
const role: UserRole = input.role ?? "operator";
|
const role: UserRole = input.role ?? "operator";
|
||||||
const result = await this._run(
|
const result = await this._run(
|
||||||
`INSERT INTO users (username, password_hash, role, is_active, must_change_password)
|
`INSERT INTO users (username, password_hash, role, is_active, must_change_password)
|
||||||
VALUES (?, ?, ?, 1, ?)`,
|
VALUES (?, ?, ?, ?, ?)`,
|
||||||
[
|
[
|
||||||
input.username,
|
input.username,
|
||||||
input.password_hash,
|
input.password_hash,
|
||||||
role,
|
role,
|
||||||
B(Boolean(input.must_change_password)),
|
true,
|
||||||
|
Boolean(input.must_change_password),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
const id = Number(result.lastInsertRowid);
|
const id = Number(result.lastInsertRowid);
|
||||||
|
|
@ -213,7 +214,7 @@ export class Repository {
|
||||||
}
|
}
|
||||||
if ("totp_enabled" in patch) {
|
if ("totp_enabled" in patch) {
|
||||||
cols.push("totp_enabled = ?");
|
cols.push("totp_enabled = ?");
|
||||||
vals.push(B(Boolean(patch.totp_enabled)));
|
vals.push(Boolean(patch.totp_enabled));
|
||||||
}
|
}
|
||||||
if ("totp_secret_encrypted" in patch) {
|
if ("totp_secret_encrypted" in patch) {
|
||||||
cols.push("totp_secret_encrypted = ?");
|
cols.push("totp_secret_encrypted = ?");
|
||||||
|
|
@ -225,7 +226,7 @@ export class Repository {
|
||||||
}
|
}
|
||||||
if ("must_change_password" in patch) {
|
if ("must_change_password" in patch) {
|
||||||
cols.push("must_change_password = ?");
|
cols.push("must_change_password = ?");
|
||||||
vals.push(B(Boolean(patch.must_change_password)));
|
vals.push(Boolean(patch.must_change_password));
|
||||||
}
|
}
|
||||||
if ("failed_login_count" in patch) {
|
if ("failed_login_count" in patch) {
|
||||||
cols.push("failed_login_count = ?");
|
cols.push("failed_login_count = ?");
|
||||||
|
|
@ -241,7 +242,7 @@ export class Repository {
|
||||||
}
|
}
|
||||||
if ("is_active" in patch) {
|
if ("is_active" in patch) {
|
||||||
cols.push("is_active = ?");
|
cols.push("is_active = ?");
|
||||||
vals.push(B(Boolean(patch.is_active)));
|
vals.push(Boolean(patch.is_active));
|
||||||
}
|
}
|
||||||
if (cols.length === 0) return;
|
if (cols.length === 0) return;
|
||||||
vals.push(id);
|
vals.push(id);
|
||||||
|
|
@ -270,7 +271,7 @@ export class Repository {
|
||||||
input.id,
|
input.id,
|
||||||
input.user_id,
|
input.user_id,
|
||||||
input.csrf_token,
|
input.csrf_token,
|
||||||
B(input.totp_pending),
|
Boolean(input.totp_pending),
|
||||||
input.user_agent,
|
input.user_agent,
|
||||||
input.ip_address,
|
input.ip_address,
|
||||||
input.expires_at,
|
input.expires_at,
|
||||||
|
|
@ -295,7 +296,7 @@ export class Repository {
|
||||||
|
|
||||||
async setSessionTotpPending(id: string, pending: boolean): Promise<void> {
|
async setSessionTotpPending(id: string, pending: boolean): Promise<void> {
|
||||||
await this._run("UPDATE sessions SET totp_pending = ? WHERE id = ?", [
|
await this._run("UPDATE sessions SET totp_pending = ? WHERE id = ?", [
|
||||||
B(pending),
|
pending,
|
||||||
id,
|
id,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
@ -573,7 +574,7 @@ export class Repository {
|
||||||
input.priority ?? "normal",
|
input.priority ?? "normal",
|
||||||
input.cooling_timeout_seconds ?? null,
|
input.cooling_timeout_seconds ?? null,
|
||||||
J(input.preload_camera_ids ?? []),
|
J(input.preload_camera_ids ?? []),
|
||||||
B(input.resets_idle_timer ?? true),
|
Boolean(input.resets_idle_timer ?? true),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
const id = Number(result.lastInsertRowid);
|
const id = Number(result.lastInsertRowid);
|
||||||
|
|
@ -590,7 +591,7 @@ export class Repository {
|
||||||
if (k === "id" || k === "display_id") continue; // display_id deprecated
|
if (k === "id" || k === "display_id") continue; // display_id deprecated
|
||||||
sets.push(`${k} = ?`);
|
sets.push(`${k} = ?`);
|
||||||
if (k === "preload_camera_ids" || k === "regions") vals.push(J(v));
|
if (k === "preload_camera_ids" || k === "regions") vals.push(J(v));
|
||||||
else if (typeof v === "boolean") vals.push(B(v));
|
else if (typeof v === "boolean") vals.push(v);
|
||||||
else vals.push(v === undefined ? null : v);
|
else vals.push(v === undefined ? null : v);
|
||||||
}
|
}
|
||||||
if (sets.length === 0) return;
|
if (sets.length === 0) return;
|
||||||
|
|
@ -936,7 +937,7 @@ export class Repository {
|
||||||
const cam = rowToCamera(existing as Record<string, unknown>);
|
const cam = rowToCamera(existing as Record<string, unknown>);
|
||||||
await this._run(
|
await this._run(
|
||||||
`UPDATE cameras SET name = ?, cloud_stream_url = ?, cloud_stream_type = ?, enabled = ? WHERE id = ?`,
|
`UPDATE cameras SET name = ?, cloud_stream_url = ?, cloud_stream_type = ?, enabled = ? WHERE id = ?`,
|
||||||
[input.name, input.cloud_stream_url, input.cloud_stream_type, B(input.enabled), cam.id],
|
[input.name, input.cloud_stream_url, input.cloud_stream_type, Boolean(input.enabled), cam.id],
|
||||||
);
|
);
|
||||||
void this.notify("cameras", "update", cam.id);
|
void this.notify("cameras", "update", cam.id);
|
||||||
return (await this.getCameraById(cam.id))!;
|
return (await this.getCameraById(cam.id))!;
|
||||||
|
|
@ -946,7 +947,7 @@ export class Repository {
|
||||||
(name, type, cloud_account_id, cloud_vendor_camera_id, cloud_stream_url, cloud_stream_type, enabled)
|
(name, type, cloud_account_id, cloud_vendor_camera_id, cloud_stream_url, cloud_stream_type, enabled)
|
||||||
VALUES (?, 'cloud', ?, ?, ?, ?, ?)`,
|
VALUES (?, 'cloud', ?, ?, ?, ?, ?)`,
|
||||||
[input.name, input.cloud_account_id, input.cloud_vendor_camera_id,
|
[input.name, input.cloud_account_id, input.cloud_vendor_camera_id,
|
||||||
input.cloud_stream_url, input.cloud_stream_type, B(input.enabled)],
|
input.cloud_stream_url, input.cloud_stream_type, Boolean(input.enabled)],
|
||||||
);
|
);
|
||||||
const id = Number(result.lastInsertRowid);
|
const id = Number(result.lastInsertRowid);
|
||||||
void this.notify("cameras", "create", id);
|
void this.notify("cameras", "create", id);
|
||||||
|
|
@ -1017,7 +1018,7 @@ export class Repository {
|
||||||
input.encoding ?? null,
|
input.encoding ?? null,
|
||||||
input.framerate ?? null,
|
input.framerate ?? null,
|
||||||
input.bitrate_kbps ?? null,
|
input.bitrate_kbps ?? null,
|
||||||
B(Boolean(input.is_discovered)),
|
Boolean(input.is_discovered),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
const id = Number(result.lastInsertRowid);
|
const id = Number(result.lastInsertRowid);
|
||||||
|
|
@ -1829,7 +1830,7 @@ export class Repository {
|
||||||
input.topic,
|
input.topic,
|
||||||
input.property_op,
|
input.property_op,
|
||||||
J(input.payload),
|
J(input.payload),
|
||||||
B(input.forwarded_to_nodered),
|
Boolean(input.forwarded_to_nodered),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
return Number(result.lastInsertRowid);
|
return Number(result.lastInsertRowid);
|
||||||
|
|
@ -2099,7 +2100,7 @@ export class Repository {
|
||||||
if (k === "id" || k === "created_at") continue;
|
if (k === "id" || k === "created_at") continue;
|
||||||
sets.push(`${k} = ?`);
|
sets.push(`${k} = ?`);
|
||||||
if (k === "capabilities") vals.push(J(v));
|
if (k === "capabilities") vals.push(J(v));
|
||||||
else if (typeof v === "boolean") vals.push(B(v));
|
else if (typeof v === "boolean") vals.push(v);
|
||||||
else vals.push(v === undefined ? null : v);
|
else vals.push(v === undefined ? null : v);
|
||||||
}
|
}
|
||||||
if (sets.length === 0) return;
|
if (sets.length === 0) return;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue