@akanjs/cli 2.3.1-rc.3 → 2.3.1-rc.4
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/index.js +9 -2
- package/package.json +2 -2
- package/templates/appSample/common/formatters.ts +32 -0
- package/templates/appSample/common/validators.ts +32 -0
- package/templates/appSample/lib/__scalar/workHistory/workHistory.abstract.ts +47 -0
- package/templates/appSample/lib/__scalar/workHistory/workHistory.constant.ts +38 -0
- package/templates/appSample/lib/__scalar/workHistory/workHistory.dictionary.ts +28 -0
- package/templates/appSample/lib/_noti/noti.abstract.ts +38 -0
- package/templates/appSample/lib/_noti/noti.dictionary.ts +25 -0
- package/templates/appSample/lib/_noti/noti.service.ts +31 -0
- package/templates/appSample/lib/_noti/noti.signal.ts +30 -0
- package/templates/appSample/lib/_noti/noti.store.ts +38 -0
- package/templates/appSample/lib/task/Task.Template.tsx +49 -0
- package/templates/appSample/lib/task/Task.Unit.tsx +70 -0
- package/templates/appSample/lib/task/Task.Util.tsx +103 -0
- package/templates/appSample/lib/task/Task.View.tsx +94 -0
- package/templates/appSample/lib/task/Task.Zone.tsx +66 -0
- package/templates/appSample/lib/task/task.abstract.ts +40 -0
- package/templates/appSample/lib/task/task.constant.ts +54 -0
- package/templates/appSample/lib/task/task.dictionary.ts +85 -0
- package/templates/appSample/lib/task/task.document.ts +54 -0
- package/templates/appSample/lib/task/task.service.ts +48 -0
- package/templates/appSample/lib/task/task.signal.ts +49 -0
- package/templates/appSample/lib/task/task.store.ts +45 -0
- package/templates/appSample/page/task/[taskId]/_index.tsx +39 -0
- package/templates/appSample/page/task/[taskId]/edit.tsx +34 -0
- package/templates/appSample/page/task/_index.tsx +38 -0
- package/templates/appSample/page/task/_layout.tsx +24 -0
- package/templates/appSample/page/task/new.tsx +34 -0
- package/templates/appSample/srvkit/AuthGuard.ts +35 -0
- package/templates/appSample/srvkit/SessionInternalArg.ts +32 -0
- package/templates/appSample/ui/GlobalLoading.tsx +30 -0
- package/templates/appSample/ui/QuantityControl.tsx +52 -0
- package/templates/appSample/webkit/useDebounce.ts +41 -0
- package/templates/workspaceRoot/AGENTS.md.template +470 -16
|
@@ -7,7 +7,7 @@ the framework generate the repeated surfaces.
|
|
|
7
7
|
|
|
8
8
|
- `apps/<app>` contains application pages, app UI, app domain modules, env files, and `akan.config.ts`.
|
|
9
9
|
- `libs/<lib>` contains shared domain and utility code reused by apps.
|
|
10
|
-
- `apps/<app>/page` contains file-routed pages. Index pages use `_index.tsx`; nested layouts use `_layout.tsx`.
|
|
10
|
+
- `apps/<app>/page` contains server-side file-routed pages. Index pages use `_index.tsx`; nested layouts use `_layout.tsx`. routeName.tsx becomes /routeName and [modelId].tsx becomes /[modelId].
|
|
11
11
|
- `apps/<app>/lib/<model>` contains database-backed domain modules.
|
|
12
12
|
- `apps/<app>/lib/_<service>` contains service modules that are not database document models.
|
|
13
13
|
- `apps/<app>/lib/__scalar/<scalar>` contains reusable scalar/value types.
|
|
@@ -20,10 +20,6 @@ Do not hand-edit generated Akan files. Regenerate them through Akan scan, lint,
|
|
|
20
20
|
CLI command instead.
|
|
21
21
|
|
|
22
22
|
Common generated files include:
|
|
23
|
-
|
|
24
|
-
- `apps/*/akan.app.json`
|
|
25
|
-
- `libs/*/akan.lib.json`
|
|
26
|
-
- `pkgs/*/akan.pkg.json`
|
|
27
23
|
- `apps/*/client.ts`
|
|
28
24
|
- `apps/*/server.ts`
|
|
29
25
|
- `apps/*/lib/cnst.ts`
|
|
@@ -34,11 +30,11 @@ Common generated files include:
|
|
|
34
30
|
- `apps/*/lib/sig.ts`
|
|
35
31
|
- `apps/*/lib/useClient.ts`
|
|
36
32
|
- `apps/*/lib/useServer.ts`
|
|
33
|
+
- `apps/*/lib/*/index.ts`
|
|
37
34
|
- `apps/*/ui/index.ts`
|
|
38
35
|
- `apps/*/webkit/index.ts`
|
|
39
36
|
- `apps/*/srvkit/index.ts`
|
|
40
37
|
- `apps/*/common/index.ts`
|
|
41
|
-
- module `index.ts` and `index.tsx` files generated by scan
|
|
42
38
|
|
|
43
39
|
## Domain Module Responsibilities
|
|
44
40
|
|
|
@@ -52,11 +48,11 @@ Use the local module shape before adding a new abstraction.
|
|
|
52
48
|
- `<model>.document.ts` owns persistence and document queries.
|
|
53
49
|
- `<model>.service.ts` owns business logic.
|
|
54
50
|
- `<model>.store.ts` owns domain state and actions.
|
|
55
|
-
- `<Model>.Template.tsx` owns form-oriented UI.
|
|
56
|
-
- `<Model>.Unit.tsx` owns list/item UI.
|
|
57
|
-
- `<Model>.View.tsx` owns detail UI.
|
|
58
|
-
- `<Model>.Zone.tsx` owns page/container integration.
|
|
59
|
-
- `<model>.Util.tsx` owns small module UI helpers.
|
|
51
|
+
- `<Model>.Template.tsx` owns form-oriented UI. Client components, with 'use client'.
|
|
52
|
+
- `<Model>.Unit.tsx` owns list/item UI. Server components, no 'use client'.
|
|
53
|
+
- `<Model>.View.tsx` owns detail UI. Server components, no 'use client'.
|
|
54
|
+
- `<Model>.Zone.tsx` owns page/container integration. Client components, with 'use client'.
|
|
55
|
+
- `<model>.Util.tsx` owns small module UI helpers. Client components, with 'use client'.
|
|
60
56
|
|
|
61
57
|
## Agent Workflow
|
|
62
58
|
|
|
@@ -73,12 +69,43 @@ Use the local module shape before adding a new abstraction.
|
|
|
73
69
|
|
|
74
70
|
Run commands from the workspace root unless a task says otherwise.
|
|
75
71
|
|
|
72
|
+
### Module Addition Workflow
|
|
73
|
+
|
|
74
|
+
When adding a new database-backed domain module (e.g., product, user):
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# 1. Scaffold the module with Akan CLI (creates constant, service, signal, store, document files)
|
|
78
|
+
akan create-module <module-name> --app <%= appName %>
|
|
79
|
+
|
|
80
|
+
# 2. Start dev server with HMR and type checking at http://localhost:8282
|
|
81
|
+
akan start <%= appName %>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Change Verification Workflow
|
|
85
|
+
|
|
86
|
+
After any code change, run these in order:
|
|
87
|
+
|
|
76
88
|
```bash
|
|
77
|
-
|
|
78
|
-
akan
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
akan
|
|
89
|
+
# 1. Fast lint check — Akan.js conventions and Biome rules
|
|
90
|
+
akan lint <%= appName %>
|
|
91
|
+
|
|
92
|
+
# 2. Type-only check — catches server/client boundary violations and import errors
|
|
93
|
+
akan typecheck <%= appName %>
|
|
94
|
+
|
|
95
|
+
# 3. Test — Run the test code (lib/*/*.signal.test.ts or others)
|
|
96
|
+
akan test <%= appName %>
|
|
97
|
+
|
|
98
|
+
# 4. Full production build — bundles the app, runs all type/lint checks combined
|
|
99
|
+
akan build <%= appName %>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Other Frequently Used Commands
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
akan create-scalar <scalar-name> --app <%= appName %> # Add a scalar module (lib/__scalar/<scalar-name>/)
|
|
106
|
+
akan create-service <service-name> --app <%= appName %> # Add a service module (lib/_<service-name>/)
|
|
107
|
+
akan test <%= appName %> # Run the test code (lib/*/*.signal.test.ts or others)
|
|
108
|
+
akan lint <%= appName %> # Lint only (no typecheck)
|
|
82
109
|
```
|
|
83
110
|
|
|
84
111
|
For the default generated app, start with:
|
|
@@ -86,3 +113,430 @@ For the default generated app, start with:
|
|
|
86
113
|
```bash
|
|
87
114
|
akan start <%= appName %>
|
|
88
115
|
```
|
|
116
|
+
|
|
117
|
+
### The Essential Loop: Edit → Scan → Check
|
|
118
|
+
|
|
119
|
+
Almost every Akan.js change follows this pattern. **Missing step 2 is the #1 cause of agent confusion.**
|
|
120
|
+
|
|
121
|
+
1. **Edit** — Create or modify source files in the correct location.
|
|
122
|
+
```
|
|
123
|
+
vi apps/<%= appName %>/lib/task/task.constant.ts # e.g., add a priority field
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
2. **Scan** — Regenerate barrel files so Akan discovers your change. This regenerates:
|
|
127
|
+
`cnst.ts`, `db.ts`, `srv.ts`, `sig.ts`, `st.ts`, `dict.ts`, `useClient.ts`, `useServer.ts`,
|
|
128
|
+
`ui/index.ts`, `webkit/index.ts`, `srvkit/index.ts`, `common/index.ts`, and all module `index.ts` files.
|
|
129
|
+
```
|
|
130
|
+
akan scan app <%= appName %>
|
|
131
|
+
```
|
|
132
|
+
**CRITICAL**: Scan after EVERY file add, delete, or rename. Without scan, other modules cannot
|
|
133
|
+
`import * as cnst from "../cnst"` and find your new model.
|
|
134
|
+
|
|
135
|
+
3. **Check** — Verify your change compiles and lints.
|
|
136
|
+
```
|
|
137
|
+
akan start <%= appName %> # dev server with live feedback (preferred)
|
|
138
|
+
akan lint <%= appName %> # quick lint-only check
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
If `akan scan` gives errors, try:
|
|
142
|
+
- `akan build <%= appName %>` — full rebuild catches type errors scan may miss
|
|
143
|
+
- Re-run `akan create-model <name> --app <%= appName %>` if the scaffold is corrupted
|
|
144
|
+
|
|
145
|
+
## Quick Decision Matrix — "Where do I put this code?"
|
|
146
|
+
|
|
147
|
+
| You want to... | Create in... | Run after... |
|
|
148
|
+
|----------------|-------------|--------------|
|
|
149
|
+
| Define a new database-backed noun (e.g., User, Product) | `lib/<model>/` → constant, document, service, signal, store, dictionary, abstract | `akan scan app <name>` |
|
|
150
|
+
| Add a pure workflow / integration (e.g., Payment, Email) | `lib/_<service>/` → service, signal, store, dictionary, abstract | `akan scan app <name>` |
|
|
151
|
+
| Add a reusable value type (e.g., Address, WorkHistory) | `lib/__scalar/<type>/` → constant, dictionary, abstract | `akan scan app <name>` |
|
|
152
|
+
| Create a new URL-visitable page | `page/` → `_index.tsx`, `_layout.tsx`, `[param]/_index.tsx` | Rebuild (akan start auto-detects) |
|
|
153
|
+
| Add a form or reusable UI component | `ui/` → PascalCase `.tsx` with `"use client"` if needed | `akan scan app <name>` |
|
|
154
|
+
| Add a React hook or browser helper | `webkit/` → camelCase `.ts` with `"use client"` | `akan scan app <name>` |
|
|
155
|
+
| Add a server-only guard, middleware, or adaptor | `srvkit/` → PascalCase `.ts` | `akan scan app <name>` |
|
|
156
|
+
| Add a pure helper (no DOM, no server API) | `common/` → camelCase `.ts` | `akan scan app <name>` |
|
|
157
|
+
|
|
158
|
+
## Anti-patterns: Never Do These
|
|
159
|
+
|
|
160
|
+
| Don't | Why | Do Instead |
|
|
161
|
+
|-------|-----|------------|
|
|
162
|
+
| Edit `cnst.ts`, `db.ts`, `srv.ts`, `sig.ts`, `st.ts`, `dict.ts`, `useClient.ts`, `useServer.ts`, or any `index.ts` | These are **generated by `akan scan`**. Your changes will be overwritten. | Edit the source files in `lib/<model>/` directories and run `akan scan` |
|
|
163
|
+
| Create a file without running scan | New files won't appear in barrel exports. Imports like `import * as cnst from "../cnst"` will fail. | Always run `akan scan app <name>` after creating, renaming, or deleting any module file |
|
|
164
|
+
| Use JS `#private` methods in service classes | Akan's build system rejects `#private`. Use TypeScript `private` keyword instead. | `private _methodName()` — never `#_methodName()` |
|
|
165
|
+
| Use `console.log()` | Biome lint forbids `console.log`. Only `console.error`, `console.info`, `console.warn` are allowed. | Use one of the three allowed console methods |
|
|
166
|
+
| Import server APIs (`fs`, `Bun`, `process.env`) in `ui/`, `webkit/`, or `common/` | Server-only imports in client code cause build failures. | Keep server dependencies in `lib/`, `srvkit/`, or `private/` only |
|
|
167
|
+
| Skip running `akan scan` after deleting a file | Deleted files remain referenced in barrel exports, causing import errors everywhere. | Run `akan scan app <name>` after every file add, remove, or rename |
|
|
168
|
+
| Use "use client" or `useState`/`useEffect` in pages/*.tsx, *.Unit.tsx, and *.View.tsx files | Server code cannot use React hooks. Wrap in a separate `"use client"` component. | Move hook logic to `webkit/` or a `"use client"` UI component |
|
|
169
|
+
| Use `<a>` tag for internal navigation between pages | Akan.js uses `<Link>` from `akanjs/ui` for client-side navigation — avoids full page reloads. | `import { Link } from "akanjs/ui"` and use `<Link href="/task">...</Link>` |
|
|
170
|
+
|
|
171
|
+
## Generated File Tracker (Quick Reference)
|
|
172
|
+
|
|
173
|
+
These files are regenerated by `akan scan` and overwritten on every scan. **Do not hand-edit them.**
|
|
174
|
+
|
|
175
|
+
| File | Generated From | Purpose |
|
|
176
|
+
|------|---------------|---------|
|
|
177
|
+
| `apps/*/lib/cnst.ts` | All `*/lib/*/**.constant.ts` | Barrel for all constants |
|
|
178
|
+
| `apps/*/lib/dict.ts` | All `*/lib/*/**.dictionary.ts` | Barrel for all dictionaries |
|
|
179
|
+
| `apps/*/lib/db.ts` | All `*/lib/<model>/*.document.ts` | Barrel for all document models |
|
|
180
|
+
| `apps/*/lib/srv.ts` | All `*/lib/**/**.service.ts` | Barrel for all services |
|
|
181
|
+
| `apps/*/lib/sig.ts` | All `*/lib/**/**.signal.ts` | Barrel for all signals |
|
|
182
|
+
| `apps/*/lib/st.ts` | All `*/lib/**/**.store.ts` | Barrel for all stores |
|
|
183
|
+
| `apps/*/lib/useClient.ts` | Client-safe module re-exports | Client-side import entry |
|
|
184
|
+
| `apps/*/lib/useServer.ts` | Server-only module re-exports | Server-side import entry |
|
|
185
|
+
| `apps/*/client.ts` | App-wide client barrel | The `fetch` and `st` instances |
|
|
186
|
+
| `apps/*/server.ts` | App-wide server barrel | Server-side service resolution |
|
|
187
|
+
| `*/lib/*/index.ts` | Per-module barrel | Module-level re-exports |
|
|
188
|
+
| `*/ui/index.ts` | All UI component files | UI barrel |
|
|
189
|
+
| `*/webkit/index.ts` | All webkit files | Webkit barrel |
|
|
190
|
+
| `*/srvkit/index.ts` | All srvkit files | Srvkit barrel |
|
|
191
|
+
| `*/common/index.ts` | All common files | Common barrel |
|
|
192
|
+
|
|
193
|
+
## Workflow Recipes
|
|
194
|
+
|
|
195
|
+
Concrete step-by-step recipes for the most frequent Akan.js changes. Each recipe shows which files to edit
|
|
196
|
+
and in what order. The code examples reference the `task` module in `apps/<%= appName %>/lib/task/` as a
|
|
197
|
+
template; replace `task` with your model name and `Task` with your PascalCase model name.
|
|
198
|
+
|
|
199
|
+
When editing a file, always read the existing content first. Only change the relevant sections — do not
|
|
200
|
+
rewrite the entire file.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
### Recipe 1: Adding a New Field to a Model
|
|
205
|
+
|
|
206
|
+
**Files to edit (in order):** `constant.ts` → `dictionary.ts` → `Template.tsx` → `Unit.tsx` → `akan scan`
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
// 1. apps/<app>/lib/<model>/<model>.constant.ts
|
|
210
|
+
// Add field to the Input class. Use field() builder with optional defaults
|
|
211
|
+
export class TaskInput extends via((field) => ({
|
|
212
|
+
title: field(String),
|
|
213
|
+
priority: field(TaskPriority, { default: "medium" }), // NEW FIELD
|
|
214
|
+
})) {}
|
|
215
|
+
|
|
216
|
+
// If the new field should appear in list views, also add it to LightTask:
|
|
217
|
+
export class LightTask extends via(TaskObject, ["title", "priority", "status", "due"] as const, () => ({})) {}
|
|
218
|
+
|
|
219
|
+
// 2. apps/<app>/lib/<model>/<model>.dictionary.ts
|
|
220
|
+
// Add i18n labels for the new field (and its enum values if any)
|
|
221
|
+
.model<Task>((t) => ({
|
|
222
|
+
priority: t(["Priority", "우선순위"]),
|
|
223
|
+
}))
|
|
224
|
+
.enum<TaskPriority>("taskPriority", (t) => ({
|
|
225
|
+
low: t(["Low", "낮음"]),
|
|
226
|
+
medium: t(["Medium", "보통"]),
|
|
227
|
+
high: t(["High", "높음"]),
|
|
228
|
+
}))
|
|
229
|
+
|
|
230
|
+
// 3. apps/<app>/lib/<model>/<Model>.Template.tsx
|
|
231
|
+
// Add a form field using st.do.setXxxOnYyy (auto-generated setter)
|
|
232
|
+
const form = st.use.taskForm();
|
|
233
|
+
<Field.ToggleSelect
|
|
234
|
+
label={l("task.priority")}
|
|
235
|
+
items={cnst.TaskPriority}
|
|
236
|
+
value={form.priority}
|
|
237
|
+
onChange={st.do.setPriorityOnTask}
|
|
238
|
+
/>
|
|
239
|
+
|
|
240
|
+
// 4. apps/<app>/lib/<model>/<Model>.Unit.tsx
|
|
241
|
+
// Display the new field in card/list views
|
|
242
|
+
<span className={clsx("badge badge-sm", {
|
|
243
|
+
"badge-error": task.priority === "high",
|
|
244
|
+
"badge-warning": task.priority === "medium",
|
|
245
|
+
"badge-ghost": task.priority === "low",
|
|
246
|
+
})}>{task.priority}</span>
|
|
247
|
+
|
|
248
|
+
// 5. Regenerate barrels
|
|
249
|
+
// akan scan app <name>
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
### Recipe 2: Injecting a Dependency into a Service
|
|
255
|
+
|
|
256
|
+
Two patterns: injecting an **adapter** (external client) or injecting another **module's service**.
|
|
257
|
+
|
|
258
|
+
**A. Adapter injection via `use<>()` (for external clients / global singletons)**
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
// 1. Create the adapter class in apps/<app>/srvkit/
|
|
262
|
+
// apps/<app>/srvkit/EmailClient.ts
|
|
263
|
+
export class EmailClient {
|
|
264
|
+
constructor(readonly apiKey: string) {}
|
|
265
|
+
async send(opts: { to: string; subject: string; body: string }) { /* ... */ }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// 2. Register in apps/<app>/lib/option.ts
|
|
269
|
+
export const option = new AkanOption()
|
|
270
|
+
.use((options) => ({
|
|
271
|
+
emailClient: new EmailClient(options.mailerApiKey),
|
|
272
|
+
}));
|
|
273
|
+
|
|
274
|
+
// 3. Inject via use<>() in <model>.service.ts
|
|
275
|
+
export class TaskService extends serve(db.task, ({ use }) => ({
|
|
276
|
+
emailClient: use<EmailClient>(),
|
|
277
|
+
})) {
|
|
278
|
+
async _postCreate(task: cnst.Task) {
|
|
279
|
+
await this.emailClient.send({ to: "...", subject: "Task Created", body: `Task "${task.title}" created.` });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**B. Cross-module service injection via `service<>()` (for other Akan services)**
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
// In <model>.service.ts — inject another module's service
|
|
288
|
+
import * as srv from "../srv";
|
|
289
|
+
|
|
290
|
+
export class TaskService extends serve(db.task, ({ service }) => ({
|
|
291
|
+
notiService: service<srv.NotiService>(),
|
|
292
|
+
})) {
|
|
293
|
+
async _postCreate(task: cnst.Task) {
|
|
294
|
+
await this.notiService.send("info", `Task "${task.title}" created`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
### Recipe 3: Creating and Using a Slice
|
|
302
|
+
|
|
303
|
+
A Slice is a named, filtered data view. Add file entries and connect from a page.
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
// 1. apps/<app>/lib/<model>/<model>.signal.ts — Define the slice
|
|
307
|
+
export class TaskSlice extends slice(srv.task, (init) => ({
|
|
308
|
+
inTodo: init()
|
|
309
|
+
.search("statuses", [cnst.TaskStatus])
|
|
310
|
+
.exec(function (statuses?) {
|
|
311
|
+
return this.taskService.queryByStatuses(statuses ?? ["todo", "inProgress"]);
|
|
312
|
+
}),
|
|
313
|
+
})) {}
|
|
314
|
+
|
|
315
|
+
// 2. apps/<app>/lib/<model>/<model>.document.ts — Add query filter for slice
|
|
316
|
+
export class TaskFilter extends from(cnst.Task, (filter) => ({
|
|
317
|
+
query: {
|
|
318
|
+
byStatuses: filter()
|
|
319
|
+
.arg("statuses", [cnst.TaskStatus])
|
|
320
|
+
.query((statuses) => ({ status: { $in: statuses } })),
|
|
321
|
+
},
|
|
322
|
+
})) {}
|
|
323
|
+
|
|
324
|
+
// 3. apps/<app>/lib/<model>/<model>.dictionary.ts — Slice labels
|
|
325
|
+
.slice<TaskSlice>((fn) => ({
|
|
326
|
+
inTodo: fn(["Tasks In Todo", "할 일"]).arg((t) => ({
|
|
327
|
+
statuses: t(["Statuses", "상태"]),
|
|
328
|
+
})),
|
|
329
|
+
}))
|
|
330
|
+
|
|
331
|
+
// 4. In page — Init slice from loader, render with Zone
|
|
332
|
+
loader={async () => {
|
|
333
|
+
const { taskInitInTodo } = await fetch.initTaskInTodo();
|
|
334
|
+
return { taskInitInTodo };
|
|
335
|
+
}}
|
|
336
|
+
render={({ data: { taskInitInTodo } }) => (
|
|
337
|
+
<Task.Zone.Card init={taskInitInTodo} sliceName="taskInTodo" />
|
|
338
|
+
)}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
The slice name in code uses camelCase (`inTodo`). In dictionary and components it becomes `"taskInTodo"`.
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
### Recipe 4: Creating a Mutation Endpoint (with Status Workflow)
|
|
346
|
+
|
|
347
|
+
**Files to edit (in order):** `document.ts` → `service.ts` → `signal.ts` → `dictionary.ts` → `store.ts` → `Util.tsx`
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
// 1. <model>.document.ts — Document chain method with state validation
|
|
351
|
+
export class TaskDocument extends by(cnst.Task) {
|
|
352
|
+
start() {
|
|
353
|
+
if (this.status !== "todo") throw new Err("task.error.cannotStartFromNonTodo");
|
|
354
|
+
this.status = "inProgress";
|
|
355
|
+
return this; // Return this for chaining: task.start().save()
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// 2. <model>.service.ts — Service method wrapping document
|
|
360
|
+
async startTask(taskId: string) {
|
|
361
|
+
const task = await this.getTask(taskId);
|
|
362
|
+
return task.start().save();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// 3. <model>.signal.ts — Mutation endpoint
|
|
366
|
+
export class TaskEndpoint extends endpoint(srv.task, ({ mutation }) => ({
|
|
367
|
+
startTask: mutation(cnst.Task)
|
|
368
|
+
.param("taskId", String)
|
|
369
|
+
.exec(async function (taskId) {
|
|
370
|
+
return await this.taskService.startTask(taskId);
|
|
371
|
+
}),
|
|
372
|
+
})) {}
|
|
373
|
+
|
|
374
|
+
// 4. <model>.dictionary.ts — Endpoint + error labels
|
|
375
|
+
.endpoint<TaskEndpoint>((fn) => ({
|
|
376
|
+
startTask: fn(["Start Task", "작업시작"])
|
|
377
|
+
.arg((t) => ({ taskId: t(["Task ID", "할 일 ID"]) })),
|
|
378
|
+
}))
|
|
379
|
+
.error({
|
|
380
|
+
cannotStartFromNonTodo: ["Task can only start from todo status", "할 일 상태에서만 시작 가능"],
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
// 5. <model>.store.ts — Client-side action with toast feedback
|
|
384
|
+
async startTask(taskId: string) {
|
|
385
|
+
msg.loading("task.startTaskLoading", { key: "startTask" });
|
|
386
|
+
const task = await fetch.startTask(taskId);
|
|
387
|
+
this.setTask(task); // Auto-generated: updates task state in store
|
|
388
|
+
msg.success("task.startTaskSuccess", { key: "startTask" });
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// 6. <Model>.Util.tsx — Reusable button component
|
|
392
|
+
export const Start = ({ taskId }: { taskId: string }) => (
|
|
393
|
+
<button className="btn btn-xs btn-primary" onClick={() => st.do.startTask(taskId)}>
|
|
394
|
+
Start
|
|
395
|
+
</button>
|
|
396
|
+
);
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
### Recipe 5: Internal Triggers — Interval & Cron
|
|
402
|
+
|
|
403
|
+
Server-side background jobs. Defined in `internal()` signal, implemented in service.
|
|
404
|
+
|
|
405
|
+
```typescript
|
|
406
|
+
// 1. <model>.signal.ts — Define triggers
|
|
407
|
+
export class TaskInternal extends internal(srv.task, ({ interval, cron }) => ({
|
|
408
|
+
cleanupStaleTasks: interval(10000).exec(async function () {
|
|
409
|
+
await this.taskService.cleanupStaleTasks();
|
|
410
|
+
}),
|
|
411
|
+
|
|
412
|
+
dailyDigest: cron("0 0 * * *").exec(async function () {
|
|
413
|
+
await this.taskService.sendDailyDigest();
|
|
414
|
+
}),
|
|
415
|
+
})) {}
|
|
416
|
+
|
|
417
|
+
// 2. <model>.service.ts — Implement the logic
|
|
418
|
+
async cleanupStaleTasks() {
|
|
419
|
+
const weekAgo = dayjs().subtract(7, "day").toDate();
|
|
420
|
+
const stale = await this.taskModel.listDueBefore(weekAgo);
|
|
421
|
+
for (const task of stale) {
|
|
422
|
+
await task.remove();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
**Available trigger types:**
|
|
428
|
+
- `interval(ms)` — runs repeatedly at the given interval
|
|
429
|
+
- `cron("min hour dom month dow")` — runs on a schedule
|
|
430
|
+
- `initialize()` — runs once on service startup
|
|
431
|
+
- `process(Return).msg(Type)` — message queue consumer
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
### Recipe 6: Creating an Insight (Aggregation / Dashboard Stats)
|
|
436
|
+
|
|
437
|
+
Insights display aggregated statistics across model data.
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
440
|
+
// 1. <model>.constant.ts — Insight class with accumulate rules
|
|
441
|
+
export class TaskInsight extends via(Task, (field) => ({
|
|
442
|
+
totalCount: field(Int, { default: 0, accumulate: {} }),
|
|
443
|
+
completedCount: field(Int, { default: 0, accumulate: { status: "completed" } }),
|
|
444
|
+
})) {}
|
|
445
|
+
|
|
446
|
+
// 2. <model>.dictionary.ts — Insight field labels
|
|
447
|
+
.insight<TaskInsight>((t) => ({
|
|
448
|
+
totalCount: t(["Total Tasks", "전체 할 일"]),
|
|
449
|
+
completedCount: t(["Completed", "완료됨"]),
|
|
450
|
+
}))
|
|
451
|
+
|
|
452
|
+
// 3. <Model>.View.tsx — Display component consuming an Insight model
|
|
453
|
+
export const Stats = ({ taskInsight }: { taskInsight: cnst.TaskInsight }) => (
|
|
454
|
+
<div className="grid grid-cols-2 gap-4">
|
|
455
|
+
<div className="stat rounded-lg border border-base-content/10 bg-base-100 p-4">
|
|
456
|
+
<div className="stat-title text-base-content/60">{l("task.totalCount")}</div>
|
|
457
|
+
<div className="stat-value text-primary">{taskInsight.totalCount}</div>
|
|
458
|
+
</div>
|
|
459
|
+
<div className="stat rounded-lg border border-base-content/10 bg-base-100 p-4">
|
|
460
|
+
<div className="stat-title text-base-content/60">{l("task.completedCount")}</div>
|
|
461
|
+
<div className="stat-value text-success">{taskInsight.completedCount}</div>
|
|
462
|
+
</div>
|
|
463
|
+
</div>
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
// 4. <Model>.Zone.tsx — Mount in page via Load.Insight bound to a slice
|
|
467
|
+
export const Insight = ({ sliceName }: { sliceName: string }) => {
|
|
468
|
+
const insight = st.slice[sliceName].use.taskInsight();
|
|
469
|
+
if (!insight) return null;
|
|
470
|
+
return <Task.View.Stats taskInsight={insight} />;
|
|
471
|
+
};
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
---
|
|
475
|
+
|
|
476
|
+
### Data Flow Summary
|
|
477
|
+
|
|
478
|
+
For each business question, follow this chain:
|
|
479
|
+
|
|
480
|
+
| Question | File Pattern |
|
|
481
|
+
|----------|-------------|
|
|
482
|
+
| What fields does it have? | `constant.ts` — `via()` layers: Input → Object → Light → Model → Insight |
|
|
483
|
+
| How is it stored/searched? | `document.ts` — `from()` filters + `by()` document methods + `into()` model |
|
|
484
|
+
| What business rule should run? | `service.ts` — `serve()` with DI (`use`, `service`, `plug`, `env`, `memory`) |
|
|
485
|
+
| What should a page call? | `signal.ts` — `internal()` (jobs), `endpoint()` (APIs), `slice()` (data views) |
|
|
486
|
+
| What client state is shared? | `store.ts` — `store()` with auto-generated form/insight state + custom actions |
|
|
487
|
+
| What should users see? | `View.tsx` + `Zone.tsx` (detail/container), `Template.tsx` (forms), `Unit.tsx` (cards), `Util.tsx` (buttons) |
|
|
488
|
+
|
|
489
|
+
## Auto-Generated API Reference
|
|
490
|
+
|
|
491
|
+
akan scan automatically generates APIs across all layers. Only write custom logic — never hand-write what the framework generates.
|
|
492
|
+
|
|
493
|
+
### Signal — Endpoint Auto-Generation
|
|
494
|
+
|
|
495
|
+
| Auto-Generated | Signature | Description |
|
|
496
|
+
|---------------|-----------|-------------|
|
|
497
|
+
| `view[Model](id)` | `fetch.viewTask(id)` | Fetch single model for detail view |
|
|
498
|
+
| `edit[Model](id)` | `fetch.editTask(id)` | Fetch model for edit view |
|
|
499
|
+
| `merge[Model](id, data)` | `fetch.mergeTask(data)` | Create (no id) or update (with id) model |
|
|
500
|
+
| `[model]List[Suffix](args, skip, limit, sort)` | `fetch.taskListInTodo(args)` | Paginated list from slice filter |
|
|
501
|
+
| `[model]Insight[Suffix](args)` | `fetch.taskInsightInTodo(args)` | Aggregated insight from slice query |
|
|
502
|
+
| `init[Model][Suffix](args)` | `fetch.initTaskInTodo(args)` | Initialize slice with list + insight |
|
|
503
|
+
|
|
504
|
+
**Rule**: Only define `query()`, `mutation()`, `message()`, `pubsub()` endpoints manually when the endpoint needs custom business logic. Standard CRUD is already auto-generated.
|
|
505
|
+
|
|
506
|
+
### Service — CRUD & Query Auto-Generation
|
|
507
|
+
|
|
508
|
+
| Auto-Generated | Description |
|
|
509
|
+
|---------------|-------------|
|
|
510
|
+
| `this.<model>Model` | Auto-injected model adaptor |
|
|
511
|
+
| `get<Model>(id)`, `load<Model>(id)` | Single document lookup |
|
|
512
|
+
| `createModel(data)`, `updateModel(id, data)`, `removeModel(id)` | CRUD operations |
|
|
513
|
+
| `list<Query>(args)`, `find<Query>(args)`, `pick<Query>(args)` | Filter-based queries |
|
|
514
|
+
| `exists<Query>(args)`, `count<Query>(args)`, `insight<Query>(args)` | Filter-based helpers |
|
|
515
|
+
| `_preCreate`, `_postCreate`, `_preUpdate`, `_postUpdate`, `_preRemove`, `_postRemove` | Lifecycle hooks (override to add logic) |
|
|
516
|
+
|
|
517
|
+
**Rule**: Use `_preCreate`/`_postCreate` lifecycle hooks for side effects (e.g., push workHistory entries). Write custom service methods only for multi-model orchestration.
|
|
518
|
+
|
|
519
|
+
### Store — State & Action Auto-Generation
|
|
520
|
+
|
|
521
|
+
| Auto-Generated | Description |
|
|
522
|
+
|---------------|-------------|
|
|
523
|
+
| `[model]` (cached full model), `[model]Loading`, `[model]Form`, `[model]Modal` | Base model states |
|
|
524
|
+
| `create[Model](data)`, `update[Model](id, data)`, `remove[Model](id)` | CRUD actions |
|
|
525
|
+
| `new[Model](partial)`, `edit[Model](model)`, `view[Model](model)` | Form/view state actions |
|
|
526
|
+
| `[slice]List`, `[slice]InitList`, `[slice]Insight`, `[slice]Selection` | Slice states |
|
|
527
|
+
| `init[Slice](args)`, `refresh[Slice]()`, `setPageOf[Slice](page)` | Slice actions |
|
|
528
|
+
| `set[Field]On[Model](value)` | Auto-setters for each model field |
|
|
529
|
+
|
|
530
|
+
**Rule**: Write custom store actions only for toast messages (`msg.loading`/`msg.success`) or multi-step workflows. State fields and CRUD actions are already auto-generated.
|
|
531
|
+
|
|
532
|
+
### Document — Filter Query Auto-Generation
|
|
533
|
+
|
|
534
|
+
| Auto-Generated (from Filter definition) | Description |
|
|
535
|
+
|----------------------------------------|-------------|
|
|
536
|
+
| `list[Query](args)`, `listIds[Query](args)` | List documents matching filter |
|
|
537
|
+
| `find[Query](args)`, `findId[Query](args)` | Find one (null if not found) |
|
|
538
|
+
| `pick[Query](args)`, `pickId[Query](args)` | Find one (throw if not found) |
|
|
539
|
+
| `exists[Query](args)`, `count[Query](args)` | Existence check and count |
|
|
540
|
+
| `insight[Query](args)`, `query[Query](args)` | Insight and raw query |
|
|
541
|
+
|
|
542
|
+
**Rule**: Define `Filter` with `.query()` conditions in `document.ts`. akan scan auto-generates all 10 query helper methods per filter. Write `Document` chain methods only for state transitions with validation.
|