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
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:
@@ -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();
|
||||
|
||||
@@ -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}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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]!),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user