UNPKG

@furystack/repository

Version:

Repository implementation for FuryStack

134 lines 5.83 kB
import { defineStore, InMemoryStore } from '@furystack/core'; import { createInjector } from '@furystack/inject'; import { usingAsync } from '@furystack/utils'; import { describe, expect, it, vi } from 'vitest'; import { DataSet } from './data-set.js'; import { defineDataSet } from './define-data-set.js'; class Test { } // Store tokens are declared at module scope so TypeScript keeps the literal // primary-key narrowing. Wrapping `defineStore` in an arrow function would // widen the inferred `TPrimaryKey` back to `keyof Test` in the return type. const BasicStore = defineStore({ name: 'test/BasicStore', model: Test, primaryKey: 'id', factory: () => new InMemoryStore({ model: Test, primaryKey: 'id' }), }); const SingletonStore = defineStore({ name: 'test/SingletonStore', model: Test, primaryKey: 'id', factory: () => new InMemoryStore({ model: Test, primaryKey: 'id' }), }); const MetaStore = defineStore({ name: 'test/MetaStore', model: Test, primaryKey: 'id', factory: () => new InMemoryStore({ model: Test, primaryKey: 'id' }), }); const HooksStore = defineStore({ name: 'test/HooksStore', model: Test, primaryKey: 'id', factory: () => new InMemoryStore({ model: Test, primaryKey: 'id' }), }); const EventsStore = defineStore({ name: 'test/EventsStore', model: Test, primaryKey: 'id', factory: () => new InMemoryStore({ model: Test, primaryKey: 'id' }), }); const DisposeStore = defineStore({ name: 'test/DisposeStore', model: Test, primaryKey: 'id', factory: () => new InMemoryStore({ model: Test, primaryKey: 'id' }), }); const RebindStore = defineStore({ name: 'test/RebindStore', model: Test, primaryKey: 'id', factory: () => new InMemoryStore({ model: Test, primaryKey: 'id' }), }); const modifyOnAddSpy = vi.fn(); const BasicDataSet = defineDataSet({ name: 'test/BasicDataSet', store: BasicStore }); const SingletonDataSet = defineDataSet({ name: 'test/SingletonDataSet', store: SingletonStore }); const MetaDataSet = defineDataSet({ name: 'test/MetaDataSet', store: MetaStore }); // The `settings` callback participates in bidirectional inference against the // surrounding generics. Even with `NoInfer` on the settings position, inline // callbacks currently widen `TPrimaryKey` to `keyof T` because TypeScript has // to contextually type the callback before it can commit to the store-driven // inference. The runtime value of `token.primaryKey` is still correct; only // the entity parameter is typed as the wider `WithOptionalId<Test, keyof Test>`. // The non-null assertion on `entity.value` reflects that widening. const HooksDataSet = defineDataSet({ name: 'test/HooksDataSet', store: HooksStore, settings: { modifyOnAdd: async ({ entity }) => { modifyOnAddSpy(); return { ...entity, value: entity.value.toUpperCase() }; }, }, }); const EventsDataSet = defineDataSet({ name: 'test/EventsDataSet', store: EventsStore }); const DisposeDataSet = defineDataSet({ name: 'test/DisposeDataSet', store: DisposeStore }); const RebindDataSet = defineDataSet({ name: 'test/RebindDataSet', store: RebindStore }); describe('defineDataSet', () => { it('resolves to a DataSet backed by the supplied store', async () => { await usingAsync(createInjector(), async (i) => { const ds = i.get(BasicDataSet); expect(ds).toBeInstanceOf(DataSet); expect(ds.settings.physicalStore).toBe(i.get(BasicStore)); }); }); it('caches the DataSet as a singleton across resolutions', async () => { await usingAsync(createInjector(), async (i) => { expect(i.get(SingletonDataSet)).toBe(i.get(SingletonDataSet)); }); }); it('mirrors the model and primary key from the backing store token', () => { expect(MetaDataSet.model).toBe(Test); expect(MetaDataSet.primaryKey).toBe('id'); }); it('applies the configured authorizers and hooks', async () => { modifyOnAddSpy.mockClear(); await usingAsync(createInjector(), async (i) => { const ds = i.get(HooksDataSet); await ds.add(i, { id: 1, value: 'asd' }); const stored = await ds.get(i, 1); expect(modifyOnAddSpy).toHaveBeenCalledTimes(1); expect(stored?.value).toBe('ASD'); }); }); it('emits change events through the DataSet EventHub', async () => { await usingAsync(createInjector(), async (i) => { const added = vi.fn(); const ds = i.get(EventsDataSet); ds.addListener('onEntityAdded', added); await ds.add(i, { id: 1, value: 'x' }); expect(added).toHaveBeenCalledWith(expect.objectContaining({ entity: { id: 1, value: 'x' } })); }); }); it('clears DataSet listeners when the injector is disposed', async () => { const i = createInjector(); const ds = i.get(DisposeDataSet); const listener = vi.fn(); ds.addListener('onEntityAdded', listener); await i[Symbol.asyncDispose](); // After disposal the DataSet's event subscriptions are cleared; // emitting directly would be a no-op. ds.emit('onEntityAdded', { injector: i, entity: { id: 1, value: 'x' } }); expect(listener).not.toHaveBeenCalled(); }); it('allows rebinding the backing store for tests', async () => { await usingAsync(createInjector(), async (i) => { const replacement = new InMemoryStore({ model: Test, primaryKey: 'id' }); i.bind(RebindStore, () => replacement); const ds = i.get(RebindDataSet); expect(ds.settings.physicalStore).toBe(replacement); }); }); }); //# sourceMappingURL=define-data-set.spec.js.map