@live-change/frontend-template 0.9.201 → 0.9.204
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/.claude/rules/live-change-backend-actions-views-triggers.md +49 -1
- package/.claude/rules/live-change-backend-architecture.md +5 -5
- package/.claude/rules/live-change-backend-event-sourcing.md +2 -2
- package/.claude/rules/live-change-backend-models-and-relations.md +65 -8
- package/.claude/rules/live-change-frontend-e2e-lifecycle.md +42 -0
- package/.claude/rules/live-change-frontend-i18n-locales.md +25 -0
- package/.claude/rules/live-change-frontend-vue-primevue.md +100 -4
- package/.claude/rules/live-change-node-toolchain-fnm.md +39 -0
- package/.claude/rules/live-change-service-structure.md +2 -2
- package/.claude/settings.json +3 -1
- package/.claude/skills/create-skills-and-rules/SKILL.md +23 -0
- package/.claude/skills/live-change-backend-change-triggers/SKILL.md +15 -0
- package/.claude/skills/live-change-dao-protocol/SKILL.md +46 -0
- package/.claude/skills/live-change-design-actions-views-triggers/SKILL.md +116 -0
- package/.claude/skills/live-change-design-models-relations/SKILL.md +63 -5
- package/.claude/skills/live-change-frontend-command-forms/SKILL.md +6 -5
- package/.claude/skills/live-change-frontend-data-views/SKILL.md +75 -4
- package/.claude/skills/live-change-frontend-e2e-lifecycle/SKILL.md +82 -0
- package/.claude/skills/live-change-frontend-range-list/SKILL.md +90 -0
- package/.claude/skills/live-change-frontend-synchronized/SKILL.md +101 -0
- package/.claude/skills/live-change-node-toolchain-fnm/SKILL.md +44 -0
- package/.cursor/rules/live-change-backend-actions-views-triggers.mdc +58 -4
- package/.cursor/rules/live-change-backend-architecture.mdc +1 -1
- package/.cursor/rules/live-change-backend-event-sourcing.mdc +1 -1
- package/.cursor/rules/live-change-backend-models-and-relations.mdc +69 -7
- package/.cursor/rules/live-change-backend-views-vs-triggers-for-reads-writes.mdc +28 -0
- package/.cursor/rules/live-change-dao-protocol.mdc +47 -0
- package/.cursor/rules/live-change-frontend-e2e-lifecycle.mdc +15 -0
- package/.cursor/rules/live-change-frontend-i18n-locales.mdc +26 -0
- package/.cursor/rules/live-change-frontend-views-not-commands-for-reads.mdc +30 -0
- package/.cursor/rules/live-change-frontend-vue-primevue.mdc +91 -0
- package/.cursor/rules/live-change-node-toolchain-fnm.mdc +40 -0
- package/.cursor/rules/live-change-service-structure.mdc +1 -1
- package/.cursor/skills/create-skills-and-rules.md +23 -0
- package/.cursor/skills/live-change-backend-change-triggers.md +15 -0
- package/.cursor/skills/live-change-design-actions-views-triggers.md +57 -0
- package/.cursor/skills/live-change-design-models-relations.md +23 -5
- package/.cursor/skills/live-change-frontend-command-forms.md +6 -5
- package/.cursor/skills/live-change-frontend-data-views.md +17 -0
- package/.cursor/skills/live-change-frontend-e2e-lifecycle.md +82 -0
- package/.cursor/skills/live-change-frontend-range-list.md +45 -0
- package/.cursor/skills/live-change-frontend-synchronized.md +101 -0
- package/.cursor/skills/live-change-node-toolchain-fnm.md +44 -0
- package/.node-version +1 -0
- package/.nvmrc +1 -0
- package/README.md +18 -0
- package/e2e/client-session.test.ts +17 -0
- package/e2e/e2eSuite.ts +12 -0
- package/e2e/env.ts +130 -0
- package/e2e/homepage.test.ts +17 -0
- package/e2e/steps.ts +3 -0
- package/e2e/withBrowser.ts +18 -0
- package/opencode.json +4 -1
- package/package.json +55 -53
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Rules for implementing actions, views, and triggers in LiveChange services
|
|
3
|
-
globs: **/services/**/*.js
|
|
3
|
+
globs: **/services/**/*.js, **/server/**/*.js, server/**/*.js
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# LiveChange backend – actions, views, triggers (Claude Code)
|
|
@@ -52,6 +52,48 @@ definition.action({
|
|
|
52
52
|
- Views should be simple query endpoints over models.
|
|
53
53
|
- Prefer `indexObjectGet` / `indexRangeGet` instead of scanning whole tables.
|
|
54
54
|
|
|
55
|
+
### Range view guardrails (for RangeViewer/rangeBuckets consumers)
|
|
56
|
+
|
|
57
|
+
- For index-backed paginated lists, prefer `Model.sortedIndexRangePath(indexName, keyPrefix, App.extractRange(props))`.
|
|
58
|
+
- Do not use `indexRangePath` semantics for views consumed by bucket-based range UI.
|
|
59
|
+
- Keep `gt/gte/lt/lte` as cursor pagination fields, not domain filter fields.
|
|
60
|
+
- If you need filtering by month/year/status, design index prefix for it first.
|
|
61
|
+
- Use `App.utils.prefixRange` as backend fallback only when index redesign is not feasible.
|
|
62
|
+
|
|
63
|
+
### Standalone index guardrail
|
|
64
|
+
|
|
65
|
+
- If an index combines peer data streams (union of multiple tables), define it as service-level `definition.index(...)` (prefer separate `indexes.js`), not as `model.indexes` inside one arbitrary model.
|
|
66
|
+
- Use model `indexes` only when one model is the clear owner of index semantics.
|
|
67
|
+
|
|
68
|
+
### Index function serialization constraint
|
|
69
|
+
|
|
70
|
+
Index functions (`definition.index({ function })` and model-level `indexes: { name: { function } }`) are **serialized via `toString()`** and executed on a remote server. They **cannot reference anything outside their own function body** — no outer variables, no imported functions, no module-scope helpers.
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
// ❌ BROKEN — helper is outside the function, undefined at runtime
|
|
74
|
+
function mapRow(obj) { return { id: obj.name + '_' + obj.id, to: obj.id } }
|
|
75
|
+
|
|
76
|
+
definition.index({
|
|
77
|
+
name: 'myIndex',
|
|
78
|
+
function: async (input, output, { tableName }) => {
|
|
79
|
+
const table = await input.table(tableName)
|
|
80
|
+
await table.map(mapRow).to(output) // mapRow is undefined!
|
|
81
|
+
},
|
|
82
|
+
parameters: { tableName: definition.name + '_MyModel' }
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// ✅ CORRECT — helper is inside the function body
|
|
86
|
+
definition.index({
|
|
87
|
+
name: 'myIndex',
|
|
88
|
+
function: async (input, output, { tableName }) => {
|
|
89
|
+
const mapRow = obj => ({ id: obj.name + '_' + obj.id, to: obj.id })
|
|
90
|
+
const table = await input.table(tableName)
|
|
91
|
+
await table.map(mapRow).to(output)
|
|
92
|
+
},
|
|
93
|
+
parameters: { tableName: definition.name + '_MyModel' }
|
|
94
|
+
})
|
|
95
|
+
```
|
|
96
|
+
|
|
55
97
|
Example of a range view:
|
|
56
98
|
|
|
57
99
|
```js
|
|
@@ -159,6 +201,12 @@ definition.trigger({
|
|
|
159
201
|
|
|
160
202
|
Check `data`/`oldData`: both present = update, only `data` = create, only `oldData` = delete.
|
|
161
203
|
|
|
204
|
+
## Cron-service — schedules, intervals, and admin UI
|
|
205
|
+
|
|
206
|
+
- For **cron-like** or **repeating-interval** execution of a **trigger**, use **`@live-change/cron-service`** (**Schedule** / **Interval**) plus **task-service** triggers — do not sketch “only a timer” without considering cron models and **`changeCron_Schedule`** / **`changeCron_Interval`** timer lifecycle.
|
|
207
|
+
- Reference admin flow (see **task-frontend**): **`setSchedule`** / **`setInterval`** via **`ActionForm`**, lists via **`path.cron.schedules`** / **`path.cron.intervals`**, enrich rows with **`.with()`** for **`scheduleInfo`** / **`intervalInfo`**, **`runState`** (`jobType` **`cron_Schedule`** or **`cron_Interval`**), and **`task.tasksByCauseAndCreatedAt`**; delete with **`deleteSchedule`** / **`deleteInterval`**.
|
|
208
|
+
- **Schedule** time fields (**minute**, **hour**, **day**, **dayOfWeek**, **month**): use **`NaN`** for “every” at that granularity; see **`15-cron-and-intervals.md`** (section **API used by task-frontend**).
|
|
209
|
+
|
|
162
210
|
## Granting access on object creation
|
|
163
211
|
|
|
164
212
|
When a model uses `entity` with `writeAccessControl` / `readAccessControl`, the auto-generated CRUD checks roles but does **not** grant them automatically. The creator must be explicitly granted roles — typically `'owner'` — otherwise they cannot access their own object.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Rules for LiveChange backend service architecture and directory structure
|
|
3
|
-
globs: **/services/**/*.js
|
|
3
|
+
globs: **/services/**/*.js, **/server/**/*.js, server/**/*.js
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# LiveChange backend – service architecture (Claude Code)
|
|
@@ -104,17 +104,17 @@ services: [
|
|
|
104
104
|
|
|
105
105
|
## Inspecting services with `describe`
|
|
106
106
|
|
|
107
|
-
Use the `describe` CLI command to see what the framework generated from your definitions (models, views, actions, triggers, indexes, events)
|
|
107
|
+
Use the `describe` CLI command to see what the framework generated from your definitions (models, views, actions, triggers, indexes, events). From the app root (directory with `.node-version` / `.nvmrc`), run Node via **fnm** so the toolchain matches the project — see rule `live-change-node-toolchain-fnm`.
|
|
108
108
|
|
|
109
109
|
```bash
|
|
110
110
|
# All services overview
|
|
111
|
-
node server/start.js describe
|
|
111
|
+
fnm exec -- node server/start.js describe
|
|
112
112
|
|
|
113
113
|
# One service in YAML (shows all generated code)
|
|
114
|
-
node server/start.js describe --service myService --output yaml
|
|
114
|
+
fnm exec -- node server/start.js describe --service myService --output yaml
|
|
115
115
|
|
|
116
116
|
# Specific entity
|
|
117
|
-
node server/start.js describe --service myService --model MyModel --output yaml
|
|
117
|
+
fnm exec -- node server/start.js describe --service myService --model MyModel --output yaml
|
|
118
118
|
```
|
|
119
119
|
|
|
120
120
|
This is especially useful after using relations (`userItem`, `itemOf`, `propertyOf`) — `describe` shows all the auto-generated views, actions, triggers, and indexes.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Event-sourcing data flow rules — emit events for DB writes, use triggerService for cross-service writes
|
|
3
|
-
globs: **/services/**/*.js
|
|
3
|
+
globs: **/services/**/*.js, **/server/**/*.js, server/**/*.js
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# LiveChange backend – event-sourcing data flow (Claude Code)
|
|
@@ -155,7 +155,7 @@ Without `waitForEvents: true`, the action returns immediately and events are pro
|
|
|
155
155
|
When a model uses relations (`userItem`, `itemOf`, `propertyOf`, etc.), the relations plugin auto-generates CRUD triggers. Use `describe` to discover them:
|
|
156
156
|
|
|
157
157
|
```bash
|
|
158
|
-
node server/start.js describe --service myService --output yaml
|
|
158
|
+
fnm exec -- node server/start.js describe --service myService --output yaml
|
|
159
159
|
```
|
|
160
160
|
|
|
161
161
|
Common auto-generated trigger patterns:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Rules for defining models, relations, indexes and access control in LiveChange
|
|
3
|
-
globs: **/services/**/*.js
|
|
3
|
+
globs: **/services/**/*.js, **/server/**/*.js, server/**/*.js
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# LiveChange backend – models and relations (Claude Code)
|
|
@@ -32,6 +32,25 @@ properties: {
|
|
|
32
32
|
}
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
## Relation arity (critical)
|
|
36
|
+
|
|
37
|
+
Always separate:
|
|
38
|
+
|
|
39
|
+
- **annotation arity** — can the annotation be a list of configs
|
|
40
|
+
- **parent tuple arity** — can one config include multiple parents/dimensions
|
|
41
|
+
|
|
42
|
+
| Relation | Annotation arity | Parent tuple arity |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `propertyOf`, `itemOf`, `boundTo` | single config only | `what` can be one model or `[A, B, ...]` |
|
|
45
|
+
| `relatedTo` | single config or config list | each config uses `what` with one model or `[A, B, ...]` |
|
|
46
|
+
| `propertyOfAny`, `itemOfAny`, `boundToAny` | single config only | `to` can contain one or many names |
|
|
47
|
+
| `relatedToAny` | single config or config list | each config uses `to` with one or many names |
|
|
48
|
+
|
|
49
|
+
Guardrail:
|
|
50
|
+
|
|
51
|
+
- valid: `propertyOf: { what: [A, B] }`
|
|
52
|
+
- invalid: `propertyOf: [configA, configB]`
|
|
53
|
+
|
|
35
54
|
## `userItem` – belongs to the signed-in user
|
|
36
55
|
|
|
37
56
|
Use when the model is owned by the currently signed-in user.
|
|
@@ -113,7 +132,7 @@ Effects:
|
|
|
113
132
|
## `propertyOf` with multiple parents (1:1 link to each)
|
|
114
133
|
|
|
115
134
|
Sometimes a model is a dedicated 1:1 link between entities (for example: invoice ↔ contractor in a specific role).
|
|
116
|
-
Most commonly this is 1–2 parents, but `
|
|
135
|
+
Most commonly this is 1–2 parents, but `what` can point to **any number** of parent models (including 3+), if that matches the domain semantics.
|
|
117
136
|
|
|
118
137
|
In that case:
|
|
119
138
|
|
|
@@ -132,10 +151,9 @@ definition.model({
|
|
|
132
151
|
properties: {
|
|
133
152
|
// optional extra fields
|
|
134
153
|
},
|
|
135
|
-
propertyOf:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
]
|
|
154
|
+
propertyOf: {
|
|
155
|
+
what: [CostInvoice, Contractor]
|
|
156
|
+
}
|
|
139
157
|
})
|
|
140
158
|
```
|
|
141
159
|
|
|
@@ -180,7 +198,7 @@ Relations automatically add **identifier fields** and **indexes** to the model.
|
|
|
180
198
|
| `propertyOfAny: { to: ['owner'] }` | `ownerType`, `owner` | `byOwner` (hash) |
|
|
181
199
|
| `boundTo: { what: Device }` | `device` | `byDevice` (hash) |
|
|
182
200
|
|
|
183
|
-
For multi-parent relations (e.g. `propertyOf:
|
|
201
|
+
For multi-parent relations (e.g. `propertyOf: { what: [A, B] }`), all index combinations are created (`byA`, `byB`, `byAAndB`).
|
|
184
202
|
|
|
185
203
|
```js
|
|
186
204
|
// ✅ Correct — only define YOUR fields
|
|
@@ -203,7 +221,7 @@ definition.model({
|
|
|
203
221
|
})
|
|
204
222
|
```
|
|
205
223
|
|
|
206
|
-
Use `node server/start.js describe --service myService --model MyModel --output yaml` to see all fields including auto-added ones.
|
|
224
|
+
Use `fnm exec -- node server/start.js describe --service myService --model MyModel --output yaml` (from the app root, with fnm — see `live-change-node-toolchain-fnm`) to see all fields including auto-added ones.
|
|
207
225
|
|
|
208
226
|
## Indexes
|
|
209
227
|
|
|
@@ -223,6 +241,45 @@ indexes: {
|
|
|
223
241
|
|
|
224
242
|
In other services, the same index may be visible with a prefixed name such as `myService_Model_byDeviceAndStatus`.
|
|
225
243
|
|
|
244
|
+
### When index should be outside a model
|
|
245
|
+
|
|
246
|
+
If an index is a union/projection over multiple peer tables (no single natural owner model), do not force it into `model.indexes`.
|
|
247
|
+
|
|
248
|
+
- Use service-level `definition.index(...)` (prefer a dedicated `indexes.js` file).
|
|
249
|
+
- Keep `model.indexes` for indexes semantically owned by one model.
|
|
250
|
+
|
|
251
|
+
### Function index serialization constraint
|
|
252
|
+
|
|
253
|
+
Function indexes (both `definition.index({ function })` and model `indexes: { name: { function } }`) are **serialized via `toString()`** and run on a remote server. All helpers, mappers, and variables used inside the function **must be defined inside the function body**. References to outer scope (module-level functions, imports, closures) will be `undefined` at runtime.
|
|
254
|
+
|
|
255
|
+
```js
|
|
256
|
+
// ❌ BROKEN — mapper defined outside, invisible after serialization
|
|
257
|
+
const mapper = obj => ({
|
|
258
|
+
id: obj.name + '_' + obj.id, to: obj.id
|
|
259
|
+
})
|
|
260
|
+
indexes: {
|
|
261
|
+
byDerived: {
|
|
262
|
+
function: async (input, output, { tableName }) => {
|
|
263
|
+
const table = await input.table(tableName)
|
|
264
|
+
await table.map(mapper).to(output) // mapper is undefined!
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ✅ CORRECT — mapper defined inside the function
|
|
270
|
+
indexes: {
|
|
271
|
+
byDerived: {
|
|
272
|
+
function: async (input, output, { tableName }) => {
|
|
273
|
+
const mapper = obj => ({
|
|
274
|
+
id: obj.name + '_' + obj.id, to: obj.id
|
|
275
|
+
})
|
|
276
|
+
const table = await input.table(tableName)
|
|
277
|
+
await table.map(mapper).to(output)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
226
283
|
## Access control on relations
|
|
227
284
|
|
|
228
285
|
- Always set `readAccessControl` and `writeAccessControl` on relations (`userItem`, `itemOf`, `propertyOf`).
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Stable node:test E2E lifecycle with env, withBrowser and e2eSuite
|
|
3
|
+
globs: **/e2e/**/*.{js,ts}
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Frontend E2E lifecycle
|
|
7
|
+
|
|
8
|
+
Use this pattern in LiveChange E2E tests based on `node:test`.
|
|
9
|
+
|
|
10
|
+
## Required files
|
|
11
|
+
|
|
12
|
+
- `e2e/env.ts` with `getTestEnv()` and `disposeTestEnv()`
|
|
13
|
+
- `e2e/withBrowser.ts` that closes context/browser in `finally`
|
|
14
|
+
- `e2e/e2eSuite.ts` that wraps each test file in `describe(...)` + `after(...)`
|
|
15
|
+
- `e2e/*.test.ts` wrapped in exactly one `e2eSuite(...)`
|
|
16
|
+
|
|
17
|
+
## Teardown rule
|
|
18
|
+
|
|
19
|
+
- Never register `after(...process.exit(...))` inside shared `e2e/env.ts`.
|
|
20
|
+
- Register final `process.exit(0)` only in `e2eSuite.ts` after calling `disposeTestEnv()`.
|
|
21
|
+
- Keep startup failure `process.exit(1)` in `getTestEnv()` catch block.
|
|
22
|
+
|
|
23
|
+
## Minimal suite wrapper
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { after, describe } from 'node:test'
|
|
27
|
+
import { disposeTestEnv } from './env.js'
|
|
28
|
+
|
|
29
|
+
export function e2eSuite(name: string, define: () => void): void {
|
|
30
|
+
describe(name, () => {
|
|
31
|
+
after(async () => {
|
|
32
|
+
await disposeTestEnv()
|
|
33
|
+
process.exit(0)
|
|
34
|
+
})
|
|
35
|
+
define()
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Running commands
|
|
41
|
+
|
|
42
|
+
When running E2E commands, use `fnm exec -- ...` so Node matches `.node-version` / `.nvmrc`.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Keep all locale files under front/locales in sync when adding or changing i18n strings
|
|
3
|
+
globs: **/front/locales/**/*.{json,js,ts}, **/front/src/**/*.{vue,js,ts}
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Frontend i18n – every file in `front/locales`
|
|
7
|
+
|
|
8
|
+
When you add or change **user-visible strings** (labels, messages, buttons, errors, page titles, etc.) that go through **vue-i18n** or the project’s locale files, you must update **every** locale resource in that app’s **`front/locales/`** directory — not only one language.
|
|
9
|
+
|
|
10
|
+
## Required workflow
|
|
11
|
+
|
|
12
|
+
1. **Identify the app root** for the frontend you are editing (the folder whose tree contains `front/src` and `front/locales`).
|
|
13
|
+
2. **List all locale files** in `front/locales/` for that app (`en.json`, `pl.json`, `en.js`, `pl.js`, `landing-en.json`, … — whatever exists).
|
|
14
|
+
3. **Add or update the same keys** in **each** of those files. Structure and nesting must stay consistent across languages (same key paths).
|
|
15
|
+
4. **Do not** add a new key to a single locale file and leave others missing — that breaks builds or shows raw keys at runtime.
|
|
16
|
+
|
|
17
|
+
## Translations you are unsure about
|
|
18
|
+
|
|
19
|
+
- Prefer a correct string in the primary locales (e.g. `en` + `pl`) when the product supports them.
|
|
20
|
+
- For other files, use a sensible placeholder, copy from English, or add a clearly marked temporary string — but **still add the key** everywhere so nothing is omitted.
|
|
21
|
+
|
|
22
|
+
## Scope
|
|
23
|
+
|
|
24
|
+
- Applies to any work under `front/src` that introduces or changes translated text, and to direct edits in `front/locales`.
|
|
25
|
+
- If a project uses multiple locale formats (`.json` and `.js`), update **all** files that participate in i18n for that app, not only one extension.
|
|
@@ -7,6 +7,10 @@ globs: **/front/src/**/*.{vue,js,ts}
|
|
|
7
7
|
|
|
8
8
|
Use these rules when working on frontends that talk to LiveChange backends.
|
|
9
9
|
|
|
10
|
+
## i18n and `front/locales`
|
|
11
|
+
|
|
12
|
+
Whenever you introduce or change translated strings, follow **`live-change-frontend-i18n-locales`**: add the same keys to **all** files in that app’s **`front/locales/`** directory (every language / variant — not only one file).
|
|
13
|
+
|
|
10
14
|
## Stack
|
|
11
15
|
|
|
12
16
|
- Vue 3 + TypeScript
|
|
@@ -48,6 +52,25 @@ In templates, use the `.value` of these refs:
|
|
|
48
52
|
</template>
|
|
49
53
|
```
|
|
50
54
|
|
|
55
|
+
## One-time fetches – `useFetch` (not `api.get` with Path)
|
|
56
|
+
|
|
57
|
+
When you need a **one-time fetch** (not a live subscription), use `useFetch`:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
import { usePath, useFetch } from '@live-change/vue3-ssr'
|
|
61
|
+
const path = usePath()
|
|
62
|
+
|
|
63
|
+
const data = await useFetch(path.paperInvoice.invoiceFileInfo({ invoiceFile: fileId }))
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**WARNING:** `path.service.view({ params })` returns a **Path object** (with `.what`, `.more`, `.to` properties), not a raw array. Do NOT pass it to `api.get()` — it will silently fail or produce wrong results.
|
|
67
|
+
|
|
68
|
+
| Pattern | Correct? |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `useFetch(path.svc.view({ ... }))` | **Yes** — handles Path objects |
|
|
71
|
+
| `api.get(['svc', 'view', { ... }])` | **Yes** — raw array, low-level |
|
|
72
|
+
| `api.get(path.svc.view({ ... }))` | **NO** — Path object, will break |
|
|
73
|
+
|
|
51
74
|
## Commands and forms – choosing the right pattern
|
|
52
75
|
|
|
53
76
|
There are 4 ways to execute backend actions. Use the right one:
|
|
@@ -65,6 +88,51 @@ Decision flow:
|
|
|
65
88
|
2. Is it editing a model record (create/update)? → **Yes**: use `editorData`. **No**: use `actionData`.
|
|
66
89
|
3. Only use `<command-form>` for the simplest throwaway cases.
|
|
67
90
|
|
|
91
|
+
## Autosave helpers – `synchronized` and `synchronizedList`
|
|
92
|
+
|
|
93
|
+
Use these helpers when editing reactive data from `live(...)`:
|
|
94
|
+
|
|
95
|
+
- Use `synchronized` for a single editable object.
|
|
96
|
+
- Use `synchronizedList` for editable list rows.
|
|
97
|
+
- Use shared context in `identifiers` and row-level keys in `objectIdentifiers`.
|
|
98
|
+
- For draft payloads, use `updateDataProperty: 'data'`.
|
|
99
|
+
|
|
100
|
+
Treat a screen as an editable list when:
|
|
101
|
+
|
|
102
|
+
- The UI uses `v-for` and each row has editable fields.
|
|
103
|
+
- Users edit many rows inline in one table/config/admin view.
|
|
104
|
+
- Autosave should persist changes row-by-row.
|
|
105
|
+
- Backend actions require shared list context plus row-specific keys.
|
|
106
|
+
|
|
107
|
+
For these cases, create one `synchronizedList(...)` and edit row fields through `syncList.value`.
|
|
108
|
+
Do not maintain a separate `id -> synchronized(...)` map for list rows.
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
const sync = synchronized({
|
|
112
|
+
source: sourceRef,
|
|
113
|
+
update: actions.service.updateThing,
|
|
114
|
+
identifiers: { thing: thingId },
|
|
115
|
+
recursive: true,
|
|
116
|
+
autoSave: true,
|
|
117
|
+
debounce: 600
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const syncList = synchronizedList({
|
|
121
|
+
source: rowsRef,
|
|
122
|
+
update: actions.service.updateRow,
|
|
123
|
+
delete: actions.service.deleteRow,
|
|
124
|
+
identifiers: { object, objectType },
|
|
125
|
+
objectIdentifiers: row => ({ row: row.to, object, objectType }),
|
|
126
|
+
recursive: true
|
|
127
|
+
})
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Typical list flows:
|
|
131
|
+
|
|
132
|
+
- role/permission editors,
|
|
133
|
+
- dictionary/configuration tables,
|
|
134
|
+
- multi-row settings pages with inline edits.
|
|
135
|
+
|
|
68
136
|
## Form validation feedback
|
|
69
137
|
|
|
70
138
|
Every field in a form using `editorData` or `actionData` **must** show validation errors. Never use bare `InputText`, `Dropdown`, or other PrimeVue inputs without error feedback.
|
|
@@ -234,6 +302,34 @@ path.blog.articles({})
|
|
|
234
302
|
|
|
235
303
|
Access: `article.authorProfile?.firstName`. Works with both `live()` and `RangeViewer`.
|
|
236
304
|
|
|
305
|
+
## Range lists with reactive filters
|
|
306
|
+
|
|
307
|
+
If range list `pathFunction` depends on reactive filters (month/status/search/company), prefer `ReactiveRangeViewer`.
|
|
308
|
+
|
|
309
|
+
Guidelines:
|
|
310
|
+
|
|
311
|
+
- do not rely on mutating `pathFunction` in `RangeViewer`
|
|
312
|
+
- avoid page-local workaround patterns with ad-hoc `:key`
|
|
313
|
+
- use `sourceKey` as an explicit reload trigger
|
|
314
|
+
- if layout stability matters, set `preserveHeightOnReload`
|
|
315
|
+
|
|
316
|
+
```vue
|
|
317
|
+
<ReactiveRangeViewer
|
|
318
|
+
:pathFunction="transactionsPathRange"
|
|
319
|
+
:sourceKey="JSON.stringify({ accountId, month: filterByMonth ? month : null })"
|
|
320
|
+
:preserveHeightOnReload="true"
|
|
321
|
+
:canLoadTop="false"
|
|
322
|
+
canDropBottom
|
|
323
|
+
/>
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## Range cursor guardrails (`RangeViewer` / `rangeBuckets`)
|
|
327
|
+
|
|
328
|
+
- For index-backed list views, backend should expose `sortedIndexRangePath`-based range views.
|
|
329
|
+
- Never override `range.gt/gte/lt/lte` inside frontend `pathFunction`.
|
|
330
|
+
- Keep cursor fields from RangeViewer intact; pass domain filters (`month`, `year`, `status`) as separate params.
|
|
331
|
+
- If filtering seems to require manual cursor rewriting, move logic to backend index design (or `prefixRange` fallback), not frontend hacks.
|
|
332
|
+
|
|
237
333
|
## WorkingZone for async actions
|
|
238
334
|
|
|
239
335
|
`ViewRoot` wraps every page in `<WorkingZone>`. Use `inject('workingZone')` for non-form button actions:
|
|
@@ -306,12 +402,12 @@ export const sitemap = sitemapEntry(App, createRouter, routerSitemap, config)
|
|
|
306
402
|
|
|
307
403
|
## Discovering views and actions with `describe`
|
|
308
404
|
|
|
309
|
-
Use the CLI `describe` command to find available views (for `live()`) and actions (for `api.command` / `editorData` / `actionData`)
|
|
405
|
+
Use the CLI `describe` command to find available views (for `live()`) and actions (for `api.command` / `editorData` / `actionData`). From the app root, use **fnm exec** so Node matches `.node-version` / `.nvmrc` (see `live-change-node-toolchain-fnm`).
|
|
310
406
|
|
|
311
407
|
```bash
|
|
312
|
-
node server/start.js describe --service blog
|
|
313
|
-
node server/start.js describe --service blog --view articlesByCreatedAt --output yaml
|
|
314
|
-
node server/start.js describe --service blog --action createMyUserArticle --output yaml
|
|
408
|
+
fnm exec -- node server/start.js describe --service blog
|
|
409
|
+
fnm exec -- node server/start.js describe --service blog --view articlesByCreatedAt --output yaml
|
|
410
|
+
fnm exec -- node server/start.js describe --service blog --action createMyUserArticle --output yaml
|
|
315
411
|
```
|
|
316
412
|
|
|
317
413
|
This is the fastest way to discover what paths and actions are available, including those auto-generated by relations.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Run Node, npm, npx, tsx with fnm exec using project .node-version or .nvmrc
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Node.js toolchain – use `fnm exec`
|
|
6
|
+
|
|
7
|
+
Do **not** rely on whatever Node.js version the agent sandbox or shell happens to use. That often differs from the project and breaks the LiveChange stack, tests, and `describe`.
|
|
8
|
+
|
|
9
|
+
Use **fnm** so the version from the project dotfiles (`.node-version` or `.nvmrc`) is selected.
|
|
10
|
+
|
|
11
|
+
## Required pattern
|
|
12
|
+
|
|
13
|
+
Work in the directory that contains the relevant `.node-version` or `.nvmrc` (app or package root). Prefix **every** `node`, `npm`, `npx`, `corepack`, or `tsx` invocation with:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
fnm exec -- <command and arguments>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
fnm exec -- node server/start.js describe --service myService --output yaml
|
|
23
|
+
fnm exec -- npm test
|
|
24
|
+
fnm exec -- npm run build
|
|
25
|
+
fnm exec -- npx eslint .
|
|
26
|
+
fnm exec -- tsx scripts/example.ts
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
If the dotfile lives only under a subfolder (e.g. `auto-firma/app/`), `cd` there first, then run `fnm exec -- ...`.
|
|
30
|
+
|
|
31
|
+
## When this applies
|
|
32
|
+
|
|
33
|
+
- Tests, CI scripts, linters, builds
|
|
34
|
+
- Framework CLI: `describe`, dev servers, migrations, anything touching `@live-change/*`
|
|
35
|
+
- Any `npm` / `node` / `npx` / `tsx` for this monorepo or its subprojects
|
|
36
|
+
|
|
37
|
+
## If `fnm` is unavailable
|
|
38
|
+
|
|
39
|
+
Tell the user to install fnm or align the environment; do not proceed assuming an arbitrary `node` on `PATH` is correct.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Rules for LiveChange service directory structure and file organization
|
|
3
|
-
globs: **/services/**/*.js
|
|
3
|
+
globs: **/services/**/*.js, **/server/**/*.js, server/**/*.js
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# LiveChange Service Structure
|
|
7
7
|
|
|
8
8
|
Every LiveChange service **must** be a directory, not a single file.
|
|
9
9
|
|
|
10
|
-
## Required
|
|
10
|
+
## Required structureW
|
|
11
11
|
|
|
12
12
|
```
|
|
13
13
|
server/services/<serviceName>/
|
package/.claude/settings.json
CHANGED
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
"Bash(cp /home/m8/IdeaProjects/live-change/.claude/rules/live-change-backend-event-sourcing.md /home/m8/IdeaProjects/live-change/automation/.claude/rules/live-change-backend-event-sourcing.md)",
|
|
23
23
|
"Bash(find /home/m8/IdeaProjects/live-change/live-change-stack/framework -type f \\\\\\(-name *.ts -o -name *.js \\\\\\))",
|
|
24
24
|
"Bash(find /home/m8/IdeaProjects/live-change/live-change-stack/services -type f -name *.js)",
|
|
25
|
-
"Bash(grep -r \"userItem\\\\|userProperty\" /home/m8/IdeaProjects/live-change/live-change-stack/services/user-service --include=*.js)"
|
|
25
|
+
"Bash(grep -r \"userItem\\\\|userProperty\" /home/m8/IdeaProjects/live-change/live-change-stack/services/user-service --include=*.js)",
|
|
26
|
+
"Bash(find /home/m8/IdeaProjects/live-change/auto-firma/app -maxdepth 2 -type f \\\\\\(-name jest.config.* -o -name vitest.config.* -o -name *.test.js -o -name *.test.ts \\\\\\))",
|
|
27
|
+
"Bash(find /home/m8/IdeaProjects/live-change/auto-firma/app -maxdepth 3 -type d -not -path */node_modules* -not -path */dist* -not -path */backups* -not -path */dev_docker_home* -not -path */storage* -not -path */tmp.db*)"
|
|
26
28
|
],
|
|
27
29
|
"additionalDirectories": [
|
|
28
30
|
"/home/m8/IdeaProjects/live-change/.claude/skills/create-skills-and-rules",
|
|
@@ -137,6 +137,22 @@ globs: **/front/src/**/*.{vue,js,ts}
|
|
|
137
137
|
| `description` | What this rule covers (used for matching) |
|
|
138
138
|
| `globs` | File patterns that trigger this rule (e.g. `**/*.js`, `**/services/**/*.js`) |
|
|
139
139
|
|
|
140
|
+
### Backend / LiveChange service rules — standard `globs`
|
|
141
|
+
|
|
142
|
+
Rules for **server-side** LiveChange code (models, actions, triggers, service layout) should use a **comma-separated** `globs` line so they still match when a project stores services under different folder names (many teams copy `.cursor/rules` into other repos):
|
|
143
|
+
|
|
144
|
+
```yaml
|
|
145
|
+
globs: **/services/**/*.js, **/server/**/*.js, server/**/*.js
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
| Pattern | Covers |
|
|
149
|
+
|---|---|
|
|
150
|
+
| `**/services/**/*.js` | Trees such as `live-change-stack/services/<service>/` |
|
|
151
|
+
| `**/server/**/*.js` | Any nested `server/` directory (e.g. `app/server/`, `packages/foo/server/`) |
|
|
152
|
+
| `server/**/*.js` | Backend rooted at top-level `server/` |
|
|
153
|
+
|
|
154
|
+
Frontend rules keep their own globs (e.g. `**/front/src/**/*.{vue,js,ts}`). Do not drop the `server` variants for backend-only rules — otherwise Cursor will miss files after a layout change.
|
|
155
|
+
|
|
140
156
|
### Body structure
|
|
141
157
|
|
|
142
158
|
```markdown
|
|
@@ -181,6 +197,7 @@ alwaysApply: false
|
|
|
181
197
|
- Do NOT quote glob patterns in frontmatter
|
|
182
198
|
- Keep rules short (target 25 lines, max 50 lines for best Cursor performance)
|
|
183
199
|
- The `.mdc` extension is required for Cursor
|
|
200
|
+
- For **backend** LiveChange rules, use the same **`globs`** standard as in Claude rules: `**/services/**/*.js, **/server/**/*.js, server/**/*.js`
|
|
184
201
|
|
|
185
202
|
## Step 5 – Register rules in OpenCode (`opencode.json`)
|
|
186
203
|
|
|
@@ -198,8 +215,12 @@ OpenCode reads `.claude/skills/<name>/SKILL.md` natively for skills (no extra st
|
|
|
198
215
|
|
|
199
216
|
When you **create a new rule**, add its path to the `instructions` array in `opencode.json`.
|
|
200
217
|
|
|
218
|
+
Project rule **`live-change-node-toolchain-fnm`** should stay **first** (or early) in `instructions` so agents always load the requirement to run `node`, `npm`, `npx`, and `tsx` via `fnm exec` using `.node-version` / `.nvmrc`.
|
|
219
|
+
|
|
201
220
|
When you **create a new skill**, no `opencode.json` change is needed — OpenCode discovers skills from `.claude/skills/<name>/SKILL.md` automatically.
|
|
202
221
|
|
|
222
|
+
When a skill or rule shows shell examples that invoke **`node`**, **`npm`**, **`npx`**, or **`tsx`**, use the **`fnm exec -- …`** form (see `.claude/rules/live-change-node-toolchain-fnm.md` and skill `live-change-node-toolchain-fnm`).
|
|
223
|
+
|
|
203
224
|
**Important:** OpenCode ignores the `globs` frontmatter from Claude Code rules. All instructions listed in `opencode.json` are always loaded.
|
|
204
225
|
|
|
205
226
|
## Step 6 – Mirror Cursor skills (`.cursor/skills/<name>.md`)
|
|
@@ -239,10 +260,12 @@ done
|
|
|
239
260
|
|
|
240
261
|
## Checklist
|
|
241
262
|
|
|
263
|
+
- [ ] Shell examples for Node/npm use `fnm exec --` per `live-change-node-toolchain-fnm`
|
|
242
264
|
- [ ] Directory created: `.claude/skills/<name>/SKILL.md`
|
|
243
265
|
- [ ] Frontmatter has both `name` (matching dir) and `description`
|
|
244
266
|
- [ ] `.cursor/skills/<name>.md` mirrored (flat file, same content)
|
|
245
267
|
- [ ] `.claude/rules/*.md` created (if rule)
|
|
246
268
|
- [ ] `.cursor/rules/*.mdc` created with `globs` + `alwaysApply` (if rule)
|
|
269
|
+
- [ ] Backend rules use standard `globs`: `**/services/**/*.js, **/server/**/*.js, server/**/*.js` (when the rule targets LiveChange server code)
|
|
247
270
|
- [ ] `opencode.json` `instructions` array updated (if new rule)
|
|
248
271
|
- [ ] Sub-projects updated (automation, auto-firma)
|
|
@@ -112,6 +112,21 @@ definition.trigger({
|
|
|
112
112
|
|
|
113
113
|
This means: when a user creates a Schedule via the UI or API, the timer is automatically set up. When they update it, the old timer is canceled and a new one created. When they delete it, the timer is canceled.
|
|
114
114
|
|
|
115
|
+
## Cron-service — planning and admin UI guardrails
|
|
116
|
+
|
|
117
|
+
When the domain needs **wall-clock schedules** or **fixed repeating intervals** that run a **trigger**, default to **`@live-change/cron-service`** (models **Schedule** / **Interval**, internal **timer** + **changeCron_*** lifecycle), not ad-hoc timers only.
|
|
118
|
+
|
|
119
|
+
**Backend:** define the **target `definition.trigger`** in your service; put **Schedule** / **Interval** rows in **cron** with **`trigger: { name, service, properties, returnTask }`**. Rely on **`changeCron_Schedule`** / **`changeCron_Interval`** for timer repair (already implemented in cron-service).
|
|
120
|
+
|
|
121
|
+
**Admin / task-frontend-style UI:** use the same integration as the reference pages:
|
|
122
|
+
|
|
123
|
+
- **Create:** `ActionForm` with `service="cron"` and `action="setSchedule"` or `action="setInterval"` (relations-driven forms).
|
|
124
|
+
- **List:** `RangeViewer` + `path.cron.schedules` / `path.cron.intervals` with **`reverseRange(range)`** as needed.
|
|
125
|
+
- **Per row:** `.with()` → `scheduleInfo` / `intervalInfo`, `runState` (`jobType` **`cron_Schedule`** or **`cron_Interval`**, **`job`** = id), and `task.tasksByCauseAndCreatedAt` for recent runs.
|
|
126
|
+
- **Delete:** `api.actions.cron.deleteSchedule` / `deleteInterval`.
|
|
127
|
+
|
|
128
|
+
See **server doc** `15-cron-and-intervals.md` → section **“API used by task-frontend”** for path examples and **Schedule** field semantics (**`NaN`** = “every” for that field).
|
|
129
|
+
|
|
115
130
|
## Step 4 – Specific lifecycle triggers (alternative)
|
|
116
131
|
|
|
117
132
|
If you only care about one lifecycle event, use the specific variant:
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Jak poprawnie wywoływać akcje i widoki frameworka LiveChange przez surowy protokół DAO.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# LiveChange DAO Protocol Arguments
|
|
6
|
+
|
|
7
|
+
Kiedy komunikujesz się z frameworkiem LiveChange używając surowego protokołu `@live-change/dao` (np. z C++, Pythona, Rusta, Go lub dowolnego innego klienta nie-JS), MUSISZ ZAWSZE przekazywać argumenty jako **tablicę**.
|
|
8
|
+
|
|
9
|
+
Framework traktuje argumenty żądań (request) i obserwabli (observable) DAO jak argumenty funkcji i używa operatora spread (`...args`), aby przekazać je do bazowych funkcji akcji lub widoków. Jeśli przekażesz obiekt zamiast tablicy, serwer rzuci błąd `TypeError: Spread syntax requires ...iterable[Symbol.iterator] to be a function`.
|
|
10
|
+
|
|
11
|
+
Nawet jeśli akcja lub widok oczekuje pojedynczego obiektu jako parametru, ten obiekt MUSI być opakowany w jednoelementową tablicę.
|
|
12
|
+
|
|
13
|
+
## Przykład w C++ (używając nlohmann/json)
|
|
14
|
+
|
|
15
|
+
### Niepoprawnie ❌
|
|
16
|
+
```cpp
|
|
17
|
+
nlohmann::json args = {
|
|
18
|
+
{"pairingKey", "123"},
|
|
19
|
+
{"connectionType", "device"}
|
|
20
|
+
};
|
|
21
|
+
connection->request({"serviceName", "actionName"}, args, settings);
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Poprawnie ✅
|
|
25
|
+
```cpp
|
|
26
|
+
// Wrap the object in an array
|
|
27
|
+
auto args = {
|
|
28
|
+
nlohmann::json::object({
|
|
29
|
+
{"pairingKey", "123"},
|
|
30
|
+
{"connectionType", "device"}
|
|
31
|
+
})
|
|
32
|
+
};
|
|
33
|
+
connection->request({"serviceName", "actionName"}, args, settings);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Lub jawnie:
|
|
37
|
+
```cpp
|
|
38
|
+
nlohmann::json args = nlohmann::json::array({
|
|
39
|
+
nlohmann::json::object({
|
|
40
|
+
{"pairingKey", "123"},
|
|
41
|
+
{"connectionType", "device"}
|
|
42
|
+
})
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Zawsze upewnij się, że Twój payload `args` jest tablicą przed wysłaniem go przez połączenie DAO.
|