@furystack/repository
Version:
Repository implementation for FuryStack
149 lines • 6.23 kB
JavaScript
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