@furystack/repository
Version:
Repository implementation for FuryStack
348 lines • 19.1 kB
JavaScript
import { AuthorizationError, defineStore, InMemoryStore } from '@furystack/core';
import { createInjector } from '@furystack/inject';
import { usingAsync } from '@furystack/utils';
import { describe, expect, it, vi } from 'vitest';
import { defineDataSet } from './define-data-set.js';
import { getDataSetFor } from './helpers.js';
class TestClass {
}
// Token declared at module scope so TypeScript preserves the literal primary
// key. The explicit `StoreToken<TestClass, 'id'>` annotation keeps
// `TPrimaryKey` narrow when the token is later passed to `defineDataSet`;
// inferred return types from `defineStore` tend to widen back to
// `keyof TestClass` in downstream inference contexts.
const TestStore = defineStore({
name: 'test/DataSetSpecStore',
model: TestClass,
primaryKey: 'id',
factory: () => new InMemoryStore({ model: TestClass, primaryKey: 'id' }),
});
/**
* Builds a fresh injector with a disposable {@link InMemoryStore} bound as the
* backing store for a {@link TestClass} dataset. Each call mints a new
* dataset token so the injector cache starts from scratch for every test.
*/
const withDataSet = async (settings, fn) => {
const TestDataSet = defineDataSet({
name: `test/DataSet-${Math.random()}`,
store: TestStore,
settings,
});
await usingAsync(createInjector(), async (i) => {
const dataSet = getDataSetFor(i, TestDataSet);
await fn({ i, dataSet });
});
};
describe('DataSet', () => {
describe('Authorizers', () => {
describe('Add', () => {
it('adds an entity when no settings are provided', async () => {
await withDataSet(undefined, async ({ i, dataSet }) => {
await dataSet.add(i, { id: 1, value: 'asd' });
const result = await dataSet.get(i, 1);
expect(result?.value).toBe('asd');
});
});
it('runs authorizeAdd and persists the entity on pass', async () => {
const authorizeAdd = vi.fn(async () => ({ isAllowed: true }));
await withDataSet({ authorizeAdd }, async ({ i, dataSet }) => {
await dataSet.add(i, { id: 1, value: 'asd' });
expect(authorizeAdd).toHaveBeenCalled();
expect((await dataSet.get(i, 1))?.value).toBe('asd');
});
});
it('throws and does not persist when authorizeAdd fails', async () => {
const authorizeAdd = vi.fn(async () => ({ isAllowed: false, message: '...' }));
await withDataSet({ authorizeAdd }, async ({ i, dataSet }) => {
await expect(dataSet.add(i, { id: 1, value: 'asd' })).rejects.toBeInstanceOf(AuthorizationError);
expect(authorizeAdd).toHaveBeenCalled();
expect(await dataSet.get(i, 1)).toBeUndefined();
});
});
it('applies modifyOnAdd before persisting', async () => {
const modifyOnAdd = vi.fn(async ({ entity }) => ({
...entity,
value: entity.value.toUpperCase(),
}));
await withDataSet({ modifyOnAdd }, async ({ i, dataSet }) => {
await dataSet.add(i, { id: 1, value: 'asd' });
const result = await dataSet.get(i, 1);
expect(modifyOnAdd).toHaveBeenCalled();
expect(result?.value).toBe('ASD');
});
});
it('emits onEntityAdded after a successful add', async () => {
await withDataSet(undefined, async ({ i, dataSet }) => {
const listener = vi.fn();
dataSet.addListener('onEntityAdded', listener);
await dataSet.add(i, { id: 1, value: 'asd' });
expect(listener).toHaveBeenCalledWith(expect.objectContaining({ entity: { id: 1, value: 'asd' } }));
});
});
});
describe('Update', () => {
it('updates an entity when no settings are provided', async () => {
await withDataSet(undefined, async ({ i, dataSet }) => {
await dataSet.add(i, { id: 1, value: 'asd' });
await dataSet.update(i, 1, { id: 1, value: 'asd2' });
expect((await dataSet.get(i, 1))?.value).toBe('asd2');
});
});
it('runs authorizeUpdate and persists on pass', async () => {
const authorizeUpdate = vi.fn(async () => ({ isAllowed: true }));
await withDataSet({ authorizeUpdate }, async ({ i, dataSet }) => {
await dataSet.add(i, { id: 1, value: 'asd' });
await dataSet.update(i, 1, { id: 1, value: 'asd2' });
expect(authorizeUpdate).toHaveBeenCalled();
expect((await dataSet.get(i, 1))?.value).toBe('asd2');
});
});
it('throws when authorizeUpdate fails and leaves the entity untouched', async () => {
const authorizeUpdate = vi.fn(async () => ({ isAllowed: false, message: '...' }));
await withDataSet({ authorizeUpdate }, async ({ i, dataSet }) => {
await dataSet.add(i, { id: 1, value: 'asd' });
await expect(dataSet.update(i, 1, { id: 1, value: 'asd2' })).rejects.toBeInstanceOf(AuthorizationError);
expect((await dataSet.get(i, 1))?.value).toBe('asd');
});
});
it('runs authorizeUpdateEntity against the loaded entity and persists on pass', async () => {
const authorizeUpdateEntity = vi.fn(async () => ({ isAllowed: true }));
await withDataSet({ authorizeUpdateEntity }, async ({ i, dataSet }) => {
await dataSet.add(i, { id: 1, value: 'asd' });
await dataSet.update(i, 1, { id: 1, value: 'asd2' });
expect(authorizeUpdateEntity).toHaveBeenCalled();
expect((await dataSet.get(i, 1))?.value).toBe('asd2');
});
});
it('throws and leaves the entity untouched when authorizeUpdateEntity fails', async () => {
const authorizeUpdateEntity = vi.fn(async () => ({ isAllowed: false, message: '...' }));
await withDataSet({ authorizeUpdateEntity }, async ({ i, dataSet }) => {
await dataSet.add(i, { id: 1, value: 'asd' });
await expect(dataSet.update(i, 1, { id: 1, value: 'asd2' })).rejects.toBeInstanceOf(AuthorizationError);
expect((await dataSet.get(i, 1))?.value).toBe('asd');
});
});
it('applies modifyOnUpdate before persisting', async () => {
const modifyOnUpdate = vi.fn(async ({ entity }) => ({
...entity,
value: entity.value?.toUpperCase() ?? '',
}));
await withDataSet({ modifyOnUpdate }, async ({ i, dataSet }) => {
await dataSet.add(i, { id: 1, value: 'asd' });
await dataSet.update(i, 1, { id: 1, value: 'asd2' });
expect(modifyOnUpdate).toHaveBeenCalled();
expect((await dataSet.get(i, 1))?.value).toBe('ASD2');
});
});
it('emits onEntityUpdated with the applied change', async () => {
await withDataSet(undefined, async ({ i, dataSet }) => {
const listener = vi.fn();
dataSet.addListener('onEntityUpdated', listener);
await dataSet.add(i, { id: 1, value: 'asd' });
await dataSet.update(i, 1, { id: 1, value: 'asd2' });
expect(listener).toHaveBeenCalledWith(expect.objectContaining({ change: { id: 1, value: 'asd2' }, id: 1 }));
});
});
});
describe('Count', () => {
it('returns the count when no settings are provided', async () => {
await withDataSet(undefined, async ({ i, dataSet }) => {
await dataSet.add(i, { id: 1, value: 'asd' });
expect(await dataSet.count(i)).toBe(1);
});
});
it('returns the count when authorizeGet passes', async () => {
const authorizeGet = vi.fn(async () => ({ isAllowed: true, message: '' }));
await withDataSet({ authorizeGet }, async ({ i, dataSet }) => {
await dataSet.add(i, { id: 1, value: 'asd' });
expect(await dataSet.count(i)).toBe(1);
});
});
it('throws when authorizeGet fails', async () => {
const authorizeGet = vi.fn(async () => ({ isAllowed: false, message: ':(' }));
await withDataSet({ authorizeGet }, async ({ i, dataSet }) => {
await dataSet.add(i, { id: 1, value: 'asd' });
await expect(dataSet.count(i)).rejects.toBeInstanceOf(AuthorizationError);
});
});
});
});
describe('filter', () => {
it('returns the unfiltered result when no settings are provided', async () => {
await withDataSet(undefined, async ({ i, dataSet }) => {
await dataSet.add(i, { id: 1, value: 'asd' });
expect((await dataSet.find(i, {})).length).toBe(1);
});
});
it('returns the unfiltered result when authorizeGet passes', async () => {
const authorizeGet = vi.fn(async () => ({ isAllowed: true, message: '' }));
await withDataSet({ authorizeGet }, async ({ i, dataSet }) => {
await dataSet.add(i, { id: 1, value: 'asd' });
expect((await dataSet.find(i, {})).length).toBe(1);
});
});
it('throws when authorizeGet fails', async () => {
const authorizeGet = vi.fn(async () => ({ isAllowed: false, message: ':(' }));
await withDataSet({ authorizeGet }, async ({ i, dataSet }) => {
await dataSet.add(i, { id: 1, value: 'asd' });
await expect(dataSet.find(i, {})).rejects.toBeInstanceOf(AuthorizationError);
});
});
});
describe('get', () => {
it('returns the entity when no settings are provided', async () => {
await withDataSet(undefined, async ({ i, dataSet }) => {
await dataSet.add(i, { id: 1, value: 'asd' });
expect((await dataSet.get(i, 1))?.id).toBe(1);
});
});
it('returns the entity when authorizeGet passes', async () => {
const authorizeGet = vi.fn(async () => ({ isAllowed: true, message: '' }));
await withDataSet({ authorizeGet }, async ({ i, dataSet }) => {
await dataSet.add(i, { id: 1, value: 'asd' });
expect((await dataSet.get(i, 1))?.id).toBe(1);
});
});
it('throws when authorizeGet fails', async () => {
const authorizeGet = vi.fn(async () => ({ isAllowed: false, message: ':(' }));
await withDataSet({ authorizeGet }, async ({ i, dataSet }) => {
await dataSet.add(i, { id: 1, value: 'asd' });
await expect(dataSet.get(i, 1)).rejects.toBeInstanceOf(AuthorizationError);
});
});
it('returns the entity when authorizeGetEntity passes', async () => {
const authorizeGetEntity = vi.fn(async () => ({ isAllowed: true, message: '' }));
await withDataSet({ authorizeGetEntity }, async ({ i, dataSet }) => {
await dataSet.add(i, { id: 1, value: 'asd' });
expect((await dataSet.get(i, 1))?.id).toBe(1);
});
});
it('throws when authorizeGetEntity fails', async () => {
const authorizeGetEntity = vi.fn(async () => ({ isAllowed: false, message: ':(' }));
await withDataSet({ authorizeGetEntity }, async ({ i, dataSet }) => {
await dataSet.add(i, { id: 1, value: 'asd' });
await expect(dataSet.get(i, 1)).rejects.toBeInstanceOf(AuthorizationError);
});
});
it('receives the full entity in authorizeGetEntity even when select is used', async () => {
const authorizeGetEntity = vi.fn(async () => ({ isAllowed: true, message: '' }));
await withDataSet({ authorizeGetEntity }, async ({ i, dataSet }) => {
await dataSet.add(i, { id: 1, value: 'asd' });
const result = await dataSet.get(i, 1, ['value']);
expect(authorizeGetEntity).toHaveBeenCalledTimes(1);
const firstCall = authorizeGetEntity.mock.calls[0];
expect(firstCall[0].entity).toEqual({ id: 1, value: 'asd' });
expect(result).toEqual({ value: 'asd' });
expect(result).not.toHaveProperty('id');
});
});
it('returns undefined without calling authorizeGetEntity when the entity does not exist', async () => {
const authorizeGetEntity = vi.fn(async () => ({ isAllowed: true, message: '' }));
await withDataSet({ authorizeGetEntity }, async ({ i, dataSet }) => {
const result = await dataSet.get(i, 999);
expect(result).toBeUndefined();
expect(authorizeGetEntity).not.toHaveBeenCalled();
});
});
});
describe('remove', () => {
it('removes the entity when no settings are provided', async () => {
await withDataSet(undefined, async ({ i, dataSet }) => {
await dataSet.add(i, { id: 1, value: 'asd' });
await dataSet.remove(i, 1);
expect(await dataSet.count(i)).toBe(0);
});
});
it('removes the entity when authorizeRemove passes', async () => {
const authorizeRemove = vi.fn(async () => ({ isAllowed: true, message: '' }));
await withDataSet({ authorizeRemove }, async ({ i, dataSet }) => {
await dataSet.add(i, { id: 1, value: 'asd' });
await dataSet.remove(i, 1);
expect(await dataSet.count(i)).toBe(0);
});
});
it('throws and preserves the entity when authorizeRemove fails', async () => {
const authorizeRemove = vi.fn(async () => ({ isAllowed: false, message: ':(' }));
await withDataSet({ authorizeRemove }, async ({ i, dataSet }) => {
await dataSet.add(i, { id: 1, value: 'asd' });
await expect(dataSet.remove(i, 1)).rejects.toBeInstanceOf(AuthorizationError);
expect(await dataSet.count(i)).toBe(1);
});
});
it('removes the entity when authorizeRemoveEntity passes', async () => {
const authorizeRemoveEntity = vi.fn(async () => ({ isAllowed: true, message: '' }));
await withDataSet({ authorizeRemoveEntity }, async ({ i, dataSet }) => {
await dataSet.add(i, { id: 1, value: 'asd' });
await dataSet.remove(i, 1);
expect(await dataSet.count(i)).toBe(0);
});
});
it('throws and preserves the entity when authorizeRemoveEntity fails', async () => {
const authorizeRemoveEntity = vi.fn(async () => ({ isAllowed: false, message: ':(' }));
await withDataSet({ authorizeRemoveEntity }, async ({ i, dataSet }) => {
await dataSet.add(i, { id: 1, value: 'asd' });
await expect(dataSet.remove(i, 1)).rejects.toBeInstanceOf(AuthorizationError);
expect(await dataSet.count(i)).toBe(1);
});
});
it('emits onEntityRemoved after a successful remove', async () => {
await withDataSet(undefined, async ({ i, dataSet }) => {
const listener = vi.fn();
dataSet.addListener('onEntityRemoved', listener);
await dataSet.add(i, { id: 1, value: 'asd' });
await dataSet.remove(i, 1);
expect(listener).toHaveBeenCalledWith(expect.objectContaining({ key: 1 }));
});
});
it('removes multiple entities at once', async () => {
await withDataSet(undefined, async ({ i, dataSet }) => {
await dataSet.add(i, { id: 1, value: 'a' }, { id: 2, value: 'b' }, { id: 3, value: 'c' });
await dataSet.remove(i, 1, 3);
expect(await dataSet.count(i)).toBe(1);
expect((await dataSet.get(i, 2))?.value).toBe('b');
});
});
it('emits onEntityRemoved for each removed key', async () => {
await withDataSet(undefined, async ({ i, dataSet }) => {
const removedKeys = [];
dataSet.addListener('onEntityRemoved', ({ key }) => {
removedKeys.push(key);
});
await dataSet.add(i, { id: 1, value: 'a' }, { id: 2, value: 'b' });
await dataSet.remove(i, 1, 2);
expect(removedKeys).toEqual([1, 2]);
});
});
it('authorizes each entity when authorizeRemoveEntity is set', async () => {
const authorizeRemoveEntity = vi.fn(async () => ({ isAllowed: true, message: '' }));
await withDataSet({ authorizeRemoveEntity }, async ({ i, dataSet }) => {
await dataSet.add(i, { id: 1, value: 'a' }, { id: 2, value: 'b' });
await dataSet.remove(i, 1, 2);
expect(authorizeRemoveEntity).toHaveBeenCalledTimes(2);
expect(await dataSet.count(i)).toBe(0);
});
});
it('does not remove anything when authorizeRemoveEntity fails for one entity (all-or-nothing)', async () => {
const authorizeRemoveEntity = vi.fn(async ({ entity }) => {
if (entity.id === 2) {
return { isAllowed: false, message: 'forbidden' };
}
return { isAllowed: true };
});
await withDataSet({ authorizeRemoveEntity }, async ({ i, dataSet }) => {
await dataSet.add(i, { id: 1, value: 'a' }, { id: 2, value: 'b' }, { id: 3, value: 'c' });
await expect(dataSet.remove(i, 1, 2, 3)).rejects.toBeInstanceOf(AuthorizationError);
expect(await dataSet.count(i)).toBe(3);
});
});
it('no-ops when called without keys', async () => {
await withDataSet(undefined, async ({ i, dataSet }) => {
const listener = vi.fn();
dataSet.addListener('onEntityRemoved', listener);
await dataSet.remove(i);
expect(listener).not.toHaveBeenCalled();
});
});
});
});
//# sourceMappingURL=data-set.spec.js.map