Skip to content
Merged
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
2 changes: 2 additions & 0 deletions prism-framework-ui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
- Added `configureWebFetch()` for setting a global base URL
- Added Vite setup guide
- Updated docs and tests to not use `/api` prefix in endpoint paths
- Rewrote the README to match the current exports (removed stale references to `QueryProvider`, React Query, and Radix UI components; documented `webFetch` / `apiFetch` / `setFetchImplementation` / `configureWebFetch` / `cn`)
- Fixed the Vite setup proxy example to target `/api` instead of `/`, matching the server's `/api/*` endpoint prefix

## 0.2.2

Expand Down
91 changes: 71 additions & 20 deletions prism-framework-ui/README.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,89 @@
# Prism Framework UI
# @facetlayer/prism-framework-ui

A React/Next.js UI component library for building web applications with the Prism framework.
Browser/React helpers for applications built on [Prism Framework](../prism-framework).

## Overview
The package provides a thin, transport-agnostic fetch layer (`webFetch` / `apiFetch`) so UI code can call Prism endpoints the same way whether the backend is:

This library provides reusable UI components, providers, and utilities extracted from the gstone project to serve as the foundation for Prism-based web applications.

## Features

- React Query integration for data fetching
- Reusable UI components built with Radix UI
- Utility functions for styling with Tailwind CSS
- TypeScript support
- A standard HTTP server (`@facetlayer/prism-framework`)
- An Electron main process (`@facetlayer/prism-framework-desktop`)
- An in-process Expo/React Native app (`@facetlayer/prism-framework-expo`)

## Installation

```bash
pnpm add @facetlayer/prism-framework-ui
```

## API

The package exports:

| Export | Purpose |
|---|---|
| `webFetch(endpoint, options?)` | HTTP-based fetch. Parses `"METHOD /path"` strings, substitutes `:params`, serializes body/query. |
| `apiFetch(endpoint, options?)` | Universal fetch. Delegates to the implementation set by `setFetchImplementation`, or falls back to `webFetch`. |
| `setFetchImplementation(fn)` | Install a non-HTTP transport (e.g. Electron IPC, in-process Expo). UI code that calls `apiFetch` then uses it automatically. |
| `configureWebFetch({ baseUrl })` | Set a global base URL for `webFetch`. Useful when the API runs on a different origin than the UI. |
| `cn(...classes)` | `clsx` + `tailwind-merge` helper for composing Tailwind class names. |

## Usage

### Basic HTTP fetch

```typescript
import { webFetch, configureWebFetch } from '@facetlayer/prism-framework-ui';

configureWebFetch({ baseUrl: 'http://localhost:4000/api' });

// GET with query params
const users = await webFetch('GET /users', { params: { limit: 10 } });

// POST with a body
const user = await webFetch('POST /users', {
params: { name: 'John', email: 'john@example.com' },
});

// Path parameters — any `:name` segment is replaced with `params.name`
const one = await webFetch('GET /users/:id', { params: { id: '123' } });
```

Prism servers mount endpoints under `/api/` (see the `server-setup` doc in `@facetlayer/prism-framework`), so `baseUrl` should include the `/api` prefix — or you should use a proxy that rewrites paths onto `/api`.

### Cross-platform fetch

Write UI code against `apiFetch` and swap the transport at startup:

```typescript
import { apiFetch, setFetchImplementation } from '@facetlayer/prism-framework-ui';

// Default: HTTP (no setup needed — apiFetch falls back to webFetch)

// Desktop: replace with Electron IPC
import { createDesktopFetch } from '@facetlayer/prism-framework-desktop';
setFetchImplementation(createDesktopFetch());

// Expo: replace with in-process fetch returned by expoLaunch()
// setFetchImplementation(result.fetch)

// UI code is identical in all three targets:
const items = await apiFetch('GET /items');
```

### Tailwind class helper

```typescript
import { QueryProvider, cn } from '@facetlayer/prism-framework-ui';

function App() {
return (
<QueryProvider>
{/* Your app components */}
</QueryProvider>
);
}
import { cn } from '@facetlayer/prism-framework-ui';

<button className={cn('px-4 py-2', isActive && 'bg-blue-500', className)} />
```

## Setup Guides

More detailed setup guides are in the `docs/` folder:

