UNPKG

@furystack/repository

Version:

Repository implementation for FuryStack

149 lines 6.23 kB
import { AuthorizationError, selectFields } from '@furystack/core'; import { EventHub } from '@furystack/utils'; /** * Authorization-enforcing wrapper around a {@link PhysicalStore}. The * recommended write gateway for application code — `furystack/no-direct-store-token` * enforces this. Each mutation runs the relevant `authorize*` and * `modify*` callbacks from {@link DataSetSettings}, persists, then emits * `onEntityAdded` / `onEntityUpdated` / `onEntityRemoved` (consumed by * entity sync, audit logs). * * Mutating methods take an `injector` parameter to surface caller identity * to the authorizers. For server-side / background work without an HTTP * request, wrap the injector with `useSystemIdentityContext` from * `@furystack/core`. * * @example * ```ts * await usingAsync( * useSystemIdentityContext({ injector, username: 'background-job' }), * async (systemInjector) => { * const dataSet = getDataSetFor(systemInjector, UserDataSet) * await dataSet.add(systemInjector, { username: 'alice', roles: [] }) * }, * ) * ``` */ export class DataSet extends EventHub { settings; primaryKey; async add(injector, ...entities) { await Promise.all(entities.map(async (entity) => { if (this.settings.authorizeAdd) { const result = await this.settings.authorizeAdd({ injector, entity }); if (!result.isAllowed) { throw new AuthorizationError(result.message); } } })); const parsed = await Promise.all(entities.map(async (entity) => { return this.settings.modifyOnAdd ? await this.settings.modifyOnAdd({ injector, entity }) : entity; })); const createResult = await this.settings.physicalStore.add(...parsed); createResult.created.map((entity) => { this.emit('onEntityAdded', { injector, entity }); }); return createResult; } async update(injector, id, change) { if (this.settings.authorizeUpdate) { const result = await this.settings.authorizeUpdate({ injector, change }); if (!result.isAllowed) { throw new AuthorizationError(result.message); } } if (this.settings.authorizeUpdateEntity) { const entity = await this.settings.physicalStore.get(id); if (entity) { const result = await this.settings.authorizeUpdateEntity({ injector, change, entity }); if (!result.isAllowed) { throw new AuthorizationError(result.message); } } } const parsed = this.settings.modifyOnUpdate ? await this.settings.modifyOnUpdate({ injector, id, entity: change }) : change; await this.settings.physicalStore.update(id, parsed); this.emit('onEntityUpdated', { injector, change: parsed, id }); } async count(injector, filter) { if (this.settings.authorizeGet) { const result = await this.settings.authorizeGet({ injector }); if (!result.isAllowed) { throw new AuthorizationError(result.message); } } return await this.settings.physicalStore.count(filter); } async find(injector, filter) { if (this.settings.authorizeGet) { const result = await this.settings.authorizeGet({ injector }); if (!result.isAllowed) { throw new AuthorizationError(result.message); } } const parsedFilter = this.settings.addFilter ? await this.settings.addFilter({ injector, filter }) : filter; return this.settings.physicalStore.find(parsedFilter); } async get(injector, key, select) { if (this.settings.authorizeGet) { const result = await this.settings.authorizeGet({ injector }); if (!result.isAllowed) { throw new AuthorizationError(result.message); } } if (this.settings.authorizeGetEntity) { const fullEntity = await this.settings.physicalStore.get(key); if (!fullEntity) { return undefined; } const result = await this.settings.authorizeGetEntity({ injector, entity: fullEntity }); if (!result.isAllowed) { throw new AuthorizationError(result.message); } if (select) { return selectFields(fullEntity, ...select); } return fullEntity; } return await this.settings.physicalStore.get(key, select); } /** * Removes by primary key. Pre-load `authorizeRemove` and per-entity * `authorizeRemoveEntity` are all-or-nothing — any rejection aborts the * whole batch before any persist call. When `authorizeRemoveEntity` is * configured, missing keys are silently forwarded to the physical store * (no entity to authorize). */ async remove(injector, ...keys) { if (keys.length === 0) { return; } if (this.settings.authorizeRemove) { const result = await this.settings.authorizeRemove({ injector }); if (!result.isAllowed) { throw new AuthorizationError(result.message); } } if (this.settings.authorizeRemoveEntity) { const entities = await this.settings.physicalStore.find({ filter: { [this.primaryKey]: { $in: keys } }, }); await Promise.all(entities.map(async (entity) => { const removeResult = await this.settings.authorizeRemoveEntity({ injector, entity }); if (!removeResult.isAllowed) { throw new AuthorizationError(removeResult.message); } })); } await this.settings.physicalStore.remove(...keys); keys.forEach((key) => this.emit('onEntityRemoved', { injector, key })); } constructor(settings) { super(); this.settings = settings; this.primaryKey = this.settings.physicalStore.primaryKey; } } //# sourceMappingURL=data-set.js.map