create-seiro 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/index.ts +36 -0
- package/package.json +26 -0
- package/template/.claude/skills/entity-ui.md +169 -0
- package/template/.claude/skills/new-entity.md +198 -0
- package/template/CLAUDE.md +49 -0
- package/template/app.ts +40 -0
- package/template/auth/server.ts +160 -0
- package/template/auth/types.ts +23 -0
- package/template/components/auth.ts +120 -0
- package/template/components/shared/modal.ts +205 -0
- package/template/components/shared/router.ts +11 -0
- package/template/compose.test.yml +18 -0
- package/template/compose.yml +18 -0
- package/template/index.html +23 -0
- package/template/init_db/01_extensions.sql +2 -0
- package/template/init_db/02_auth_tables.sql +7 -0
- package/template/init_db/03_auth_functions.sql +55 -0
- package/template/package.json +21 -0
- package/template/server.test.ts +129 -0
- package/template/server.ts +44 -0
- package/template/tsconfig.json +16 -0
- package/template/types.ts +9 -0
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# create-seiro
|
|
2
|
+
|
|
3
|
+
Scaffold a new Seiro project.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bunx create-seiro my-app
|
|
9
|
+
cd my-app
|
|
10
|
+
docker compose up -d
|
|
11
|
+
bun run dev
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## What's Included
|
|
15
|
+
|
|
16
|
+
The scaffolded project includes:
|
|
17
|
+
|
|
18
|
+
- **Authentication** - User registration and login with JWT tokens
|
|
19
|
+
- **Database** - PostgreSQL with Docker Compose setup
|
|
20
|
+
- **Server** - WebSocket server with CQRS pattern
|
|
21
|
+
- **Client** - Web Components with Preact Signals
|
|
22
|
+
- **Skills** - Claude skills for adding new entities
|
|
23
|
+
|
|
24
|
+
## Project Structure
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
my-app/
|
|
28
|
+
├── auth/ # Authentication types and handlers
|
|
29
|
+
├── components/ # Web components
|
|
30
|
+
│ ├── auth.ts # Login/register form
|
|
31
|
+
│ └── shared/ # Shared utilities (modal, router)
|
|
32
|
+
├── init_db/ # SQL initialization scripts
|
|
33
|
+
├── .claude/skills/ # Claude skills for development
|
|
34
|
+
├── server.ts # Server entry point
|
|
35
|
+
├── app.ts # Client entry point
|
|
36
|
+
├── types.ts # Combined types
|
|
37
|
+
└── compose.yml # Docker Compose for PostgreSQL
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Adding Features
|
|
41
|
+
|
|
42
|
+
Use the Claude skills to add new entities:
|
|
43
|
+
|
|
44
|
+
1. Ask Claude to use the `new-entity` skill
|
|
45
|
+
2. Provide entity name and fields
|
|
46
|
+
3. Claude generates types, SQL, and handlers
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
MIT
|
package/index.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { $ } from "bun";
|
|
3
|
+
import { cp, readFile, writeFile } from "fs/promises";
|
|
4
|
+
import { join, resolve } from "path";
|
|
5
|
+
|
|
6
|
+
const projectName = process.argv[2];
|
|
7
|
+
if (!projectName) {
|
|
8
|
+
console.error("Usage: bunx create-seiro <project-name>");
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const targetDir = resolve(projectName);
|
|
13
|
+
const templateDir = join(import.meta.dir, "template");
|
|
14
|
+
|
|
15
|
+
console.log(`Creating ${projectName}...`);
|
|
16
|
+
|
|
17
|
+
// Copy template
|
|
18
|
+
await cp(templateDir, targetDir, { recursive: true });
|
|
19
|
+
|
|
20
|
+
// Update package.json name
|
|
21
|
+
const pkgPath = join(targetDir, "package.json");
|
|
22
|
+
const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
|
|
23
|
+
pkg.name = projectName;
|
|
24
|
+
await writeFile(pkgPath, JSON.stringify(pkg, null, 2));
|
|
25
|
+
|
|
26
|
+
// Install dependencies
|
|
27
|
+
console.log("Installing dependencies...");
|
|
28
|
+
await $`cd ${targetDir} && bun install`.quiet();
|
|
29
|
+
|
|
30
|
+
console.log(`
|
|
31
|
+
Done! Next steps:
|
|
32
|
+
|
|
33
|
+
cd ${projectName}
|
|
34
|
+
docker compose up -d
|
|
35
|
+
bun run dev
|
|
36
|
+
`);
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-seiro",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scaffold a new Seiro project",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-seiro": "./index.ts"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.ts",
|
|
11
|
+
"template"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"seiro",
|
|
15
|
+
"scaffold",
|
|
16
|
+
"cli",
|
|
17
|
+
"cqrs",
|
|
18
|
+
"websocket"
|
|
19
|
+
],
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/blueshed/seiro.git",
|
|
24
|
+
"directory": "packages/create-seiro"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Entity UI Skill
|
|
2
|
+
|
|
3
|
+
Create UI components for entities using Web Components and Preact Signals.
|
|
4
|
+
|
|
5
|
+
## Critical: Research First
|
|
6
|
+
|
|
7
|
+
1. **Read existing components** - Look at `components/auth.ts` for the pattern
|
|
8
|
+
2. **Check app.ts** - See how views are switched
|
|
9
|
+
3. **Use shared utilities** - Don't reinvent modals, routing
|
|
10
|
+
|
|
11
|
+
## Component Architecture
|
|
12
|
+
|
|
13
|
+
### Initialization Pattern
|
|
14
|
+
|
|
15
|
+
Components are initialized with the client after connection:
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// component file
|
|
19
|
+
import { signal, effect } from "seiro/client";
|
|
20
|
+
import type { Client } from "seiro";
|
|
21
|
+
import type { Commands, Queries, Events } from "../types";
|
|
22
|
+
|
|
23
|
+
type AppClient = Client<Commands, Queries, Events>;
|
|
24
|
+
|
|
25
|
+
let client: AppClient;
|
|
26
|
+
|
|
27
|
+
export function initMyEntity(c: AppClient) {
|
|
28
|
+
client = c;
|
|
29
|
+
|
|
30
|
+
// Subscribe to events
|
|
31
|
+
client.on("entity_created", handleCreated);
|
|
32
|
+
client.on("entity_updated", handleUpdated);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// app.ts - initialize after connect
|
|
36
|
+
const profile = await client.connect<User>();
|
|
37
|
+
initMyEntity(client);
|
|
38
|
+
initAuth(client, profile); // Auth last - triggers rendering
|
|
39
|
+
client.subscribe();
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### State with Signals
|
|
43
|
+
|
|
44
|
+
Use signals for reactive state that drives UI updates:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { signal, effect } from "seiro/client";
|
|
48
|
+
|
|
49
|
+
// Module-level state
|
|
50
|
+
const entities = signal<Map<number, Entity>>(new Map());
|
|
51
|
+
|
|
52
|
+
// Update by creating new Map (immutable pattern)
|
|
53
|
+
function addEntity(entity: Entity) {
|
|
54
|
+
const next = new Map(entities.value);
|
|
55
|
+
next.set(entity.id, entity);
|
|
56
|
+
entities.value = next;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Load from query
|
|
60
|
+
async function loadEntities() {
|
|
61
|
+
const next = new Map<number, Entity>();
|
|
62
|
+
for await (const row of client.query("entities.all")) {
|
|
63
|
+
next.set(row.id, row);
|
|
64
|
+
}
|
|
65
|
+
entities.value = next;
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Web Component Structure
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
class EntityList extends HTMLElement {
|
|
73
|
+
private disposeEffects: (() => void)[] = [];
|
|
74
|
+
|
|
75
|
+
connectedCallback() {
|
|
76
|
+
this.innerHTML = `<div data-list></div>`;
|
|
77
|
+
const list = this.querySelector("[data-list]")!;
|
|
78
|
+
|
|
79
|
+
// React to signal changes
|
|
80
|
+
this.disposeEffects.push(
|
|
81
|
+
effect(() => {
|
|
82
|
+
const items = Array.from(entities.value.values());
|
|
83
|
+
list.innerHTML = items
|
|
84
|
+
.map(e => `<div>${e.name}</div>`)
|
|
85
|
+
.join("") || "<p>None yet</p>";
|
|
86
|
+
})
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
disconnectedCallback() {
|
|
91
|
+
// Clean up effects to prevent memory leaks
|
|
92
|
+
for (const dispose of this.disposeEffects) dispose();
|
|
93
|
+
this.disposeEffects = [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
customElements.define("entity-list", EntityList);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Routing
|
|
101
|
+
|
|
102
|
+
### Adding a Route
|
|
103
|
+
|
|
104
|
+
Routes are handled in `app.ts` with hash-based navigation:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import { route, navigate } from "./components/shared/router";
|
|
108
|
+
|
|
109
|
+
effect(() => {
|
|
110
|
+
const currentRoute = route.value;
|
|
111
|
+
|
|
112
|
+
if (currentRoute.startsWith("#/myview")) {
|
|
113
|
+
main.innerHTML = `<my-component></my-component>`;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Navigation Links
|
|
119
|
+
|
|
120
|
+
```html
|
|
121
|
+
<a href="#/myview" class="text-zinc-500 hover:text-zinc-300">My View</a>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Shared Utilities
|
|
125
|
+
|
|
126
|
+
### Modals (components/shared/modal.ts)
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
import { showToast, showInputModal, showConfirmModal, showFormModal } from "./shared/modal";
|
|
130
|
+
|
|
131
|
+
// Toast notification
|
|
132
|
+
showToast("Saved successfully");
|
|
133
|
+
|
|
134
|
+
// Input modal - returns string or null
|
|
135
|
+
const name = await showInputModal("Enter name", "Default value");
|
|
136
|
+
|
|
137
|
+
// Confirm modal - returns boolean
|
|
138
|
+
const confirmed = await showConfirmModal("Delete?", "This cannot be undone.");
|
|
139
|
+
|
|
140
|
+
// Form modal - returns object or null
|
|
141
|
+
const result = await showFormModal("Edit User", [
|
|
142
|
+
{ name: "name", label: "Name", required: true },
|
|
143
|
+
{ name: "email", label: "Email" },
|
|
144
|
+
]);
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Router (components/shared/router.ts)
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import { route, navigate } from "./shared/router";
|
|
151
|
+
|
|
152
|
+
// Read current route (reactive)
|
|
153
|
+
effect(() => {
|
|
154
|
+
if (route.value === "#/myview") {
|
|
155
|
+
// show my view
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Navigate programmatically
|
|
160
|
+
navigate("#/myview/123");
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Checklist
|
|
164
|
+
|
|
165
|
+
- [ ] Component has `initX(client)` function called from app.ts
|
|
166
|
+
- [ ] Event subscriptions set up in init function
|
|
167
|
+
- [ ] Signal state with immutable updates (new Map)
|
|
168
|
+
- [ ] Effects cleaned up in disconnectedCallback
|
|
169
|
+
- [ ] Routes added to app.ts effect
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# New Entity Skill
|
|
2
|
+
|
|
3
|
+
Create a new entity with database, server handlers, and client types.
|
|
4
|
+
|
|
5
|
+
## Critical: Research First, Copy Patterns
|
|
6
|
+
|
|
7
|
+
**Before writing any code:**
|
|
8
|
+
|
|
9
|
+
1. **Read existing similar code** - Look at `auth/` for the pattern
|
|
10
|
+
2. **Copy patterns exactly** - Don't invent new approaches, follow what already works
|
|
11
|
+
3. **Check the wire protocol** - Review `CLAUDE.md` for message formats
|
|
12
|
+
|
|
13
|
+
## Type Definitions
|
|
14
|
+
|
|
15
|
+
Use `Command<D, R>` and `Query<P, R>` helpers for type-safe definitions:
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// {entity}/types.ts
|
|
19
|
+
import type { Command, Query } from "seiro";
|
|
20
|
+
|
|
21
|
+
export type {Entity} = {
|
|
22
|
+
id: number;
|
|
23
|
+
name: string;
|
|
24
|
+
// fields...
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type {Entity}Commands = {
|
|
28
|
+
"{entity}.create": Command<{ name: string }, { id: number }>;
|
|
29
|
+
"{entity}.save": Command<{ id: number; name: string }, { id: number }>;
|
|
30
|
+
"{entity}.delete": Command<{ id: number }, void>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type {Entity}Queries = {
|
|
34
|
+
"{entity}s.all": Query<void, {Entity}>;
|
|
35
|
+
"{entity}.detail": Query<{ id: number }, {Entity}Detail>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type {Entity}Events = {
|
|
39
|
+
{entity}_created: {Entity};
|
|
40
|
+
{entity}_updated: {Entity};
|
|
41
|
+
};
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Typed SQL Pattern
|
|
45
|
+
|
|
46
|
+
Use postgres library's type parameter:
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// Query returning multiple rows
|
|
50
|
+
server.query("{entity}s.all", async function* (_params, ctx) {
|
|
51
|
+
if (!ctx.userId) throw new Error("Not authenticated");
|
|
52
|
+
const rows = await sql<{ query_{entity}s_all: {Entity} }[]>`
|
|
53
|
+
SELECT query_{entity}s_all(${ctx.userId})
|
|
54
|
+
`;
|
|
55
|
+
for (const row of rows) {
|
|
56
|
+
yield row.query_{entity}s_all;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Command returning result
|
|
61
|
+
server.command("{entity}.save", async (data, ctx) => {
|
|
62
|
+
if (!ctx.userId) throw new Error("Not authenticated");
|
|
63
|
+
const [row] = await sql<[{ result: { id: number } }]>`
|
|
64
|
+
SELECT cmd_{entity}_save(${ctx.userId}, ${sql.json(data)}) as result
|
|
65
|
+
`;
|
|
66
|
+
return row?.result;
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Steps
|
|
71
|
+
|
|
72
|
+
### 1. Types ({entity}/types.ts)
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import type { Command, Query } from "seiro";
|
|
76
|
+
|
|
77
|
+
export type {Entity} = {
|
|
78
|
+
id: number;
|
|
79
|
+
name: string;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export type {Entity}Commands = {
|
|
83
|
+
"{entity}.create": Command<{ name: string }, { id: number }>;
|
|
84
|
+
"{entity}.save": Command<{ id?: number; name: string }, { id: number }>;
|
|
85
|
+
"{entity}.delete": Command<{ id: number }, void>;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export type {Entity}Queries = {
|
|
89
|
+
"{entity}s.all": Query<void, {Entity}>;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export type {Entity}Events = {
|
|
93
|
+
{entity}_created: {Entity};
|
|
94
|
+
{entity}_updated: {Entity};
|
|
95
|
+
};
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 2. Database (init_db/)
|
|
99
|
+
|
|
100
|
+
Create a new numbered SQL file (e.g., `04_{entity}_tables.sql`):
|
|
101
|
+
|
|
102
|
+
```sql
|
|
103
|
+
-- Table
|
|
104
|
+
CREATE TABLE {entity}s (
|
|
105
|
+
id SERIAL PRIMARY KEY,
|
|
106
|
+
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
107
|
+
name TEXT NOT NULL,
|
|
108
|
+
created_at TIMESTAMPTZ DEFAULT now()
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
-- Command: create
|
|
112
|
+
CREATE OR REPLACE FUNCTION cmd_{entity}_create(p_user_id INTEGER, data JSONB)
|
|
113
|
+
RETURNS JSONB AS $$
|
|
114
|
+
DECLARE
|
|
115
|
+
v_result {entity}s;
|
|
116
|
+
BEGIN
|
|
117
|
+
INSERT INTO {entity}s (user_id, name)
|
|
118
|
+
VALUES (p_user_id, data->>'name')
|
|
119
|
+
RETURNING * INTO v_result;
|
|
120
|
+
|
|
121
|
+
RETURN jsonb_build_object('id', v_result.id);
|
|
122
|
+
END;
|
|
123
|
+
$$ LANGUAGE plpgsql;
|
|
124
|
+
|
|
125
|
+
-- Query: all for user
|
|
126
|
+
CREATE OR REPLACE FUNCTION query_{entity}s_all(p_user_id INTEGER)
|
|
127
|
+
RETURNS SETOF JSONB AS $$
|
|
128
|
+
SELECT jsonb_build_object('id', id, 'name', name)
|
|
129
|
+
FROM {entity}s
|
|
130
|
+
WHERE user_id = p_user_id
|
|
131
|
+
ORDER BY created_at DESC;
|
|
132
|
+
$$ LANGUAGE sql;
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### 3. Server Handlers ({entity}/server.ts)
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
import type { Sql } from "postgres";
|
|
139
|
+
import type { Server } from "seiro";
|
|
140
|
+
import type {
|
|
141
|
+
{Entity},
|
|
142
|
+
{Entity}Commands,
|
|
143
|
+
{Entity}Queries,
|
|
144
|
+
{Entity}Events,
|
|
145
|
+
} from "./types";
|
|
146
|
+
|
|
147
|
+
export function register<
|
|
148
|
+
C extends {Entity}Commands,
|
|
149
|
+
Q extends {Entity}Queries,
|
|
150
|
+
E extends {Entity}Events,
|
|
151
|
+
>(server: Server<C, Q, E>, sql: Sql) {
|
|
152
|
+
server.command("{entity}.create", async (data, ctx) => {
|
|
153
|
+
if (!ctx.userId) throw new Error("Not authenticated");
|
|
154
|
+
const [row] = await sql<[{ result: { id: number } }]>`
|
|
155
|
+
SELECT cmd_{entity}_create(${ctx.userId}, ${sql.json(data)}) as result
|
|
156
|
+
`;
|
|
157
|
+
return row?.result;
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
server.query("{entity}s.all", async function* (_params, ctx) {
|
|
161
|
+
if (!ctx.userId) throw new Error("Not authenticated");
|
|
162
|
+
const rows = await sql<{ query_{entity}s_all: {Entity} }[]>`
|
|
163
|
+
SELECT query_{entity}s_all(${ctx.userId})
|
|
164
|
+
`;
|
|
165
|
+
for (const row of rows) {
|
|
166
|
+
yield row.query_{entity}s_all;
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export const channels = ["{entity}_created", "{entity}_updated"] as const;
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### 4. Register Types (types.ts)
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
import type { {Entity}Commands, {Entity}Queries, {Entity}Events } from "./{entity}/types";
|
|
178
|
+
export type { {Entity} } from "./{entity}/types";
|
|
179
|
+
|
|
180
|
+
export type Commands = AuthCommands & {Entity}Commands;
|
|
181
|
+
export type Queries = AuthQueries & {Entity}Queries;
|
|
182
|
+
export type Events = AuthEvents & {Entity}Events;
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### 5. Register in Server (server.ts)
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
import * as {entity} from "./{entity}/server";
|
|
189
|
+
|
|
190
|
+
{entity}.register(server, sql);
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Checklist
|
|
194
|
+
|
|
195
|
+
- [ ] Types use `Command<D, R>` and `Query<P, R>` helpers
|
|
196
|
+
- [ ] Server handlers use typed SQL: `sql<[{ result: Type }]>`
|
|
197
|
+
- [ ] Database functions return JSONB with `jsonb_build_object()`
|
|
198
|
+
- [ ] Tests written and passing
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Project Dev Context
|
|
2
|
+
|
|
3
|
+
CQRS over WebSocket. Bun + Preact Signals + Web Components.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
docker compose up -d # start database
|
|
9
|
+
bun run dev # server on :3000
|
|
10
|
+
bun run check # typecheck
|
|
11
|
+
bun test # run tests
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Adding Entities
|
|
15
|
+
|
|
16
|
+
Use the new-entity skill to add new entities with proper types and handlers.
|
|
17
|
+
|
|
18
|
+
## Type Patterns
|
|
19
|
+
|
|
20
|
+
Use `Command<D, R>` and `Query<P, R>` from seiro:
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import type { Command, Query } from "seiro";
|
|
24
|
+
|
|
25
|
+
export type MyCommands = {
|
|
26
|
+
"thing.create": Command<{ name: string }, { id: number }>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type MyQueries = {
|
|
30
|
+
"things.all": Query<void, Thing>;
|
|
31
|
+
};
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Typed SQL
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
const rows = await sql<{ query_things_all: Thing }[]>`
|
|
38
|
+
SELECT query_things_all(${ctx.userId})
|
|
39
|
+
`;
|
|
40
|
+
for (const row of rows) {
|
|
41
|
+
yield row.query_things_all;
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Wire Protocol
|
|
46
|
+
|
|
47
|
+
Commands: `{ cmd: "name", cid: "abc123", data: {...} }`
|
|
48
|
+
Queries: `{ q: "name", id: 1, params: {...} }`
|
|
49
|
+
Events: `{ ev: "name", data: {...} }`
|
package/template/app.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createClient, effect } from "seiro/client";
|
|
2
|
+
import type { Commands, Queries, Events, User } from "./types";
|
|
3
|
+
import { initAuth, user } from "./components/auth";
|
|
4
|
+
import "./components/auth";
|
|
5
|
+
|
|
6
|
+
// Create client
|
|
7
|
+
const wsUrl = `ws://${window.location.host}/ws`;
|
|
8
|
+
const client = createClient<Commands, Queries, Events>(wsUrl);
|
|
9
|
+
|
|
10
|
+
// Connect and initialize
|
|
11
|
+
async function main() {
|
|
12
|
+
const profile = await client.connect<User>();
|
|
13
|
+
initAuth(client, profile);
|
|
14
|
+
client.subscribe();
|
|
15
|
+
|
|
16
|
+
// Show/hide content based on auth state
|
|
17
|
+
effect(() => {
|
|
18
|
+
const main = document.querySelector("main");
|
|
19
|
+
if (main) {
|
|
20
|
+
if (user.value) {
|
|
21
|
+
main.innerHTML = `
|
|
22
|
+
<div class="text-center py-12">
|
|
23
|
+
<h2 class="text-2xl font-bold text-white mb-4">Welcome!</h2>
|
|
24
|
+
<p class="text-zinc-400">You are logged in as ${user.value.email}</p>
|
|
25
|
+
<p class="text-zinc-500 mt-8">Start building your app by adding entities.</p>
|
|
26
|
+
</div>
|
|
27
|
+
`;
|
|
28
|
+
} else {
|
|
29
|
+
main.innerHTML = `
|
|
30
|
+
<div class="text-center py-12">
|
|
31
|
+
<h2 class="text-2xl font-bold text-white mb-4">Welcome to Seiro</h2>
|
|
32
|
+
<p class="text-zinc-400">Please login or register to continue.</p>
|
|
33
|
+
</div>
|
|
34
|
+
`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
main().catch(console.error);
|