- `vite-setup` — Vite + React setup (recommended for local tools and GUIs)
- `nextjs-setup` — Next.js setup notes (QueryClient, monorepo lockfile)

## License

MIT
38 changes: 13 additions & 25 deletions prism-framework-ui/docs/vite-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ pnpm add -D vite @vitejs/plugin-react typescript

### 2. Configure Vite proxy

The simplest approach for local development is to proxy API requests through Vite to the Prism API server. This avoids CORS issues entirely.
The simplest approach for local development is to proxy `/api` requests through Vite to the Prism API server. This avoids CORS issues entirely.

**vite.config.ts:**

Expand All @@ -51,24 +51,12 @@ export default defineConfig({
server: {
port: 4001,
proxy: {
// Proxy all non-asset requests to the API server.
// Adjust the target port to match PRISM_API_PORT.
'/': {
// The Prism server mounts every endpoint under /api/, so only
// proxy that prefix. Vite continues to serve assets, HTML, and
// HMR requests itself. Adjust the target to match PRISM_API_PORT.
'/api': {
target: 'http://localhost:4000',
bypass(req) {
// Let Vite handle HTML, JS, CSS, and HMR requests
const accept = req.headers.accept || '';
if (accept.includes('text/html')
|| req.url?.includes('.tsx')
|| req.url?.includes('.ts')
|| req.url?.includes('.js')
|| req.url?.includes('.css')
|| req.url?.startsWith('/@')
|| req.url?.startsWith('/src')
|| req.url?.startsWith('/node_modules')) {
return req.url;
}
},
changeOrigin: true,
},
},
},
Expand All @@ -79,18 +67,18 @@ With this proxy setup, `webFetch` works without any extra configuration because

### 3. Alternative: Use configureWebFetch

If you prefer not to use the Vite proxy (or need to call the API server directly), configure `webFetch` with the API base URL:
If you prefer not to use the Vite proxy (or need to call the API server directly), configure `webFetch` with the API base URL. The `baseUrl` should end with `/api`, because the server mounts every endpoint under that prefix:

```typescript
// src/main.tsx
import { configureWebFetch } from '@facetlayer/prism-framework-ui';

configureWebFetch({
baseUrl: import.meta.env.VITE_API_URL || 'http://localhost:4000',
baseUrl: (import.meta.env.VITE_API_URL || 'http://localhost:4000') + '/api',
});
```

When using this approach, make sure the API server has `allowLocalhost: true` in its CORS config.
When using this approach, make sure the API server has `allowLocalhost: true` in its CORS config (see the `cors-setup` doc in `@facetlayer/prism-framework`).

## Environment Variables

Expand Down Expand Up @@ -128,21 +116,21 @@ candle start ui

## Using webFetch

With the proxy setup, use `webFetch` the same way as in any other Prism frontend. Do not use the `/api` prefix in paths.
The Prism server mounts every endpoint under `/api/`, so the URLs issued by `webFetch` must include that prefix. Either include `/api` in each call or put it in `configureWebFetch({ baseUrl })` once and drop it from individual paths. The examples below use the "include `/api` in each call" style, which pairs well with the `'/api'` Vite proxy shown above:

```typescript
import { webFetch } from '@facetlayer/prism-framework-ui';

// GET request
const users = await webFetch('GET /users');
const users = await webFetch('GET /api/users');

// POST with data
const newUser = await webFetch('POST /users', {
const newUser = await webFetch('POST /api/users', {
params: { name: 'John', email: 'john@example.com' },
});

// Path parameters
const user = await webFetch('GET /users/:id', {
const user = await webFetch('GET /api/users/:id', {
params: { id: '123' },
});
```
9 changes: 9 additions & 0 deletions prism-framework/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
# Unreleased
- Documentation pass across the Prism libraries:
- Clarified that HTTP callers (including `prism call`) must use the `/api/` prefix, while `createEndpoint({ path })` should not; fixed the examples in README, `endpoint-tools`, and `source-directory-organization` accordingly.
- Fixed the `creating-mobile-apps` cross-platform comparison table (desktop auth uses the `getAuth` option, not Express middleware).
- Fixed the `MigrationBehavior` values listed in `launch-configuration` and `database-setup` (the real values are `strict` / `safe-upgrades` / `full-destructive-updates` / `ignore`).
- Updated `authorization` to handle the `getCurrentRequestContext() === undefined` case and to describe what `requires: ['authenticated-user']` actually does.
- Corrected the `generate-api-clients-config` doc to reference `/api/openapi.json` and the `port-assignment`-based port resolution instead of a legacy `.env` lookup.
- Expanded `overview` and the README's doc list to include previously unreferenced docs (`creating-mobile-apps`, `cors-setup`, `error-handling`, `generate-api-clients-config`, `metrics`, `source-directory-organization`).

# 0.7.0
- Added `configureMetrics({ appName })` to label all metrics with an `app` identifier
- Added `metricsConfig` option to `ServerSetupConfig` for easy setup
Expand Down
27 changes: 19 additions & 8 deletions prism-framework/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ async function main() {
main().catch(console.error);
```

The endpoint above is served at `GET http://localhost:3000/api/hello` — every endpoint is mounted under `/api/`, so the `path` you pass to `createEndpoint` does **not** need an `/api` prefix of its own.

## CLI Commands

| Command | Description |
Expand All @@ -64,10 +66,11 @@ main().catch(console.error);
# List all endpoints
prism list-endpoints

# Call endpoints
prism call /users # GET request
prism call POST /users --name "John" # POST with body
prism call POST /data --config '{"timeout":30}' # JSON arguments
# Call endpoints — note that HTTP paths include the /api/ prefix
# (the framework mounts every endpoint under /api/)
prism call /api/users # GET request
prism call POST /api/users --name "John" # POST with body
prism call POST /api/data --config '{"timeout":30}' # JSON arguments

# Generate TypeScript types
prism generate-api-clients --out ./src/api-types.ts
Expand All @@ -90,25 +93,33 @@ Available documentation includes:
- `getting-started` - Setup guide for Prism Framework projects
- `overview` - Framework overview and concepts
- `creating-services` - How to create services and endpoints
- `creating-mobile-apps` - Running a Prism app on Expo/React Native
- `server-setup` - Server configuration options
- `database-setup` - Database integration
- `authorization` - Authentication and authorization
- `launch-configuration` - App configuration options
- `source-directory-organization` - Recommended project layout
- `endpoint-tools` - CLI tools for calling endpoints
- `env-files` - Environment configuration strategy
- `cors-setup` - Cross-origin request configuration
- `error-handling` - HTTP error classes
- `generate-api-clients-config` - Configuring generated TypeScript API clients
- `metrics` - Prometheus metrics integration
- `stdin-protocol` - Stdin/stdout protocol for subprocess communication

## Testing Endpoints

Use the `prism` CLI to test your endpoints:

```bash
prism list-endpoints # See all available endpoints
prism call /hello # Call an endpoint (GET)
prism call POST /users --name "John" --email "john@example.com" # POST with data
prism list-endpoints # See all available endpoints
prism call /api/hello # Call an endpoint (GET)
prism call POST /api/users --name "John" --email "john@example.com" # POST with data
```

This is preferred over using curl directly because the `prism` CLI automatically reads your `.env` file for the API port.
The framework mounts every endpoint under `/api/` (so a `createEndpoint({ path: '/hello' })` is served at `GET /api/hello`). Pass the full HTTP path — including `/api/` — to `prism call`.

This is preferred over using curl directly because the `prism` CLI automatically resolves the API port for the current project directory.

## Environment Variables

Expand Down
39 changes: 26 additions & 13 deletions prism-framework/docs/authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,17 @@ if (auth.hasPermission('write:projects')) {

## Request Context Integration

Every request has an `Authorization` instance in its context:
Every in-flight request has an `Authorization` instance attached to its `RequestContext`. Retrieve it with `getCurrentRequestContext()`:

```typescript
import { getCurrentRequestContext } from '@facetlayer/prism-framework';
import { getCurrentRequestContext, UnauthorizedError } from '@facetlayer/prism-framework';

// In endpoint handler
const context = getCurrentRequestContext();
if (!context) {
// This only happens when code runs outside of any request (e.g. a background job).
throw new Error('No request context available');
}
const auth = context.auth;

// Check if user is authenticated
Expand All @@ -103,6 +107,8 @@ if (!user) {
}
```

`getCurrentRequestContext()` returns `RequestContext | undefined`. It is defined whenever code is reached through an HTTP request (web), an IPC call (desktop), or an `apiFetch`/`callEndpoint` on mobile — but it is undefined in `startJobs` callbacks and other background code, so always handle the undefined case in shared helpers.

## Middleware Example

Create middleware to populate authorization:
Expand Down Expand Up @@ -149,46 +155,53 @@ export const authMiddleware: MiddlewareDefinition = {

## Endpoint Requirements

Use the `requires` array to enforce authentication:
The `requires` array is a hint for framework-aware tooling and middleware. Today the only value the framework defines is:

- `'authenticated-user'` — indicates the handler assumes a `user` resource is present on `context.auth`.

The base framework does **not** automatically reject unauthenticated calls based on `requires`; the handler (or a middleware you write) is still responsible for the actual check. Treat `requires` as documentation the framework can consume (for OpenAPI metadata, middleware, etc.), and always guard handlers explicitly:

```typescript
createEndpoint({
method: 'GET',
path: '/api/protected',
path: '/protected', // served at /api/protected
requires: ['authenticated-user'],
handler: async () => {
// This handler only runs if user is authenticated
const context = getCurrentRequestContext();
const user = context.auth.getResource('user');
const user = context?.auth.getResource('user');
if (!user) {
throw new UnauthorizedError('Authentication required');
}
return { userId: user.id };
},
});
```

The `'authenticated-user'` requirement checks that a user resource exists in the authorization context.

## Custom Authorization Checks

Implement custom authorization logic in your handlers:

```typescript
import { ForbiddenError } from '@facetlayer/prism-framework';
import { ForbiddenError, UnauthorizedError, getCurrentRequestContext } from '@facetlayer/prism-framework';

createEndpoint({
method: 'DELETE',
path: '/api/projects/:projectId',
path: '/projects/:projectId', // served at /api/projects/:projectId
requires: ['authenticated-user'],
handler: async (input) => {
const context = getCurrentRequestContext();
const user = context?.auth.getResource('user');
if (!user) {
throw new UnauthorizedError('Authentication required');
}

// Check permission
if (!context.auth.hasPermission('delete:projects')) {
if (!context!.auth.hasPermission('delete:projects')) {
throw new ForbiddenError('Insufficient permissions');
}

// Check project ownership
const project = await getProject(input.projectId);
const user = context.auth.getResource('user');

if (project.ownerId !== user.id) {
throw new ForbiddenError('Not the project owner');
Expand Down Expand Up @@ -227,7 +240,7 @@ Common permission naming patterns:
Create helper functions for common authorization checks:

```typescript
import { getCurrentRequestContext, ForbiddenError } from '@facetlayer/prism-framework';
import { getCurrentRequestContext, ForbiddenError, UnauthorizedError } from '@facetlayer/prism-framework';

export function requirePermission(permission: string) {
const context = getCurrentRequestContext();
Expand Down
10 changes: 5 additions & 5 deletions prism-framework/docs/creating-mobile-apps.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,9 +196,9 @@ Express middleware defined on services is ignored on mobile (a warning is logged
| | Web | Desktop | Mobile |
|---|---|---|---|
| Package | `prism-framework` | `prism-framework-desktop` | `prism-framework-expo` |
| Transport | HTTP (Express) | Electron IPC | In-process `callEndpoint()` |
| Transport | HTTP (Express) | Electron IPC (or a loopback Express server) | In-process `callEndpoint()` |
| Database | `better-sqlite3` | `better-sqlite3` | `expo-sqlite` |
| UI fetch | `webFetch` (HTTP) | `window.electron.apiCall` | `createExpoFetch` (direct) |
| Events | SSE `ConnectionManager` | SSE `ConnectionManager` | `ExpoEventEmitter` |
| Auth | Express middleware | Express middleware | `getAuth` option |
| Import path | `@facetlayer/prism-framework` | `@facetlayer/prism-framework` | `@facetlayer/prism-framework/core` |
| UI fetch | `webFetch` (HTTP) | `createDesktopFetch` (via `window.electron.apiCall`) | `createExpoFetch` (direct) |
| Events | SSE `ConnectionManager` | SSE `ConnectionManager` (Option B only) | `ExpoEventEmitter` |
| Auth | Express middleware | `getAuth` option on `desktopLaunch` | `getAuth` option on `expoLaunch` |
| Import path | `@facetlayer/prism-framework` | `@facetlayer/prism-framework` and `@facetlayer/prism-framework/core` | `@facetlayer/prism-framework/core` |
Loading
Loading