@furystack/repository
Version:
Repository implementation for FuryStack
135 lines (105 loc) • 4.94 kB
Markdown
DataSet implementation for FuryStack. A DataSet wraps a physical store with
entity-level business logic — authorization, modification hooks, and change
events — in a structured way.
```bash
npm install @furystack/repository
yarn add @furystack/repository
```
A DataSet is declared with `defineDataSet`. It takes the underlying
`StoreToken` (from `@furystack/core` or a backend adapter) and optional
settings (authorizers, modifiers, event callbacks). The returned
`DataSetToken` is a DI token that resolves to a ready-to-use `DataSet`.
```ts
import { createInjector } from '@furystack/inject'
import { InMemoryStore, defineStore } from '@furystack/core'
import { defineDataSet, getDataSetFor } from '@furystack/repository'
import { getLogger } from '@furystack/logging'
class MyModel {
declare id: number
declare value: string
}
const MyStore = defineStore({
name: 'my-app/MyStore',
model: MyModel,
primaryKey: 'id',
factory: () => new InMemoryStore({ model: MyModel, primaryKey: 'id' }),
})
const MyDataSet = defineDataSet({
name: 'my-app/MyDataSet',
store: MyStore,
settings: {
onEntityAdded: ({ injector, entity }) => {
getLogger(injector).verbose({ message: `An entity was added with value '${entity.value}'` })
},
authorizeUpdate: async () => ({
isAllowed: false,
message: 'This is a read-only dataset. No update is allowed. :(',
}),
},
})
const myInjector = createInjector()
```
Resolve via `injector.get(MyDataSet)` or the convenience helper
`getDataSetFor(injector, MyDataSet)`:
```ts
const dataSet = getDataSetFor(myInjector, MyDataSet)
await dataSet.add(myInjector, { id: 1, value: 'foo' }) // <-- logs via onEntityAdded
await dataSet.update(myInjector, 1, { id: 1, value: 'bar' }) // <-- rejected by authorizeUpdate
```
Events are great for logging, monitoring DataSet changes, or distributing
changes to clients. They are optional callbacks — if defined, they are called
on a specific event. Supported events: `onEntityAdded`, `onEntityUpdated`,
`onEntityRemoved`.
**Authorizers** are similar callbacks that return a promise with an
`AuthorizationResult`. You can allow or deny CRUD operations, or add
additional filters to collections. Supported authorizers: `authorizeAdd`,
`authorizeUpdate`, `authorizeUpdateEntity` (reloads the entity, compares with
the original), `authorizeRemove`, `authorizeRemoveEntity`, `authorizeGet`,
`authorizeGetEntity`.
`modifyOnAdd` / `modifyOnUpdate` transform entities before persisting (e.g.
fill `createdByUser` / `lastModifiedByUser`). `addFilter` injects a
pre-filter condition **before** a user-supplied filter expression is
evaluated, ensuring the caller only ever sees entities they have permission
for.
### Getting the Context
Every callback receives an `injector` — use it to resolve request-scoped
services like `HttpUserContext` to identify the caller.
### Server-side writes and the elevated IdentityContext
The DataSet is the **recommended write gateway** for all entity mutations.
Writing through the DataSet ensures that authorization rules, modification
hooks, and change events (`onEntityAdded`, `onEntityUpdated`,
`onEntityRemoved`) all fire. These events are required for features like
[entity sync](./../entity-sync-service/README.md) to work correctly.
> **Warning:** Writing directly to the underlying physical store bypasses
> the DataSet layer entirely. No authorization checks, hooks, or events
> fire, and downstream consumers (such as entity sync) will **not** be
> notified of the change. The `furystack/no-direct-store-token` lint rule
> guards against this in application code.
For server-side or background operations that don't originate from an HTTP
request (e.g. scheduled jobs, migrations, seed scripts), you won't have a
user session. Use `useSystemIdentityContext` from `@furystack/core` to
create a scoped child injector with elevated privileges:
```ts
import { useSystemIdentityContext } from '@furystack/core'
import { getDataSetFor } from '@furystack/repository'
import { usingAsync } from '@furystack/utils'
await usingAsync(useSystemIdentityContext({ injector, username: 'background-job' }), async (systemInjector) => {
const dataSet = getDataSetFor(systemInjector, MyDataSet)
await dataSet.add(systemInjector, { value: 'created by background job' })
})
// systemInjector is disposed here -- all scoped instances cleaned up
```
> **Warning:** `useSystemIdentityContext` bypasses **all** authorization
> checks. Only use it in trusted server-side contexts. Never pass the
> returned injector to user-facing request handlers.
This pattern ensures that all writes go through the same pipeline, keeping
authorization, hooks, and event-driven features consistent regardless of
the caller.