Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions apps/api/src/core/repositories/base.repository.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { DrizzleQueryError } from "drizzle-orm";
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import { describe, expect, it, vi } from "vitest";
import { mock } from "vitest-mock-extended";

import type { ApiPgDatabase } from "@src/core/providers";
import { BaseRepository } from "@src/core/repositories/base.repository";
import type { TxService } from "@src/core/services";
import { DataKeys } from "@src/secret/model-schemas";
import { DataKeyRepository } from "@src/secret/repositories/data-key/data-key.repository";
import { Users } from "@src/user/model-schemas";
import { UserRepository } from "@src/user/repositories";

const DATA_KEY_ID = "6e4c9a2c-0f1d-4a3b-9c5e-7d8f0a1b2c3d";
const OTHER_DATA_KEY_ID = "f1a2b3c4-d5e6-4789-9abc-def012345678";
const USER_ID = "1b2c3d4e-5f60-4718-9a2b-3c4d5e6f7081";
const UNREACHABLE_DRIVER = "the driver is stubbed out in unit tests";

describe(BaseRepository.name, () => {
describe("updateBy", () => {
it("stamps updated_at on a table that declares the column", async () => {
const { dataKeyRepository, executedQueries } = setup();

await executeAgainstStubbedDriver(() => dataKeyRepository.updateBy({ userId: USER_ID }, { wrappedByKid: "kms-v2" }));

expect(executedQueries).toEqual([
{
query: 'update "data_keys" set "wrapped_by_kid" = $1, "updated_at" = now() where "data_keys"."user_id" = $2',
params: ["kms-v2", USER_ID]
}
]);
});

it("keeps an updatedAt supplied by the caller", async () => {
const { dataKeyRepository, executedQueries } = setup();
const updatedAt = new Date("2026-03-04T05:06:07.000Z");

await executeAgainstStubbedDriver(() => dataKeyRepository.updateBy({ id: DATA_KEY_ID }, { wrappedByKid: "kms-v2", updatedAt }));

expect(executedQueries).toEqual([
{
query: 'update "data_keys" set "wrapped_by_kid" = $1, "updated_at" = $2 where "data_keys"."id" = $3',
params: ["kms-v2", updatedAt.toISOString(), DATA_KEY_ID]
}
]);
});

it("touches updated_at when the payload holds nothing else", async () => {
const { dataKeyRepository, executedQueries } = setup();

await executeAgainstStubbedDriver(() => dataKeyRepository.updateBy({ id: DATA_KEY_ID }, {}));

expect(executedQueries).toEqual([
{
query: 'update "data_keys" set "updated_at" = now() where "data_keys"."id" = $1',
params: [DATA_KEY_ID]
}
]);
});

it("leaves out updated_at on a table that has no such column", async () => {
const { userRepository, executedQueries } = setup();

await executeAgainstStubbedDriver(() => userRepository.updateBy({ id: USER_ID }, { bio: "hello" }));

expect(executedQueries).toEqual([
{
query: 'update "userSetting" set "bio" = $1 where "userSetting"."id" = $2',
params: ["hello", USER_ID]
}
]);
});
});

describe("updateById", () => {
it("stamps updated_at", async () => {
const { dataKeyRepository, executedQueries } = setup();

await executeAgainstStubbedDriver(() => dataKeyRepository.updateById(DATA_KEY_ID, { wrappedByKid: "kms-v2" }));

expect(executedQueries).toEqual([
{
query: 'update "data_keys" set "wrapped_by_kid" = $1, "updated_at" = now() where "data_keys"."id" = $2',
params: ["kms-v2", DATA_KEY_ID]
}
]);
});
});

describe("updateManyById", () => {
it("stamps updated_at on every matched row", async () => {
const { dataKeyRepository, executedQueries } = setup();

await executeAgainstStubbedDriver(() => dataKeyRepository.updateManyById([DATA_KEY_ID, OTHER_DATA_KEY_ID], { wrappedByKid: "kms-v2" }));

expect(executedQueries).toEqual([
{
query: 'update "data_keys" set "wrapped_by_kid" = $1, "updated_at" = now() where "data_keys"."id" in ($2, $3)',
params: ["kms-v2", DATA_KEY_ID, OTHER_DATA_KEY_ID]
}
]);
});
});

async function executeAgainstStubbedDriver(run: () => Promise<unknown>) {
await expect(run()).rejects.toThrow(DrizzleQueryError);
}

function setup() {
const executedQueries: Array<{ query: string; params: unknown[] }> = [];
const client = postgres("postgres://localhost:5432/unused");
vi.spyOn(client, "unsafe").mockImplementation((query, params) => {
executedQueries.push({ query, params: params ?? [] });
throw new Error(UNREACHABLE_DRIVER);
});
const driverlessDb = drizzle(client);
const pg = mock<ApiPgDatabase>({ update: driverlessDb.update.bind(driverlessDb) });
const txManager = mock<TxService>();

return {
executedQueries,
dataKeyRepository: new DataKeyRepository(pg, DataKeys, txManager),
userRepository: new UserRepository(pg, Users, txManager)
};
}
});
15 changes: 7 additions & 8 deletions apps/api/src/core/repositories/base.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,7 @@ export abstract class BaseRepository<
async updateManyById(ids: Output["id"][], payload: Partial<Input>): Promise<void> {
await this.cursor
.update(this.table)
.set({
...this.toInput(payload),
updated_at: sql`now()`
})
.set(this.toUpdateSet(payload))
.where(inArray(this.table.id, ids));
}

Expand All @@ -162,10 +159,7 @@ export abstract class BaseRepository<
async updateBy(query: Partial<Output>, payload: Partial<Input>, options?: MutationOptions): Promise<void | Output> {
const cursor = this.cursor
.update(this.table)
.set({
...this.toInput(payload),
updated_at: sql`now()`
})
.set(this.toUpdateSet(payload))
.where(this.queryToWhere(query));

if (options?.returning) {
Expand Down Expand Up @@ -219,6 +213,11 @@ export abstract class BaseRepository<
return this.whereAccessibleBy(where);
}

/** Drizzle builds the SET clause from schema property names, so a raw column key such as updated_at is dropped without an error. */
private toUpdateSet(payload: Partial<Input>) {
return { updatedAt: sql`now()`, ...this.toInput(payload) };
}

protected toInput(payload: Partial<Input>): Partial<T["$inferInsert"]> {
return payload as Partial<T["$inferSelect"]>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -642,12 +642,4 @@ export class DeploymentSettingRepository extends BaseRepository<Table, Deploymen
set: { closed: true, updatedAt: sql`now()` }
});
}

protected toInput(payload: Partial<DeploymentSettingsInput>): Partial<DeploymentSettingsInput> {
if (!payload.updatedAt) {
payload.updatedAt = new Date();
}

return payload;
}
}