Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 85 additions & 13 deletions .cursor/rules/REST_SERVICE.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,74 @@ export const authorizedDataSet: Partial<DataSetSettings<unknown, unknown>> = {
}
```

## Data Access

### NEVER Use `getStoreManager()` or `StoreManager` Directly

All data access **must** go through the Repository layer (`getRepository()`) using DataSets. Direct `StoreManager` / `getStoreManager()` usage bypasses authorization and is **forbidden**.

```typescript
// ❌ FORBIDDEN - bypasses authorization
import { getStoreManager } from '@furystack/core'
const sm = getStoreManager(injector)
const users = await sm.getStoreFor(User, 'username').find({})

// ❌ FORBIDDEN - direct StoreManager injection
@Injected(StoreManager)
declare private storeManager: StoreManager

// ✅ Good - use Repository DataSets
import { getRepository } from '@furystack/repository'
const repository = getRepository(injector)
const users = await repository.getDataSetFor(User, 'username').find(injector, {})
```

### REST Action Handlers

REST action handlers receive a scoped `injector` with an `IdentityContext` already set up per-request. Pass it directly to DataSet methods:

```typescript
const MyAction: RequestAction<MyEndpoint> = async ({ injector }) => {
const repository = getRepository(injector)
const items = await repository.getDataSetFor(MyModel, 'id').find(injector, {})
return JsonResult({ items })
}
```

### Elevated Context for Background Operations

Background services, middleware, and startup code have no HTTP request context. Use `useSystemIdentityContext()` from `@furystack/core` to create a child injector with system-level privileges:

```typescript
import { useSystemIdentityContext } from '@furystack/core'
import { getRepository } from '@furystack/repository'
import { usingAsync } from '@furystack/utils'

// One-off operation (automatic cleanup with usingAsync)
await usingAsync(useSystemIdentityContext({ injector }), async (elevated) => {
const repository = getRepository(elevated)
const items = await repository.getDataSetFor(MyModel, 'id').find(elevated, {})
await repository.getDataSetFor(MyModel, 'id').update(elevated, id, changes)
})

// Singleton services (cache and dispose with service)
@Injectable({ lifetime: 'singleton' })
export class MyService {
private elevatedInjector?: Injector

private getElevatedInjector(): Injector {
if (!this.elevatedInjector) {
this.elevatedInjector = useSystemIdentityContext({ injector: getInjectorReference(this) })
}
return this.elevatedInjector
}

public async [Symbol.asyncDispose]() {
await this.elevatedInjector?.[Symbol.asyncDispose]()
}
}
```

## Store Types

### FileSystemStore
Expand Down Expand Up @@ -316,26 +384,25 @@ void attachShutdownHandler(injector)

### Seed Script

Create a seed script for initial data:
Create a seed script for initial data using elevated context:

```typescript
// service/src/seed.ts
import { StoreManager } from '@furystack/core'
import { useSystemIdentityContext } from '@furystack/core'
import { getRepository } from '@furystack/repository'
import { usingAsync } from '@furystack/utils'
import { PasswordAuthenticator, PasswordCredential } from '@furystack/security'
import { User } from 'common'
import { injector } from './config.js'

export const seed = async (i: Injector): Promise<void> => {
const sm = i.getInstance(StoreManager)
const userStore = sm.getStoreFor(User, 'username')
const pwcStore = sm.getStoreFor(PasswordCredential, 'userName')

// Create default user credentials
const cred = await i.getInstance(PasswordAuthenticator).hasher.createCredential('testuser', 'password')
await usingAsync(useSystemIdentityContext({ injector: i }), async (elevated) => {
const repository = getRepository(elevated)
const cred = await i.getInstance(PasswordAuthenticator).hasher.createCredential('testuser', 'password')

// Save to stores
await pwcStore.add(cred)
await userStore.add({ username: 'testuser', roles: [] })
await repository.getDataSetFor(PasswordCredential, 'userName').add(elevated, cred)
await repository.getDataSetFor(User, 'username').add(elevated, { username: 'testuser', roles: [] })
})
}

await seed(injector)
Expand Down Expand Up @@ -375,16 +442,21 @@ useRestService<StackCraftApi>({
3. **Validate requests** - Use `Validate` wrapper with JSON schemas
4. **Configure stores** - `FileSystemStore` for persistence, `InMemoryStore` for sessions
5. **Handle authorization** - Define authorization functions for data sets
6. **Graceful shutdown** - Implement proper cleanup with `Symbol.asyncDispose`
7. **CORS setup** - Configure for frontend origins
6. **NEVER use `getStoreManager()` or `StoreManager` directly** - Always use `getRepository().getDataSetFor()` for data access
7. **Use `useSystemIdentityContext()` from `@furystack/core`** for background services, middleware, and startup operations that lack an HTTP request context
8. **Graceful shutdown** - Implement proper cleanup with `Symbol.asyncDispose`
9. **CORS setup** - Configure for frontend origins

**Service Checklist:**

- [ ] API types defined in `common` package
- [ ] JSON schemas generated for validation
- [ ] Stores configured for all models
- [ ] DataSets created for all models via `getRepository().createDataSet()`
- [ ] Authentication set up with `useHttpAuthentication`
- [ ] Authorization functions defined
- [ ] No `getStoreManager()` or `StoreManager` usage — all data access via Repository
- [ ] Background services use `useSystemIdentityContext()` for data access
- [ ] CORS configured for frontend
- [ ] Graceful shutdown handler attached
- [ ] Error handling for startup failures
Expand Down
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,22 @@ dist
*.tsbuildinfo
frontend/bundle/js

# TypeScript compilation artifacts for config/spec files
e2e/*.js
e2e/*.d.ts
e2e/*.js.map
frontend/vite.config.js
frontend/vite.config.d.ts
frontend/vite.config.js.map
playwright.config.js
playwright.config.d.ts
playwright.config.js.map
vitest.config.mjs
vitest.config.d.mts
vitest.config.mjs.map

service/data.sqlite
service/data/
testresults

.pnp.*
Expand Down
5 changes: 5 additions & 0 deletions .yarn/versions/0de640ed.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
releases:
common: patch
frontend: patch
service: patch
stack-craft: patch
5 changes: 3 additions & 2 deletions common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@
"create-schemas": "node ./dist/bin/create-schemas.js"
},
"devDependencies": {
"@types/node": "^25.2.3",
"@types/node": "^25.3.0",
"ts-json-schema-generator": "^2.5.0",
"vitest": "^4.0.18"
},
"dependencies": {
"@furystack/rest": "^8.0.36"
"@furystack/core": "^15.1.0",
"@furystack/rest": "^8.0.37"
}
}
Loading
Loading