ci: add pull request quality gates
CI / install-and-build (pull_request) Successful in 1m29s
CI / format (pull_request) Successful in 51s
CI / typecheck-source (pull_request) Successful in 41s
CI / typecheck-tests (pull_request) Successful in 39s
CI / test (pull_request) Successful in 51s
CI / lint (pull_request) Successful in 21s

Co-Authored-By: gpt-5.6-terra <noreply@openai.com>
This commit was merged in pull request #12.
This commit is contained in:
2026-07-22 21:50:29 +00:00
co-authored by Codex
parent b9fa79ff1f
commit 31d4636dde
80 changed files with 2007 additions and 398 deletions
+31 -30
View File
@@ -92,7 +92,7 @@ export class PostgresInterface implements BackendDbInterface {
async getUserByEmail(email: string): Promise<ClientUser> {
const user = await selectClientUser(
this.#db,
sql`u."email" = ${{ text: email }}`
sql`u."email" = ${{ text: email }}`,
);
if (!user) throw new NotFoundAbodeError();
return user;
@@ -111,7 +111,7 @@ export class PostgresInterface implements BackendDbInterface {
SELECT "uid", "email", "name", "flags", "created_at", "updated_at", "password"
FROM "users"
WHERE "email" = ${{ text: email }}
`
`,
);
if (!rawUser) throw new NotFoundAbodeError();
if (rawUser.password.startsWith("#")) throw new ConflictAbodeError();
@@ -125,7 +125,7 @@ export class PostgresInterface implements BackendDbInterface {
sql`
DELETE FROM "users"
WHERE "uid" = ${{ uuid: id }}
`
`,
);
if (!changes) throw new NotFoundAbodeError();
}
@@ -145,10 +145,10 @@ export class PostgresInterface implements BackendDbInterface {
${{ text: user.password }},
${{ jsonb: user.flags }}
)
`
`,
);
return this.#getUserById(uid, tx);
})
}),
);
}
async updateUser(user: UpdateUser): Promise<ClientUser> {
@@ -161,7 +161,8 @@ export class PostgresInterface implements BackendDbInterface {
if ("name" in user && user.name !== undefined)
updates.push(sql`"name" = ${{ text: user.name }}`);
if ("password" in user && user.password !== undefined) {
if (!isValidUserPassword(user.password)) throw new InvalidAbodeError();
if (!isValidUserPassword(user.password))
throw new InvalidAbodeError();
if (user.password.startsWith("#")) {
await tx.run(sql`
DELETE FROM "apikeys"
@@ -185,11 +186,11 @@ export class PostgresInterface implements BackendDbInterface {
"updated_at" = NOW(),
${joinSql(updates, sql`, `)}
WHERE "uid" = ${{ uuid: user.uid }}
`
`,
);
if (!changes) throw new NotFoundAbodeError();
return this.#getUserById(user.uid, tx);
})
}),
);
}
@@ -210,7 +211,7 @@ export class PostgresInterface implements BackendDbInterface {
sql`
DELETE FROM "abodes"
WHERE "aid" = ${{ uuid: id }}
`
`,
);
if (!changes) throw new NotFoundAbodeError();
}
@@ -228,10 +229,10 @@ export class PostgresInterface implements BackendDbInterface {
${{ uuid: ctx.uid }},
${{ uuid: ctx.uid }}
)
`
`,
);
return this.#getAbodeById(aid, tx);
})
}),
);
}
async updateAbode(abode: UpdateAbode, ctx: { uid: string }): Promise<Abode> {
@@ -250,11 +251,11 @@ export class PostgresInterface implements BackendDbInterface {
"updated_by" = ${{ uuid: ctx.uid }},
${joinSql(updates, sql`, `)}
WHERE "aid" = ${{ uuid: abode.aid }}
`
`,
);
if (!changes) throw new NotFoundAbodeError();
return this.#getAbodeById(abode.aid, tx);
})
}),
);
}
@@ -270,11 +271,11 @@ export class PostgresInterface implements BackendDbInterface {
async #getResidentById(
uid: string,
aid: string,
db: WrappedPgClient
db: WrappedPgClient,
): Promise<Resident> {
const resident = await selectResident(
db,
sql`"uid" = ${{ uuid: uid }} AND "aid" = ${{ uuid: aid }}`
sql`"uid" = ${{ uuid: uid }} AND "aid" = ${{ uuid: aid }}`,
);
if (!resident) throw new NotFoundAbodeError();
return resident;
@@ -288,13 +289,13 @@ export class PostgresInterface implements BackendDbInterface {
sql`
DELETE FROM "residents"
WHERE "uid" = ${{ uuid: uid }} AND "aid" = ${{ uuid: aid }}
`
`,
);
if (!changes) throw new NotFoundAbodeError();
}
async createResident(
resident: CreateResident,
ctx: { uid: string }
ctx: { uid: string },
): Promise<Resident> {
this.#checkReadonly();
return this.#db.rethrow(() =>
@@ -309,15 +310,15 @@ export class PostgresInterface implements BackendDbInterface {
${{ uuid: ctx.uid }},
${{ uuid: ctx.uid }}
)
`
`,
);
return this.#getResidentById(resident.uid, resident.aid, tx);
})
}),
);
}
async updateResident(
resident: updateResident,
ctx: { uid: string }
ctx: { uid: string },
): Promise<Resident> {
this.#checkReadonly();
const updates = calcUpdates({
@@ -336,11 +337,11 @@ export class PostgresInterface implements BackendDbInterface {
WHERE
"uid" = ${{ uuid: resident.uid }}
AND "aid" = ${{ uuid: resident.aid }}
`
`,
);
if (!changes) throw new NotFoundAbodeError();
return this.#getResidentById(resident.uid, resident.aid, tx);
})
}),
);
}
@@ -350,7 +351,7 @@ export class PostgresInterface implements BackendDbInterface {
sql`
JOIN "residents" r ON u."uid" = r."uid"
WHERE r."aid" = ${{ uuid: id }}
`
`,
);
}
async listAbodesByUserId(id: string): Promise<Abode[]> {
@@ -359,7 +360,7 @@ export class PostgresInterface implements BackendDbInterface {
sql`
JOIN "residents" r ON a."aid" = r."aid"
WHERE r."uid" = ${{ uuid: id }}
`
`,
);
}
@@ -418,17 +419,17 @@ export class PostgresInterface implements BackendDbInterface {
async #getApikeyByToken(
token: `at_${string}`,
db: WrappedPgClient
db: WrappedPgClient,
): Promise<ClientApikey> {
const apikey = await selectClientApikey(
db,
sql`k."token" = ${{ text: token }}`
sql`k."token" = ${{ text: token }}`,
);
if (!apikey) throw new NotFoundAbodeError();
return apikey;
}
async getUserByApikey(
token: `at_${string}`
token: `at_${string}`,
): Promise<[ClientUser, ClientApikey]> {
const apikey = await this.#getApikeyByToken(token, this.#db);
if (
@@ -445,13 +446,13 @@ export class PostgresInterface implements BackendDbInterface {
async getApikeyById(kid: string): Promise<ClientApikey> {
const apikey = await selectClientApikey(
this.#db,
sql`k."kid" = ${{ uuid: kid }}`
sql`k."kid" = ${{ uuid: kid }}`,
);
if (!apikey) throw new NotFoundAbodeError();
return apikey;
}
async createApikey(
apikey: CreateApikey
apikey: CreateApikey,
): Promise<[ClientApikey, `at_${string}`]> {
this.#checkReadonly();
const token = createApikeyToken();
@@ -459,7 +460,7 @@ export class PostgresInterface implements BackendDbInterface {
let expires = apikey.expires_at;
if (expires === undefined)
expires = new Date(
new Date().getTime() + 1000 * 60 * 60 * 24 * 365
new Date().getTime() + 1000 * 60 * 60 * 24 * 365,
).toISOString();
if (expires && new Date(expires).getTime() < Date.now())
throw new InvalidAbodeError();
+5 -6
View File
@@ -24,7 +24,7 @@ export class PostgresMigrator implements Migrator {
`SELECT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = '_migrations'
) AS "exists"`
) AS "exists"`,
);
if (!existsResult.rows[0]?.exists) return null;
@@ -33,7 +33,7 @@ export class PostgresMigrator implements Migrator {
name: string;
applied_at: Date;
}>(
`SELECT "id", "name", "applied_at" FROM "_migrations" ORDER BY "id" ASC`
`SELECT "id", "name", "applied_at" FROM "_migrations" ORDER BY "id" ASC`,
);
return result.rows.map((x) => ({
...x,
@@ -73,17 +73,16 @@ export class PostgresMigrator implements Migrator {
throw new Error(`Applied migration ${id} (${name}) not known`);
if (migration.name !== name)
throw new Error(
`Applied migration ${id} (${name}) has a different name from expected (${migration.name})`
`Applied migration ${id} (${name}) has a different name from expected (${migration.name})`,
);
}
const start =
migrations.findIndex((x) => x.id === current.at(-1)?.id) + 1;
const start = migrations.findIndex((x) => x.id === current.at(-1)?.id) + 1;
const end = migrations.indexOf(target) + 1;
if (end < start) {
throw new Error(
`Cannot migrate backward, at ${current.at(-1)?.id ?? 0}, going to ${target.id}`
`Cannot migrate backward, at ${current.at(-1)?.id ?? 0}, going to ${target.id}`,
);
}
+3 -5
View File
@@ -31,9 +31,7 @@ async function rethrow<R>(fn: () => Promise<R>): Promise<R> {
// Rollback on a broken connection can itself throw; the original error is
// the one worth surfacing.
export async function rollbackQuietly(
client: pg.PoolClient
): Promise<void> {
export async function rollbackQuietly(client: pg.PoolClient): Promise<void> {
try {
await client.query("ROLLBACK");
} catch {}
@@ -58,7 +56,7 @@ abstract class WrappedPgBase implements WrappedPgClient {
async all<R>(stmt: SqlCode): Promise<R[]> {
const result = await this.#queryable.query(
toPositional(stmt._sql),
stmt._vars
stmt._vars,
);
return result.rows as R[];
}
@@ -72,7 +70,7 @@ abstract class WrappedPgBase implements WrappedPgClient {
async run(stmt: SqlCode): Promise<{ changes: number }> {
const result = await this.#queryable.query(
toPositional(stmt._sql),
stmt._vars
stmt._vars,
);
return { changes: result.rowCount ?? 0 };
}
+14 -16
View File
@@ -26,20 +26,18 @@ const sqlClientUser = sql`
export async function selectClientUser(
db: WrappedPgClient,
where: SqlCode
where: SqlCode,
): Promise<ClientUser | null> {
const raw = await db.get<RawClientUser>(
sql`${sqlClientUser} WHERE ${where}`
);
const raw = await db.get<RawClientUser>(sql`${sqlClientUser} WHERE ${where}`);
if (raw) return pgToClientUser(raw);
return null;
}
export async function selectClientUsers(
db: WrappedPgClient,
rest?: SqlCode
rest?: SqlCode,
): Promise<ClientUser[]> {
const rows = await db.all<RawClientUser>(
rest ? sql`${sqlClientUser} ${rest}` : sqlClientUser
rest ? sql`${sqlClientUser} ${rest}` : sqlClientUser,
);
return rows.map(pgToClientUser);
}
@@ -59,7 +57,7 @@ const sqlAbode = sql`
export async function selectAbode(
db: WrappedPgClient,
where: SqlCode
where: SqlCode,
): Promise<Abode | null> {
const raw = await db.get<RawAbode>(sql`${sqlAbode} WHERE ${where}`);
if (raw) return pgToAbode(raw);
@@ -67,10 +65,10 @@ export async function selectAbode(
}
export async function selectAbodes(
db: WrappedPgClient,
rest?: SqlCode
rest?: SqlCode,
): Promise<Abode[]> {
const rows = await db.all<RawAbode>(
rest ? sql`${sqlAbode} ${rest}` : sqlAbode
rest ? sql`${sqlAbode} ${rest}` : sqlAbode,
);
return rows.map(pgToAbode);
}
@@ -91,7 +89,7 @@ const sqlResident = sql`
export async function selectResident(
db: WrappedPgClient,
where: SqlCode
where: SqlCode,
): Promise<Resident | null> {
const raw = await db.get<RawResident>(sql`${sqlResident} WHERE ${where}`);
if (raw) return pgToResident(raw);
@@ -99,10 +97,10 @@ export async function selectResident(
}
export async function selectResidents(
db: WrappedPgClient,
where?: SqlCode
where?: SqlCode,
): Promise<Resident[]> {
const rows = await db.all<RawResident>(
where ? sql`${sqlResident} WHERE ${where}` : sqlResident
where ? sql`${sqlResident} WHERE ${where}` : sqlResident,
);
return rows.map(pgToResident);
}
@@ -122,20 +120,20 @@ const sqlClientApikey = sql`
export async function selectClientApikey(
db: WrappedPgClient,
where: SqlCode
where: SqlCode,
): Promise<ClientApikey | null> {
const raw = await db.get<RawClientApikey>(
sql`${sqlClientApikey} WHERE ${where}`
sql`${sqlClientApikey} WHERE ${where}`,
);
if (raw) return pgToClientApikey(raw);
return null;
}
export async function selectClientApikeys(
db: WrappedPgClient,
where: SqlCode
where: SqlCode,
): Promise<ClientApikey[]> {
const rows = await db.all<RawClientApikey>(
sql`${sqlClientApikey} WHERE ${where}`
sql`${sqlClientApikey} WHERE ${where}`,
);
return rows.map(pgToClientApikey);
}
+1 -1
View File
@@ -65,7 +65,7 @@ export function calcUpdates<T extends object>(updater: {
for (const [prop, update] of Object.entries(updater)) {
if (prop in obj) {
updates.push(
(update as (value: unknown) => SqlCode)(obj[prop as keyof T]!)
(update as (value: unknown) => SqlCode)(obj[prop as keyof T]!),
);
}
}
+4 -1
View File
@@ -9,7 +9,10 @@ export function isPgUrl(url: string): boolean {
}
}
export function parsePgUrl(url: string): { connectionString: string; readonly: boolean } {
export function parsePgUrl(url: string): {
connectionString: string;
readonly: boolean;
} {
if (!isPgUrl(url)) throw new Error("Not a postgres: URL");
const urlObj = new URL(url);
const readonly = (urlObj.searchParams.get("readonly") ?? "0") !== "0";