create-seiro 0.1.1 → 0.1.2

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/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "create-seiro",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Scaffold a new Seiro project",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "create-seiro": "./index.ts"
8
8
  },
9
9
  "scripts": {
10
- "test": "bun test --ignore-pattern 'template/**/*'"
10
+ "test": "bun test"
11
11
  },
12
12
  "files": [
13
13
  "index.ts",
@@ -0,0 +1,141 @@
1
+ ---
2
+ name: cqrs-document
3
+ description: Document-first CQRS design through conversation. Use when designing systems where users see documents (views) that are kept up to date by commands. Outputs SQL migrations and TypeScript types directly - no intermediate DSL. Triggers on discussions about documents, commands, entities, CQRS, Seiro protocol, or system design conversations that involve "what does the user see" questions.
4
+ ---
5
+
6
+ # Document-First CQRS Design
7
+
8
+ Design systems by starting with what the user sees (the document), then deriving entities and commands.
9
+
10
+ ## Core Principle
11
+
12
+ A document is a composition of entity views. Commands exist to transition the document from one valid state to another. Entities are derived from what's needed to support the documents.
13
+
14
+ ## Conversation Workflow
15
+
16
+ ### 1. Establish the Document
17
+
18
+ Ask: "What does the user see?"
19
+
20
+ Capture the shape:
21
+ ```
22
+ Document Venue {
23
+ id, name, location
24
+ sites: [Site { id, name, position, dimensions, area_id? }]
25
+ areas: [Area { id, name, parent_id? }]
26
+ }
27
+ ```
28
+
29
+ - Nesting implies joins
30
+ - `?` means optional/nullable
31
+ - `[]` means array/list
32
+ - Use entity names, subset fields for views
33
+
34
+ ### 2. Define Commands
35
+
36
+ Commands that transition the document:
37
+ ```
38
+ commands {
39
+ save_venue(name, location, id?)
40
+ delete_venue(id)
41
+ save_site(venue_id, name, position, dimensions, area_id?, id?)
42
+ delete_site(id)
43
+ }
44
+ ```
45
+
46
+ Conventions:
47
+ - `save_*` for create/update (id absent = create, id present = update)
48
+ - Required parameters first, optional `id?` last
49
+ - Commands return `{ id }` on success
50
+ - `delete_*` with cascade logic documented
51
+
52
+ ### 3. Derive Entities
53
+
54
+ Extract the minimal tables:
55
+ ```
56
+ venues (id, name, location)
57
+ sites (id, venue_id FK, name, position, dimensions, area_id?)
58
+ areas (id, venue_id FK, name, parent_id?)
59
+ ```
60
+
61
+ ### 4. Output Code
62
+
63
+ Once document is agreed, write directly:
64
+
65
+ **SQL Migration** - Tables, constraints, command functions with pg_notify
66
+ **TypeScript Types** - Document shapes, command/query types for Seiro
67
+
68
+ ## Output Conventions
69
+
70
+ ### SQL Command Functions
71
+
72
+ ```sql
73
+ CREATE FUNCTION cmd_entity_save(
74
+ p_required_field text,
75
+ p_optional_field text DEFAULT NULL,
76
+ p_id int DEFAULT NULL
77
+ ) RETURNS jsonb AS $$
78
+ DECLARE
79
+ v_entity entities%ROWTYPE;
80
+ BEGIN
81
+ IF p_id IS NULL THEN
82
+ INSERT INTO entities (...) VALUES (...) RETURNING * INTO v_entity;
83
+ ELSE
84
+ UPDATE entities SET ... WHERE id = p_id RETURNING * INTO v_entity;
85
+ END IF;
86
+
87
+ PERFORM pg_notify('entity_saved', jsonb_build_object(...));
88
+ RETURN jsonb_build_object('id', v_entity.id);
89
+ END;
90
+ $$ LANGUAGE plpgsql;
91
+ ```
92
+
93
+ ### SQL Delete with Cascade
94
+
95
+ ```sql
96
+ CREATE FUNCTION cmd_entity_delete(p_id int) RETURNS void AS $$
97
+ BEGIN
98
+ -- Nullify references before delete
99
+ UPDATE children SET entity_id = NULL WHERE entity_id = p_id;
100
+ DELETE FROM entities WHERE id = p_id;
101
+ PERFORM pg_notify('entity_deleted', jsonb_build_object('id', p_id));
102
+ END;
103
+ $$ LANGUAGE plpgsql;
104
+ ```
105
+
106
+ ### TypeScript Document Type
107
+
108
+ ```typescript
109
+ export type VenueDocument = {
110
+ id: number
111
+ name: string
112
+ location: string
113
+ sites: Site[]
114
+ areas: Area[]
115
+ }
116
+
117
+ export type Site = {
118
+ id: number
119
+ name: string
120
+ position: { x: number, y: number }
121
+ dimensions: { w: number, h: number }
122
+ area_id: number | null
123
+ }
124
+ ```
125
+
126
+ ## What NOT To Do
127
+
128
+ - No intermediate DSL or spec files
129
+ - No compiler or code generator to maintain
130
+ - No UML diagrams
131
+ - No abstract entity modelling before documents are clear
132
+
133
+ ## Iteration Pattern
134
+
135
+ When requirements change:
136
+ 1. Update the document shape in conversation
137
+ 2. Identify entity/command changes
138
+ 3. Write migration SQL
139
+ 4. Update TypeScript types
140
+
141
+ The code is the spec. PostgreSQL enforces it. TypeScript checks it.
@@ -0,0 +1,288 @@
1
+ # Seiro Protocol Reference
2
+
3
+ CQRS over WebSocket with Bun + Preact Signals + Web Components.
4
+
5
+ ## Wire Format
6
+
7
+ ```
8
+ ← { profile } sent on connect (User or null)
9
+
10
+ → { cmd, cid, data } command request
11
+ ← { cid, result } command success
12
+ ← { cid, err } command error
13
+
14
+ → { q, id, params } query request
15
+ ← { id, row } query row (repeated)
16
+ ← { id } query end
17
+ ← { id, err } query error
18
+
19
+ ← { ev, data } event broadcast
20
+
21
+ → { sub: "pattern" } subscribe to events
22
+ → { unsub: "pattern" } unsubscribe
23
+ ```
24
+
25
+ ## Type Definitions
26
+
27
+ Use `Command<D, R>` and `Query<P, R>` helpers:
28
+
29
+ ```typescript
30
+ import type { Command, Query } from "seiro";
31
+
32
+ export type Entity = {
33
+ id: number;
34
+ name: string;
35
+ };
36
+
37
+ export type EntityCommands = {
38
+ "entity.create": Command<{ name: string }, { id: number }>;
39
+ "entity.save": Command<{ id: number; name: string }, { id: number }>;
40
+ };
41
+
42
+ export type EntityQueries = {
43
+ "entities.all": Query<void, Entity>;
44
+ "entity.detail": Query<{ id: number }, EntityDetail>;
45
+ };
46
+
47
+ export type EntityEvents = {
48
+ entity_created: Entity;
49
+ entity_updated: Entity;
50
+ };
51
+ ```
52
+
53
+ ## Server Setup
54
+
55
+ ```typescript
56
+ import postgres from "postgres";
57
+ import { createServer } from "seiro/server";
58
+ import * as auth from "./auth/server";
59
+ import * as entity from "./entity/server";
60
+
61
+ const sql = postgres(DATABASE_URL);
62
+
63
+ const server = createServer({
64
+ port: 3000,
65
+ auth: {
66
+ verify: auth.verifyToken,
67
+ public: ["auth.register", "auth.login"],
68
+ },
69
+ healthCheck: async () => {
70
+ await sql`SELECT 1`;
71
+ return true;
72
+ },
73
+ });
74
+
75
+ // Register domain handlers
76
+ auth.register(server, sql);
77
+ entity.register(server, sql);
78
+
79
+ await server.start({ "/": homepage });
80
+ ```
81
+
82
+ ## Server Handler Pattern
83
+
84
+ ```typescript
85
+ import type { Sql } from "postgres";
86
+ import type { Server } from "seiro";
87
+ import type { Entity, EntityCommands, EntityQueries, EntityEvents } from "./types";
88
+
89
+ export function register<
90
+ C extends EntityCommands,
91
+ Q extends EntityQueries,
92
+ E extends EntityEvents,
93
+ >(server: Server<C, Q, E>, sql: Sql) {
94
+
95
+ // Send profile on connect (auth example)
96
+ server.onOpen(async (ctx) => {
97
+ if (!ctx.userId) {
98
+ ctx.send({ profile: null });
99
+ return;
100
+ }
101
+ // fetch and send user profile
102
+ ctx.send({ profile: user });
103
+ });
104
+
105
+ // Command with typed result
106
+ server.command("entity.save", async (data, ctx) => {
107
+ if (!ctx.userId) throw new Error("Not authenticated");
108
+ const [row] = await sql<[{ result: { id: number } }]>`
109
+ SELECT cmd_entity_save(${ctx.userId}, ${sql.json(data)}) as result
110
+ `;
111
+ return row?.result;
112
+ });
113
+
114
+ // Query with typed rows (generator)
115
+ server.query("entities.all", async function* (_params, ctx) {
116
+ if (!ctx.userId) throw new Error("Not authenticated");
117
+ const rows = await sql<{ query_entities_all: Entity }[]>`
118
+ SELECT query_entities_all(${ctx.userId})
119
+ `;
120
+ for (const row of rows) {
121
+ yield row.query_entities_all;
122
+ }
123
+ });
124
+ }
125
+
126
+ // Broadcast events from server
127
+ server.emit("entity_created", entity);
128
+ ```
129
+
130
+ ## Client Setup
131
+
132
+ ```typescript
133
+ import { createClient, effect } from "seiro/client";
134
+ import type { Commands, Queries, Events, User } from "./types";
135
+
136
+ const client = createClient<Commands, Queries, Events>(wsUrl);
137
+
138
+ // Connect returns profile (User | null)
139
+ const profile = await client.connect<User>();
140
+
141
+ // Set up event listeners before subscribing
142
+ client.on("entity_created", (data) => updateList(data));
143
+
144
+ // Start receiving events
145
+ client.subscribe();
146
+ ```
147
+
148
+ ## Client API
149
+
150
+ ```typescript
151
+ // Command with callbacks
152
+ client.cmd("entity.save", { id: 1, name: "Updated" }, {
153
+ onSuccess: (result) => navigate(`#/entities/${result.id}`),
154
+ onError: (err) => showError(err),
155
+ });
156
+
157
+ // Query (async iterator)
158
+ for await (const row of client.query("entities.all")) {
159
+ items.push(row);
160
+ }
161
+
162
+ // Query all at once
163
+ const items = await client.queryAll("entities.all");
164
+
165
+ // Event subscription (returns unsubscribe function)
166
+ const unsubscribe = client.on("entity_*", (data) => handle(data));
167
+
168
+ // Sync events to a signal with reducer
169
+ const entities = client.sync("entity_updated", initialState, (state, event) => {
170
+ return { ...state, [event.id]: event };
171
+ });
172
+
173
+ // Sync events to a Map signal
174
+ const entityMap = client.syncMap("entity_updated", (e) => e.id);
175
+
176
+ // Connection state (reactive signal)
177
+ effect(() => {
178
+ if (client.connected.value) {
179
+ console.log("Connected");
180
+ }
181
+ });
182
+
183
+ // Auth token management
184
+ client.setToken(token);
185
+ client.getToken();
186
+ client.logout(); // clears token
187
+
188
+ // Connection management
189
+ client.close();
190
+ await client.reconnect();
191
+ ```
192
+
193
+ ## Web Component Pattern
194
+
195
+ ```typescript
196
+ import { signal, effect } from "seiro/client";
197
+ import type { Client } from "seiro";
198
+ import type { Commands, Queries, Events, Entity } from "../types";
199
+
200
+ type AppClient = Client<Commands, Queries, Events>;
201
+
202
+ // Module-level state
203
+ const entities = signal<Map<number, Entity>>(new Map());
204
+ let client: AppClient;
205
+
206
+ export function initEntity(c: AppClient) {
207
+ client = c;
208
+ client.on("entity_created", (entity) => {
209
+ const next = new Map(entities.value);
210
+ next.set(entity.id, entity);
211
+ entities.value = next;
212
+ });
213
+ }
214
+
215
+ class EntityList extends HTMLElement {
216
+ connectedCallback() {
217
+ this.innerHTML = `<div data-list></div>`;
218
+ const list = this.querySelector("[data-list]")!;
219
+
220
+ effect(() => {
221
+ const items = Array.from(entities.value.values());
222
+ list.innerHTML = items
223
+ .map(e => `<div>${e.name}</div>`)
224
+ .join("") || "<p>None yet</p>";
225
+ });
226
+ }
227
+ }
228
+
229
+ customElements.define("entity-list", EntityList);
230
+ ```
231
+
232
+ ## File Structure
233
+
234
+ ```
235
+ project/
236
+ types.ts # Combined Commands, Queries, Events
237
+ server.ts # Server setup, registers handlers
238
+ app.ts # Client setup, routing
239
+ init_db/ # SQL migrations
240
+ {domain}/
241
+ types.ts # Domain types using Command/Query helpers
242
+ server.ts # Domain handlers with typed SQL
243
+ components/
244
+ {domain}.ts # Web Components with signals
245
+ shared/
246
+ modal.ts # Shared modal utilities
247
+ router.ts # Hash-based routing
248
+ ```
249
+
250
+ ## SQL Conventions
251
+
252
+ Commands return `{ id }` via JSONB:
253
+
254
+ ```sql
255
+ CREATE FUNCTION cmd_entity_save(p_user_id int, data jsonb)
256
+ RETURNS jsonb AS $$
257
+ DECLARE
258
+ v_entity entities%ROWTYPE;
259
+ BEGIN
260
+ INSERT INTO entities (user_id, name)
261
+ VALUES (p_user_id, data->>'name')
262
+ RETURNING * INTO v_entity;
263
+
264
+ PERFORM pg_notify('entity_created', row_to_json(v_entity)::text);
265
+ RETURN jsonb_build_object('id', v_entity.id);
266
+ END;
267
+ $$ LANGUAGE plpgsql;
268
+ ```
269
+
270
+ Queries return SETOF jsonb:
271
+
272
+ ```sql
273
+ CREATE FUNCTION query_entities_all(p_user_id int)
274
+ RETURNS SETOF jsonb AS $$
275
+ SELECT jsonb_build_object('id', id, 'name', name)
276
+ FROM entities
277
+ WHERE user_id = p_user_id
278
+ ORDER BY created_at DESC;
279
+ $$ LANGUAGE sql;
280
+ ```
281
+
282
+ ## Conventions
283
+
284
+ - Commands return `{ id }` for create/save operations
285
+ - Queries stream rows, end with empty `{ id }`
286
+ - Use typed SQL: `sql<[{ result: Type }]>` or `sql<{ fn_name: Type }[]>`
287
+ - Events broadcast full data via pg_notify
288
+ - Pattern subscriptions support wildcards: `entity_*`
@@ -11,9 +11,9 @@ bun run check # typecheck
11
11
  bun test # run tests
12
12
  ```
13
13
 
14
- ## Adding Entities
14
+ ## Adding Features
15
15
 
16
- Use the new-entity skill to add new entities with proper types and handlers.
16
+ Use the `cqrs-document` skill for document-first CQRS design through conversation.
17
17
 
18
18
  ## Type Patterns
19
19
 
@@ -1,169 +0,0 @@
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
@@ -1,198 +0,0 @@
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