Skip to content

Commit 3efcf1f

Browse files
authored
Merge pull request #32 from script-development/feat/adapter-store-retrieve-by-id
2 parents 7f10009 + 6d81392 commit 3efcf1f

4 files changed

Lines changed: 221 additions & 1 deletion

File tree

packages/adapter-store/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@script-development/fs-adapter-store",
3-
"version": "0.1.1",
3+
"version": "0.1.2",
44
"description": "Reactive adapter-store pattern with domain state management and CRUD resource adapters",
55
"homepage": "https://packages.script.nl/packages/adapter-store",
66
"license": "MIT",

packages/adapter-store/src/adapter-store.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ export const createAdapterStoreModule = <
7878
return item;
7979
},
8080
generateNew: () => adapter(storeModule),
81+
retrieveById: async (id: number) => {
82+
const { data } = await httpService.getRequest<T>(`${domainName}/${id}`);
83+
setById(data);
84+
},
8185
retrieveAll: async () => {
8286
const { data } = await httpService.getRequest<T[]>(domainName);
8387
state.value = data.reduce<{ [id: number]: Readonly<T> }>((acc, item) => {

packages/adapter-store/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,5 +78,6 @@ export type StoreModuleForAdapter<
7878
getById: (id: number) => ComputedRef<E | undefined>;
7979
getOrFailById: (id: number) => Promise<E>;
8080
generateNew: () => N;
81+
retrieveById: (id: number) => Promise<void>;
8182
retrieveAll: () => Promise<void>;
8283
};

packages/adapter-store/tests/adapter-store.spec.ts

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,168 @@ describe("createAdapterStoreModule", () => {
559559
});
560560
});
561561

