Skip to content

Commit 2a06515

Browse files
committed
chore: address pr review comments
1 parent 1885d44 commit 2a06515

4 files changed

Lines changed: 96 additions & 10 deletions

File tree

apps/backend/src/providers/repositories/storage-provider.repository.spec.ts

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { In, Not } from "typeorm";
12
import { beforeEach, describe, expect, it, vi } from "vitest";
23
import type { StorageProvider } from "../../database/entities/storage-provider.entity.js";
34
import type { PDPProviderEx } from "../../wallet-sdk/wallet-sdk.types.js";
@@ -326,12 +327,17 @@ describe("StorageProviderRepository", () => {
326327
});
327328

328329
describe("upsertFromRegistry", () => {
329-
let repo: { create: ReturnType<typeof vi.fn>; upsert: ReturnType<typeof vi.fn> };
330+
let repo: { create: ReturnType<typeof vi.fn>; manager: { transaction: ReturnType<typeof vi.fn> } };
331+
let txRepo: { upsert: ReturnType<typeof vi.fn>; update: ReturnType<typeof vi.fn> };
330332
let service: StorageProviderRepository;
331333
let loggerMock: { warn: ReturnType<typeof vi.fn>; error: ReturnType<typeof vi.fn> };
332334

333335
beforeEach(() => {
334-
repo = { create: vi.fn((data) => data), upsert: vi.fn() };
336+
txRepo = { upsert: vi.fn(), update: vi.fn() };
337+
repo = {
338+
create: vi.fn((data) => data),
339+
manager: { transaction: vi.fn((runInTransaction) => runInTransaction({ getRepository: () => txRepo })) },
340+
};
335341
service = new StorageProviderRepository(repo as any, {} as any);
336342
loggerMock = { warn: vi.fn(), error: vi.fn() };
337343
(service as any).logger = loggerMock;
@@ -357,7 +363,7 @@ describe("StorageProviderRepository", () => {
357363
);
358364
expect(loggerMock.error).not.toHaveBeenCalled();
359365

360-
const [entities, options] = repo.upsert.mock.calls[0];
366+
const [entities, options] = txRepo.upsert.mock.calls[0];
361367
expect(options).toEqual(expect.objectContaining({ conflictPaths: ["address", "network"] }));
362368
expect(entities).toEqual(
363369
expect.arrayContaining([
@@ -374,7 +380,7 @@ describe("StorageProviderRepository", () => {
374380
await service.upsertFromRegistry([active, inactive], "calibration");
375381

376382
expect(loggerMock.error).not.toHaveBeenCalled();
377-
const [entities] = repo.upsert.mock.calls[0];
383+
const [entities] = txRepo.upsert.mock.calls[0];
378384
expect(entities).toEqual(
379385
expect.arrayContaining([
380386
expect.objectContaining({ address: "0xdup2", network: "calibration", providerId: 30n, name: "active" }),
@@ -391,12 +397,47 @@ describe("StorageProviderRepository", () => {
391397
expect(loggerMock.error).toHaveBeenCalledWith(
392398
expect.objectContaining({ event: "duplicate_provider_addresses_unresolved" }),
393399
);
394-
const [entities] = repo.upsert.mock.calls[0];
400+
const [entities] = txRepo.upsert.mock.calls[0];
395401
expect(entities).toEqual(
396402
expect.arrayContaining([
397403
expect.objectContaining({ address: "0xdup3", network: "calibration", providerId: 41n, name: "second" }),
398404
]),
399405
);
400406
});
407+
408+
it("deactivates rows whose address is absent from the registry snapshot", async () => {
409+
const stillPresent = makeProvider({ id: 50n, serviceProvider: "0xstill", isActive: true });
410+
411+
await service.upsertFromRegistry([stillPresent], "calibration");
412+
413+
expect(txRepo.update).toHaveBeenCalledWith(
414+
{ network: "calibration", isActive: true, address: Not(In(["0xstill"])) },
415+
{ isActive: false },
416+
);
417+
});
418+
419+
it("deactivates within the same transaction as the upsert, after it", async () => {
420+
const provider = makeProvider({ id: 51n, serviceProvider: "0xa" });
421+
const callOrder: string[] = [];
422+
txRepo.upsert.mockImplementation(() => {
423+
callOrder.push("upsert");
424+
return Promise.resolve();
425+
});
426+
txRepo.update.mockImplementation(() => {
427+
callOrder.push("update");
428+
return Promise.resolve();
429+
});
430+
431+
await service.upsertFromRegistry([provider], "calibration");
432+
433+
expect(repo.manager.transaction).toHaveBeenCalledTimes(1);
434+
expect(callOrder).toEqual(["upsert", "update"]);
435+
});
436+
437+
it("clears the address filter (deactivating every row for the network) when the registry snapshot is empty", async () => {
438+
await service.upsertFromRegistry([], "calibration");
439+
440+
expect(txRepo.update).toHaveBeenCalledWith({ network: "calibration", isActive: true }, { isActive: false });
441+
});
401442
});
402443
});

apps/backend/src/providers/repositories/storage-provider.repository.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Injectable, Logger } from "@nestjs/common";
22
import { ConfigService } from "@nestjs/config";
33
import { InjectRepository } from "@nestjs/typeorm";
4-
import { Raw, type Repository } from "typeorm";
4+
import { In, Not, Raw, type Repository } from "typeorm";
55
import { toJsonSafe } from "../../common/logging.js";
66
import type { Network } from "../../common/types.js";
77
import type { IConfig } from "../../config/index.js";
@@ -213,9 +213,27 @@ export class StorageProviderRepository {
213213
}),
214214
);
215215

216-
await this.repo.upsert(entities, {
217-
conflictPaths: ["address", "network"],
218-
skipUpdateIfNoValuesChanged: true,
216+
const seenAddresses = Array.from(dedupedProviders.keys());
217+
218+
await this.repo.manager.transaction(async (manager) => {
219+
const txRepo = manager.getRepository(StorageProvider);
220+
221+
await txRepo.upsert(entities, {
222+
conflictPaths: ["address", "network"],
223+
skipUpdateIfNoValuesChanged: true,
224+
});
225+
226+
// Registry responses are a full snapshot each refresh, so any active row whose
227+
// address wasn't in this snapshot is stale (provider deregistered/removed) and
228+
// must be flipped inactive here — otherwise it stays isActive=true forever.
229+
await txRepo.update(
230+
{
231+
network,
232+
isActive: true,
233+
...(seenAddresses.length > 0 ? { address: Not(In(seenAddresses)) } : {}),
234+
},
235+
{ isActive: false },
236+
);
219237
});
220238
}
221239

apps/backend/src/wallet-sdk/wallet-sdk.service.spec.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,16 @@ const makeProvider = (overrides: Partial<PDPProviderEx>): PDPProviderEx =>
6666

6767
describe("WalletSdkService", () => {
6868
let service: WalletSdkService;
69-
let storageProviderRepositoryMock: { countByNetwork: ReturnType<typeof vi.fn> };
69+
let storageProviderRepositoryMock: {
70+
countByNetwork: ReturnType<typeof vi.fn>;
71+
upsertFromRegistry: ReturnType<typeof vi.fn>;
72+
};
7073
let loggerMock: LoggerLike;
7174

7275
beforeEach(() => {
7376
storageProviderRepositoryMock = {
7477
countByNetwork: vi.fn().mockResolvedValue(0),
78+
upsertFromRegistry: vi.fn().mockResolvedValue(undefined),
7579
};
7680

7781
const configService = {
@@ -135,6 +139,28 @@ describe("WalletSdkService", () => {
135139
expect(loadProviders).not.toHaveBeenCalled();
136140
});
137141

142+
it("returns false when the DB sync fails, so callers don't record a false success", async () => {
143+
storageProviderRepositoryMock.upsertFromRegistry.mockRejectedValue(new Error("db down"));
144+
const provider = makeProvider({ id: 1n });
145+
const mockState = {
146+
config: baseNetworkConfig,
147+
warmStorageService: { getApprovedProviderIds: vi.fn().mockResolvedValue([]) },
148+
spRegistry: {
149+
getProviderCount: vi.fn().mockResolvedValue(1n),
150+
getAllActiveProviders: vi.fn().mockResolvedValue([provider]),
151+
},
152+
providersLoadPromise: null,
153+
};
154+
(service as any).networkStates.set("calibration", mockState);
155+
156+
const result = await service.loadProviders("calibration");
157+
158+
expect(result).toBe(false);
159+
expect(storageProviderRepositoryMock.upsertFromRegistry).toHaveBeenCalled();
160+
expect(loggerMock.error).toHaveBeenCalledWith(expect.objectContaining({ event: "providers_sync_to_db_failed" }));
161+
expect(loggerMock.error).toHaveBeenCalledWith(expect.objectContaining({ event: "providers_load_failed" }));
162+
});
163+
138164
describe("ensureWalletAllowances", () => {
139165
it("performs read-only check in session key mode", async () => {
140166
const mockState = {

apps/backend/src/wallet-sdk/wallet-sdk.service.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ export class WalletSdkService implements OnModuleInit {
193193
message: "Failed to sync providers to DB",
194194
error: toStructuredError(err),
195195
});
196+
throw err;
196197
}
197198

198199
this.logger.log({

0 commit comments

Comments
 (0)