Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e9b2f23
feat: MVP 1.
gallayl Feb 18, 2026
f3f4804
code review fixes
gallayl Feb 18, 2026
409e1c5
code review fixes pt.2
gallayl Feb 18, 2026
f552a4b
regenerated schemas
gallayl Feb 18, 2026
89d7d62
code reviews pt. 2.
gallayl Feb 18, 2026
fa51fde
repo add fix
gallayl Feb 18, 2026
89a7231
CORS and gitignore fix
gallayl Feb 18, 2026
ec0d2a7
code review fixes pt.1.
gallayl Feb 18, 2026
621fb67
user settings refactor
gallayl Feb 18, 2026
ecb75df
cr fixes
gallayl Feb 18, 2026
8d7285a
token list fix
gallayl Feb 18, 2026
e9a9b8f
page layout optimalizations
gallayl Feb 18, 2026
2728b0c
progress
gallayl Feb 18, 2026
4d1064b
progress
gallayl Feb 18, 2026
0a1faa1
progress
gallayl Feb 18, 2026
55f1e87
repository layer fixes
gallayl Feb 18, 2026
4ff2fda
progress
gallayl Feb 18, 2026
de1c1e7
code review fixes
gallayl Feb 18, 2026
cdb275c
code review fixes
gallayl Feb 19, 2026
c8f9ecc
added createElevatedContext
gallayl Feb 19, 2026
e26f6d5
FS updates, system identity context
gallayl Feb 19, 2026
c19d04a
fs update
gallayl Feb 19, 2026
87c1efa
cr fixes
gallayl Feb 19, 2026
3ab785d
visual hierarchy update
gallayl Feb 20, 2026
31e9c90
persist log entries, bulk removes
gallayl Feb 20, 2026
d35e6b6
progress on entity refactors
gallayl Feb 20, 2026
5555624
service details to data grid
gallayl Feb 21, 2026
96c7ff9
review fixes, bulk remove fixes
gallayl Feb 21, 2026
559c6c0
navigation fixes
gallayl Feb 21, 2026
e64004f
allow to clear logs
gallayl Feb 21, 2026
9a4220f
code review fixes
gallayl Feb 21, 2026
9d8c8d7
code review fixes
gallayl Feb 21, 2026
676b7e5
service creation and GH pull improvements
gallayl Feb 21, 2026
936743e
Form refactors
gallayl Feb 21, 2026
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
96 changes: 96 additions & 0 deletions .cursor/rules/SHADES_COMPONENTS.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,100 @@ export const LoginForm = Shade({
});
```

## Form Handling

### Always use `Form<T>` from `@furystack/shades-common-components`

Never use raw `<form>` HTML elements. The `Form<T>` component provides type-safe form data collection, two-tier validation (input + form level), and integration with all form field components (`Input`, `Select`, `Checkbox`, etc.).

### Pattern: Typed Form Payload

Every form needs:

1. A **payload type** describing the form data shape
2. A **type-guard `validate` function** that narrows `unknown` to the payload type
3. A `Form<T>` component with `validate` and `onSubmit` props

```typescript
import { Form, Input, Button } from '@furystack/shades-common-components';

type CreateStackPayload = {
name: string;
displayName: string;
description: string;
mainDirectory: string;
};

const isCreateStackPayload = (data: unknown): data is CreateStackPayload => {
const d = data as CreateStackPayload;
return d.name?.length > 0 && d.displayName?.length > 0 && d.mainDirectory?.length > 0;
};

// In render:
<Form<CreateStackPayload>
validate={isCreateStackPayload}
onSubmit={(data) => {
// `data` is fully typed as CreateStackPayload
void handleSubmit(data);
}}
style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}
>
<Input name="name" labelTitle="Name" variant="outlined" required />
<Input name="displayName" labelTitle="Display Name" variant="outlined" required />
<Input name="description" labelTitle="Description" variant="outlined" />
<Input name="mainDirectory" labelTitle="Main Directory" variant="outlined" required />
<Button type="submit" variant="contained">Create</Button>
</Form>
```

### Use `Checkbox` component instead of raw `<input type="checkbox">`

```typescript
import { Checkbox } from '@furystack/shades-common-components';

// ✅ Good - uses Checkbox component, integrates with Form<T>
<Checkbox name="autoFetchEnabled" labelTitle="Auto-fetch" checked={initialValue} />

// ❌ Avoid - raw checkbox, no FormService integration
<label>
<input type="checkbox" name="autoFetchEnabled" checked={initialValue} />
Auto-fetch
</label>
```

### Forbidden Patterns

Never use these patterns for form data handling:

```typescript
// ❌ FORBIDDEN - raw <form> tag
<form onsubmit={(ev) => { ... }}>

// ❌ FORBIDDEN - FormData extraction from DOM
const formData = new FormData(ev.target as HTMLFormElement);
const data = Object.fromEntries(formData.entries()) as Record<string, string>;

// ❌ FORBIDDEN - useRef to imperatively read form data
const formRef = useRef<HTMLFormElement>('myForm');
const data = new FormData(formRef.current);

// ❌ FORBIDDEN - direct DOM value access for form fields
oninput={(ev) => setValue((ev.target as HTMLInputElement).value)}
```

### Form Field Components

Use these components from `@furystack/shades-common-components` inside `Form<T>`:

- `Input` - Text, number, email, password fields
- `Select` - Single/multi select dropdowns
- `Checkbox` - Boolean checkboxes
- `TextArea` - Multi-line text input
- `Switch` - Toggle switches
- `Radio` / `RadioGroup` - Radio button groups

All form field components automatically register with the parent `Form<T>` via the `FormService` and participate in validation.

## Theming

### Access Theme Properties
Expand Down Expand Up @@ -332,6 +426,7 @@ initializeShadeRoot({
6. **useDisposable for cleanup** - Manage subscriptions properly
7. **Use common components** - Leverage `@furystack/shades-common-components`
8. **Consistent theming** - Use `ThemeProviderService` for styles
9. **Use `Form<T>` for all forms** - Never use raw `<form>` tags, `FormData`, or `useRef` for form handling

**Component Checklist:**

Expand All @@ -341,3 +436,4 @@ initializeShadeRoot({
- [ ] Observable subscriptions use `useObservable`
- [ ] Manual subscriptions cleaned up with `useDisposable`
- [ ] Theme values from `ThemeProviderService`
- [ ] Forms use `Form<T>` with typed payload and validate function
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