562+
describe("retrieveById", () => {
563+
it("should call httpService.getRequest with domainName and id", async () => {
564+
// Arrange
565+
const httpService: Pick<HttpService, "getRequest"> = { getRequest: vi.fn() };
566+
const storageService: TestStorageService = { put: vi.fn(), get: vi.fn().mockReturnValue({}) };
567+
const loadingService: TestLoadingService = {
568+
ensureLoadingFinished: vi.fn().mockResolvedValue(undefined),
569+
};
570+
const config: AdapterStoreConfig<TestItem, TestAdapted, TestNewAdapted> = {
571+
domainName: "test-items",
572+
adapter: createTestAdapter,
573+
httpService,
574+
storageService,
575+
loadingService,
576+
};
577+
vi.mocked(httpService.getRequest).mockResolvedValue({
578+
data: {
579+
id: 7,
580+
name: "Item 7",
581+
createdAt: "2024-01-01T00:00:00Z",
582+
updatedAt: "2024-01-01T00:00:00Z",
583+
} satisfies TestItem,
584+
} as AxiosResponse<TestItem>);
585+
const store = createAdapterStoreModule(config);
586+
587+
// Act
588+
await store.retrieveById(7);
589+
590+
// Assert
591+
expect(httpService.getRequest).toHaveBeenCalledWith("test-items/7");
592+
});
593+
594+
it("should insert the returned item into the store", async () => {
595+
// Arrange
596+
const httpService: Pick<HttpService, "getRequest"> = { getRequest: vi.fn() };
597+
const storageService: TestStorageService = { put: vi.fn(), get: vi.fn().mockReturnValue({}) };
598+
const loadingService: TestLoadingService = {
599+
ensureLoadingFinished: vi.fn().mockResolvedValue(undefined),
600+
};
601+
const config: AdapterStoreConfig<TestItem, TestAdapted, TestNewAdapted> = {
602+
domainName: "test-items",
603+
adapter: createTestAdapter,
604+
httpService,
605+
storageService,
606+
loadingService,
607+
};
608+
vi.mocked(httpService.getRequest).mockResolvedValue({
609+
data: {
610+
id: 7,
611+
name: "Item 7",
612+
createdAt: "2024-01-01T00:00:00Z",
613+
updatedAt: "2024-01-01T00:00:00Z",
614+
} satisfies TestItem,
615+
} as AxiosResponse<TestItem>);
616+
const store = createAdapterStoreModule(config);
617+
618+
// Act
619+
await store.retrieveById(7);
620+
621+
// Assert
622+
expect(store.getById(7).value?.testMethod()).toBe("adapted-7");
623+
});
624+
625+
it("should refresh an existing item's adapted view after re-retrieval", async () => {
626+
// Arrange
627+
const httpService: Pick<HttpService, "getRequest"> = { getRequest: vi.fn() };
628+
const storageService: TestStorageService = { put: vi.fn(), get: vi.fn().mockReturnValue({}) };
629+
const loadingService: TestLoadingService = {
630+
ensureLoadingFinished: vi.fn().mockResolvedValue(undefined),
631+
};
632+
const config: AdapterStoreConfig<TestItem, TestAdapted, TestNewAdapted> = {
633+
domainName: "test-items",
634+
adapter: createTestAdapter,
635+
httpService,
636+
storageService,
637+
loadingService,
638+
};
639+
vi.mocked(httpService.getRequest).mockResolvedValueOnce({
640+
data: {
641+
id: 1,
642+
name: "Original",
643+
createdAt: "2024-01-01T00:00:00Z",
644+
updatedAt: "2024-01-01T00:00:00Z",
645+
} satisfies TestItem,
646+
} as AxiosResponse<TestItem>);
647+
const store = createAdapterStoreModule(config);
648+
await store.retrieveById(1);
649+
expect(store.getById(1).value?.name).toBe("Original");
650+
vi.mocked(httpService.getRequest).mockResolvedValueOnce({
651+
data: {
652+
id: 1,
653+
name: "Updated",
654+
createdAt: "2024-01-01T00:00:00Z",
655+
updatedAt: "2024-01-02T00:00:00Z",
656+
} satisfies TestItem,
657+
} as AxiosResponse<TestItem>);
658+
659+
// Act
660+
await store.retrieveById(1);
661+
662+
// Assert
663+
expect(store.getById(1).value?.name).toBe("Updated");
664+
});
665+
666+
it("should persist to storage service", async () => {
667+
// Arrange
668+
const httpService: Pick<HttpService, "getRequest"> = { getRequest: vi.fn() };
669+
const storageService: TestStorageService = { put: vi.fn(), get: vi.fn().mockReturnValue({}) };
670+
const loadingService: TestLoadingService = {
671+
ensureLoadingFinished: vi.fn().mockResolvedValue(undefined),
672+
};
673+
const config: AdapterStoreConfig<TestItem, TestAdapted, TestNewAdapted> = {
674+
domainName: "test-items",
675+
adapter: createTestAdapter,
676+
httpService,
677+
storageService,
678+
loadingService,
679+
};
680+
vi.mocked(httpService.getRequest).mockResolvedValue({
681+
data: {
682+
id: 3,
683+
name: "Item 3",
684+
createdAt: "2024-01-01T00:00:00Z",
685+
updatedAt: "2024-01-01T00:00:00Z",
686+
} satisfies TestItem,
687+
} as AxiosResponse<TestItem>);
688+
const store = createAdapterStoreModule(config);
689+
690+
// Act
691+
await store.retrieveById(3);
692+
693+
// Assert
694+
expect(storageService.put).toHaveBeenCalledWith(
695+
"test-items",
696+
expect.objectContaining({ 3: expect.any(Object) as unknown }),
697+
);
698+
});
699+
700+
it("should propagate http errors and leave state untouched", async () => {
701+
// Arrange
702+
const httpService: Pick<HttpService, "getRequest"> = { getRequest: vi.fn() };
703+
const storageService: TestStorageService = { put: vi.fn(), get: vi.fn().mockReturnValue({}) };
704+
const loadingService: TestLoadingService = {
705+
ensureLoadingFinished: vi.fn().mockResolvedValue(undefined),
706+
};
707+
const config: AdapterStoreConfig<TestItem, TestAdapted, TestNewAdapted> = {
708+
domainName: "test-items",
709+
adapter: createTestAdapter,
710+
httpService,
711+
storageService,
712+
loadingService,
713+
};
714+
vi.mocked(httpService.getRequest).mockRejectedValue(new Error("network down"));
715+
const store = createAdapterStoreModule(config);
716+
717+
// Act & Assert
718+
await expect(store.retrieveById(1)).rejects.toThrow("network down");
719+
expect(store.getById(1).value).toBeUndefined();
720+
expect(storageService.put).not.toHaveBeenCalled();
721+
});
722+
});
723+
562724
describe("retrieveAll", () => {
563725
it("should call httpService.getRequest with domainName", async () => {
564726
// Arrange
@@ -1101,6 +1263,59 @@ describe("createAdapterStoreModule", () => {
11011263
expect(refBefore).not.toBe(refAfter);
11021264
});
11031265

1266+
it("should reuse cached adapted entries for untouched ids when state changes for a different id", async () => {
1267+
// Arrange
1268+
const httpService: Pick<HttpService, "getRequest"> = { getRequest: vi.fn() };
1269+
const storageService: TestStorageService = { put: vi.fn(), get: vi.fn().mockReturnValue({}) };
1270+
const loadingService: TestLoadingService = {
1271+
ensureLoadingFinished: vi.fn().mockResolvedValue(undefined),
1272+
};
1273+
const { adapter, getCapturedStoreModule } = createCapturingAdapter();
1274+
const config: AdapterStoreConfig<TestItem, TestAdapted, TestNewAdapted> = {
1275+
domainName: "test-items",
1276+
adapter,
1277+
httpService,
1278+
storageService,
1279+
loadingService,
1280+
};
1281+
const items: TestItem[] = [
1282+
{
1283+
id: 1,
1284+
name: "Item 1",
1285+
createdAt: "2024-01-01T00:00:00Z",
1286+
updatedAt: "2024-01-01T00:00:00Z",
1287+
},
1288+
{
1289+
id: 2,
1290+
name: "Item 2",
1291+
createdAt: "2024-01-01T00:00:00Z",
1292+
updatedAt: "2024-01-01T00:00:00Z",
1293+
},
1294+
];
1295+
vi.mocked(httpService.getRequest).mockResolvedValue({ data: items } as AxiosResponse<
1296+
TestItem[]
1297+
>);
1298+
const store = createAdapterStoreModule(config);
1299+
await store.retrieveAll();
1300+
const itemOneBefore = store.getById(1).value;
1301+
const itemTwoBefore = store.getById(2).value;
1302+
expect(itemOneBefore).toBeDefined();
1303+
expect(itemTwoBefore).toBeDefined();
1304+
1305+
// Act — setById for id 2 only. Clears adaptedCache for 2; id 1 remains cached.
1306+
const storeModule = getCapturedStoreModule() as unknown as AdapterStoreModule<TestItem>;
1307+
storeModule.setById({
1308+
id: 2,
1309+
name: "Item 2 Updated",
1310+
createdAt: "2024-01-01T00:00:00Z",
1311+
updatedAt: "2024-01-02T00:00:00Z",
1312+
});
1313+
1314+
// Assert — id 1 returns the same cached adapted reference; id 2 is freshly adapted.
1315+
expect(store.getById(1).value).toBe(itemOneBefore);
1316+
expect(store.getById(2).value).not.toBe(itemTwoBefore);
1317+
});
1318+
11041319
it("should return cached adapted object via getById when state has not changed", async () => {
11051320
// Arrange
11061321
const httpService: Pick<HttpService, "getRequest"> = { getRequest: vi.fn() };

0 commit comments

Comments
 (0)