@live-change/frontend-template 0.9.203 → 0.9.205

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.
Files changed (30) hide show
  1. package/.claude/rules/live-change-backend-actions-views-triggers.md +49 -1
  2. package/.claude/rules/live-change-backend-models-and-relations.md +24 -6
  3. package/.claude/rules/live-change-service-structure.md +2 -2
  4. package/.claude/settings.json +3 -1
  5. package/.claude/skills/live-change-backend-change-triggers/SKILL.md +15 -0
  6. package/.claude/skills/live-change-dao-protocol/SKILL.md +46 -0
  7. package/.claude/skills/live-change-design-actions-views-triggers/SKILL.md +110 -0
  8. package/.claude/skills/live-change-design-models-relations/SKILL.md +63 -5
  9. package/.claude/skills/live-change-frontend-data-views/SKILL.md +73 -4
  10. package/.claude/skills/live-change-frontend-range-list/SKILL.md +90 -0
  11. package/.claude/skills/live-change-frontend-synchronized/SKILL.md +101 -0
  12. package/.cursor/rules/live-change-backend-actions-views-triggers.mdc +23 -4
  13. package/.cursor/rules/live-change-backend-architecture.mdc +1 -1
  14. package/.cursor/rules/live-change-backend-event-sourcing.mdc +1 -1
  15. package/.cursor/rules/live-change-backend-models-and-relations.mdc +36 -7
  16. package/.cursor/rules/live-change-backend-views-vs-triggers-for-reads-writes.mdc +28 -0
  17. package/.cursor/rules/live-change-dao-protocol.mdc +47 -0
  18. package/.cursor/rules/live-change-frontend-views-not-commands-for-reads.mdc +30 -0
  19. package/.cursor/rules/live-change-frontend-vue-primevue.mdc +70 -4
  20. package/.cursor/rules/live-change-service-structure.mdc +1 -1
  21. package/.cursor/skills/live-change-backend-change-triggers.md +15 -0
  22. package/.cursor/skills/live-change-design-actions-views-triggers.md +51 -0
  23. package/.cursor/skills/live-change-design-models-relations.md +23 -5
  24. package/.cursor/skills/live-change-frontend-data-views.md +15 -0
  25. package/.cursor/skills/live-change-frontend-range-list.md +21 -0
  26. package/.cursor/skills/live-change-frontend-synchronized.md +101 -0
  27. package/.node-version +1 -1
  28. package/.nvmrc +1 -1
  29. package/front/src/pages/index.vue +1 -1
  30. package/package.json +55 -55
@@ -0,0 +1,101 @@
1
+ ---
2
+ name: live-change-frontend-synchronized
3
+ description: Use synchronized and synchronizedList for autosave editing in LiveChange Vue frontends, including object vs list decision flow, identifiers mapping, and save/delete patterns
4
+ ---
5
+
6
+ # Skill: live-change-frontend-synchronized (Claude Code)
7
+
8
+ Use this skill when implementing or refactoring frontend editing flows that should keep local form state synchronized with backend data.
9
+
10
+ ## When to use
11
+
12
+ - Editing one object loaded from `live(...)` with autosave or manual save.
13
+ - Editing list items inline (for example access roles) with per-row identifiers.
14
+ - Replacing custom `watch + api.command` autosave logic with a standard helper.
15
+ - Choosing between `synchronized`, `synchronizedList`, and `editorData`.
16
+
17
+ ## Decision flow
18
+
19
+ 1. **One editable object** (`profile`, `note`, `settings`) -> use `synchronized`.
20
+ 2. **Editable list rows** (`accesses`, `invitations`, `requests`) -> use `synchronizedList`.
21
+ 3. **Definition-driven CRUD form with validation UI** -> use `editorData` from auto-form stack.
22
+
23
+ ## What counts as "editable list rows"
24
+
25
+ Treat the feature as a list flow (`synchronizedList`) when most of these are true:
26
+
27
+ - The UI renders rows with `v-for` and each row has editable fields.
28
+ - Users can edit many rows inline in one screen (table/config/admin list).
29
+ - Autosave should happen per row while the list stays reactive.
30
+ - Each row needs its own action payload keys in addition to shared list context.
31
+
32
+ In this case, wire one `synchronizedList(...)` and edit fields directly on `syncList.value`.
33
+ Do not build a parallel `id -> synchronized(...)` map for rows.
34
+
35
+ ## `synchronized` pattern (object)
36
+
37
+ ```javascript
38
+ import { synchronized } from '@live-change/vue3-components'
39
+
40
+ const sync = synchronized({
41
+ source: sourceRef,
42
+ update: actions.service.updateThing,
43
+ identifiers: computed(() => ({ thing: thingId.value })),
44
+ recursive: true,
45
+ autoSave: true,
46
+ debounce: 600
47
+ })
48
+
49
+ const { value: editable, changed, saving, save } = sync
50
+ ```
51
+
52
+ ### Rules
53
+
54
+ - `source` should come from `live(...)` or a computed source.
55
+ - Keep identifiers stable and pass them through `identifiers` (plain object or `computed`).
56
+ - Use `updateDataProperty: 'data'` for draft-like payloads where backend expects nested data.
57
+ - Use `autoSave: false` when save must happen only after explicit confirmation.
58
+
59
+ ## `synchronizedList` pattern (list)
60
+
61
+ ```javascript
62
+ import { synchronizedList } from '@live-change/vue3-components'
63
+
64
+ const syncList = synchronizedList({
65
+ source: accesses,
66
+ update: actions.accessControl.updateSessionOrUserAndObjectOwnedAccess,
67
+ delete: actions.accessControl.resetSessionOrUserAndObjectOwnedAccess,
68
+ identifiers: { object, objectType },
69
+ objectIdentifiers: ({ to, sessionOrUser, sessionOrUserType }) => ({
70
+ access: to, sessionOrUser, sessionOrUserType, object, objectType
71
+ }),
72
+ recursive: true
73
+ })
74
+
75
+ const editableRows = syncList.value
76
+ await syncList.delete(editableRows.value[0])
77
+ ```
78
+
79
+ ### Rules
80
+
81
+ - `source` should be a list from `live(...)`.
82
+ - Every item should have stable `id`.
83
+ - Put shared context in `identifiers`, and row-specific keys in `objectIdentifiers`.
84
+ - Edit rows through `syncList.value`; call `syncList.delete(...)` / `syncList.insert(...)` on the helper object.
85
+ - Prefer this pattern for configuration lists, permission tables, dictionaries, and other multi-row settings editors.
86
+
87
+ ## Project examples to follow
88
+
89
+ - `speed-dating/front/src/components/notes/NoteEditor.vue` (`synchronized` with autosave)
90
+ - `speed-dating/front/src/components/profile/ProfileSettings.vue` (`synchronized` + `updateDataProperty: 'data'`)
91
+ - `family-tree/front/src/components/AgreementDialog.vue` (`synchronized` with manual save)
92
+ - `rcstreamer/front/src/configuration/AccessRequests.vue` (`synchronizedList`)
93
+ - `rcstreamer/front/src/configuration/AccessInvitations.vue` (`synchronizedList`)
94
+ - `rcstreamer/front/src/configuration/AccessList.vue` (`synchronizedList`)
95
+
96
+ ## Common mistakes
97
+
98
+ - Using `synchronized` for a list of rows instead of `synchronizedList`.
99
+ - Forgetting `objectIdentifiers` when backend update/delete actions need per-row keys.
100
+ - Mutating raw `live(...)` list data instead of `synchronizedList(...).value`.
101
+ - Mixing autosave helper patterns with unrelated one-shot action form patterns.
@@ -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
  alwaysApply: false
5
5
  ---
6
6
 
@@ -54,9 +54,9 @@ definition.action({
54
54
 
55
55
  ## Widoki – ogólne zasady
56
56
 
57
- - Widok ma być prostym, czytelnym wejściem do danych:
58
- - korzystaj z indeksów (`indexObjectGet`, `indexRangeGet`),
59
- - nie implementuj skomplikowanej logiki w widokach, jeśli można ją przenieść do akcji.
57
+ - Widok ma być **kanałem odczytu** (DAO, indeksy, agregacje, wyliczenia). Logika odczytu należy do widoku (ew. funkcji pomocniczej wywołanej z `daoPath` / `get`), a nie do akcji „tylko po to, żeby zwrócić dane bez zmiany stanu”.
58
+ - W implementacji ścieżek korzystaj z indeksów (`indexObjectGet`, `indexRangeGet`) zamiast pełnych skanów.
59
+ - Odczyt vs zapis (CQRS-like): [live-change-backend-views-vs-triggers-for-reads-writes.mdc](./live-change-backend-views-vs-triggers-for-reads-writes.mdc).
60
60
 
61
61
  ## Widoki – reguła: `get` + `observable` zawsze razem
62
62
 
@@ -173,6 +173,19 @@ Jeśli filtr po miesiącu jest częsty, lepiej dodać indeks z bucketem miesiąc
173
173
  BankTransaction.sortedIndexRangePath('byBankAccountAndMonthAndDate', [bankAccount, month], range)
174
174
  ```
175
175
 
176
+ ### Guardrails dla widoków pod RangeViewer/rangeBuckets
177
+
178
+ - Dla list paginowanych po indeksie preferuj `Model.sortedIndexRangePath(indexName, keyPrefix, App.extractRange(props))`.
179
+ - Nie stosuj semantyki `indexRangePath` dla widoków konsumowanych przez bucketowe UI zakresowe.
180
+ - `gt/gte/lt/lte` zostaw wyłącznie jako kursor paginacji, nie jako filtry domenowe.
181
+ - Dla filtrów typu miesiąc/rok/status najpierw projektuj indeks z odpowiednim prefiksem.
182
+ - `App.utils.prefixRange` traktuj jako fallback backendowy, gdy zmiana indeksu nie jest możliwa.
183
+
184
+ ### Guardrail dla indeksów standalone
185
+
186
+ - Gdy indeks łączy równorzędne strumienie danych (union wielu tabel), definiuj go jako serwisowy `definition.index(...)` (najlepiej w osobnym `indexes.js`), a nie jako `model.indexes` przypisany do przypadkowego modelu.
187
+ - `model.indexes` stosuj tylko wtedy, gdy semantycznym właścicielem indeksu jest jeden model.
188
+
176
189
  ## Triggery – online/offline i batchowanie
177
190
 
178
191
  - Triggery są do reakcji na zdarzenia (np. zmiana stanu sesji, start serwera).
@@ -271,6 +284,12 @@ definition.trigger({
271
284
 
272
285
  Sprawdzaj `data`/`oldData`: oba obecne = update, tylko `data` = create, tylko `oldData` = delete.
273
286
 
287
+ ## Cron-service — harmonogramy, interwały i UI admina
288
+
289
+ - Dla wykonywania **triggerów** wg **czasu ściennego** lub **stałego interwału** używaj **`@live-change/cron-service`** (**Schedule** / **Interval**) wraz z **task-service** — nie planuj „tylko timera” bez modeli cron i cyklu życia **`changeCron_Schedule`** / **`changeCron_Interval`**.
290
+ - Wzorzec admina (jak **task-frontend**): **`setSchedule`** / **`setInterval`** przez **`ActionForm`**, listy przez **`path.cron.schedules`** / **`path.cron.intervals`**, wzbogacanie wierszy **`.with()`** o **`scheduleInfo`** / **`intervalInfo`**, **`runState`** (`jobType` **`cron_Schedule`** lub **`cron_Interval`**), **`task.tasksByCauseAndCreatedAt`**; usuwanie przez **`deleteSchedule`** / **`deleteInterval`**.
291
+ - Pola czasu **Schedule** (**minute**, **hour**, **day**, **dayOfWeek**, **month**): **`NaN`** = „każdy” na danym poziomie — szczegóły w **`15-cron-and-intervals.md`** (sekcja **API used by task-frontend**).
292
+
274
293
  ## Wzorzec „pending + resolve” (asynchroniczny wynik)
275
294
 
276
295
  - Używaj, gdy akcja w serwisie musi poczekać na wynik z zewnętrznego procesu (np. urządzenie, worker).
@@ -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
  alwaysApply: false
5
5
  ---
6
6
 
@@ -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
  alwaysApply: false
5
5
  ---
6
6
 
@@ -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
  alwaysApply: false
5
5
  ---
6
6
 
@@ -36,6 +36,29 @@ properties: {
36
36
  }
37
37
  ```
38
38
 
39
+ ## Arity relacji (bardzo ważne)
40
+
41
+ Rozróżniaj dwa poziomy:
42
+
43
+ - **arity adnotacji** — czy można podać listę konfiguracji
44
+ - **arity rodziców w konfiguracji** — `what: [A, B]` lub `to: ['owner', 'topic']`
45
+
46
+ | Relacja | Arity adnotacji | Arity rodziców |
47
+ |---|---|---|
48
+ | `propertyOf` | jedna konfiguracja | `what` może być pojedyncze lub tablica modeli |
49
+ | `itemOf` | jedna konfiguracja | `what` może być pojedyncze lub tablica modeli |
50
+ | `boundTo` | jedna konfiguracja | `what` może być pojedyncze lub tablica modeli |
51
+ | `relatedTo` | jedna lub wiele konfiguracji | w każdej konfiguracji `what` może być pojedyncze lub tablica modeli |
52
+ | `propertyOfAny` | jedna konfiguracja | `to` może mieć jedną lub wiele nazw |
53
+ | `itemOfAny` | jedna konfiguracja | `to` może mieć jedną lub wiele nazw |
54
+ | `boundToAny` | jedna konfiguracja | `to` może mieć jedną lub wiele nazw |
55
+ | `relatedToAny` | jedna lub wiele konfiguracji | w każdej konfiguracji `to` może mieć jedną lub wiele nazw |
56
+
57
+ Przykłady:
58
+
59
+ - poprawnie: `propertyOf: { what: [A, B] }`
60
+ - niepoprawnie: `propertyOf: [configA, configB]`
61
+
39
62
  ## `userItem` – zasób należący do użytkownika
40
63
 
41
64
  Używaj, gdy model należy do zalogowanego użytkownika.
@@ -123,7 +146,7 @@ Wtedy:
123
146
 
124
147
  - **nie** przechowuj „drugiej strony” jako zwykłego `contractorId` (albo ogólnie `someId`) w `properties`
125
148
  - **nie** dodawaj ręcznie pól `...Id` w modelu relacyjnym, jeśli to ma być relacja – to utrudnia generatorowi CRUD/relacji poprawne wnioskowanie o powiązaniach
126
- - zamiast tego zdefiniuj relacje jako `propertyOf` do **każdego** z modeli, które mają być rodzicami relacji
149
+ - zamiast tego zdefiniuj jedną relację `propertyOf` z `what: [ModelA, ModelB, ...]`
127
150
 
128
151
  To jest istotne, bo generator CRUD/relacji rozumie wtedy, że encja jest powiązaniem pomiędzy dwiema encjami, a nie „zwykłym obiektem z polem id”.
129
152
 
@@ -138,10 +161,9 @@ definition.model({
138
161
  properties: {
139
162
  // dodatkowe pola relacji (opcjonalnie)
140
163
  },
141
- propertyOf: [
142
- { what: CostInvoice },
143
- { what: Contractor }
144
- ]
164
+ propertyOf: {
165
+ what: [CostInvoice, Contractor]
166
+ }
145
167
  })
146
168
  ```
147
169
 
@@ -186,7 +208,7 @@ Relacje automatycznie dodają **pola identyfikatorów** i **indeksy** do modelu.
186
208
  | `propertyOfAny: { ownerTypes: [...] }` | `ownerType`, `owner` | `byOwner` (hash) |
187
209
  | `boundTo: { what: Device }` | `device` | `byDevice` (hash) |
188
210
 
189
- Dla relacji z wieloma rodzicami (np. `propertyOf: [{ what: A }, { what: B }]`) tworzone są wszystkie kombinacje indeksów (`byA`, `byB`, `byAAndB`).
211
+ Dla relacji z wieloma rodzicami (np. `propertyOf: { what: [A, B] }`) tworzone są wszystkie kombinacje indeksów (`byA`, `byB`, `byAAndB`).
190
212
 
191
213
  ## `propertyOfAny` — typy rodzica (`{name}Types`)
192
214
 
@@ -249,6 +271,13 @@ indexes: {
249
271
 
250
272
  Poza serwisem indeks bywa widoczny pod nazwą z prefiksem serwisu, np. `myService_Model_byDeviceAndStatus`.
251
273
 
274
+ ### Kiedy indeks ma być poza modelem
275
+
276
+ Jeśli indeks jest unią/projekcją danych z kilku równorzędnych tabel (bez jednego naturalnego właściciela), nie wciskaj go do `model.indexes`.
277
+
278
+ - Użyj serwisowego `definition.index(...)`, najlepiej w osobnym `indexes.js`.
279
+ - To dotyczy szczególnie indeksów łączących różne typy encji w jeden strumień odczytu.
280
+
252
281
  ### Indeksy `function` dla pól wyliczanych
253
282
 
254
283
  Gdy część klucza indeksu nie istnieje jako zwykłe pole modelu (np. `month` wyliczany z `date`), użyj indeksu `function` zamiast `property`.
@@ -0,0 +1,28 @@
1
+ ---
2
+ description: LiveChange backend — views/viewGet for reads; actions/triggers + emit for writes (CQRS-like)
3
+ globs: **/services/**/*.js, **/server/**/*.js, server/**/*.js
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # LiveChange backend — widoki do odczytu, akcja/trigger do zapisu
8
+
9
+ Na serwerze **inne ścieżki niż na froncie**: nie ma `live` / `useFetch` — odczyt definiujesz jako **`definition.view`** i ewentualnie wywołujesz go z kodu przez **`app.viewGet`** / **`app.serviceViewGet`**.
10
+
11
+ ## Odczyt
12
+
13
+ - **Widok** (`definition.view`) — jedyny kanał wystawienia danych do klienta (DAO, indeksy, wyliczenia, `fetch` zewnętrzny). Może zwracać dane złożone lub wyliczone (np. podgląd „następnego numeru”).
14
+ - **Z kodu serwera** (trigger, akcja, batch):
15
+ - `await app.viewGet('viewName', { ...properties })` — widok **w tym samym** serwisie co `app`.
16
+ - `await app.serviceViewGet('otherService', 'viewName', { ...properties })` — widok w **innym** serwisie.
17
+ - Bezpośredni odczyt modelu (`Model.get`, `indexObjectGet`, …) jest OK tam, gdzie nie potrzebujesz warstwy widoku/access control widoku — ale **nie** zamieniaj tego na „akcję tylko po to, żeby coś zwrócić”.
18
+
19
+ ## Zapis / zmiana stanu
20
+
21
+ - **Akcja** (`definition.action`) i **trigger** (`definition.trigger`) — walidacja, orchestracja, **`emit`** do eventów, **`trigger`** / **`triggerService`** do zapisu zgodnie z [live-change-backend-event-sourcing.mdc](./live-change-backend-event-sourcing.mdc).
22
+ - Nie dodawaj `definition.action`, której jedynym celem jest zwrócenie danych **bez** trwałej zmiany stanu — użyj **widoku**.
23
+
24
+ ## Antywzorzec
25
+
26
+ - Akcja/command po stronie klienta tylko po to, żeby pobrać podgląd — błąd projektu po obu stronach: na backendzie powinien być **widok**, na froncie `live`/`useFetch` (reguła frontendowa).
27
+
28
+ Szczegóły: `live-change-stack/docs/docs/server/07-views.md`, `06-actions.md`, `08-triggers.md`. API klienta (Vue) — `live-change-stack/docs/docs/frontend/04-logic-and-data-layer.md`, nie ta reguła.
@@ -0,0 +1,47 @@
1
+ ---
2
+ description: Zasady komunikacji przez surowy protokół DAO (C++, Python, inne klienty)
3
+ globs: *.cpp, *.py, *.rs, *.go
4
+ ---
5
+
6
+ # LiveChange DAO Protocol Arguments
7
+
8
+ When communicating with the LiveChange framework using the raw `@live-change/dao` protocol (e.g., from C++, Python, Rust, Go, or any other non-JS client), you MUST ALWAYS pass arguments as an **array**.
9
+
10
+ The framework treats DAO request and observable arguments like function arguments and uses the spread operator (`...args`) to pass them to the underlying action or view functions. If you pass an object instead of an array, the server will throw a `TypeError: Spread syntax requires ...iterable[Symbol.iterator] to be a function`.
11
+
12
+ Even if an action or view expects a single object as its parameter, that object MUST be wrapped in a single-element array.
13
+
14
+ ## C++ Example (using nlohmann/json)
15
+
16
+ ### Incorrect ❌
17
+ ```cpp
18
+ nlohmann::json args = {
19
+ {"pairingKey", "123"},
20
+ {"connectionType", "device"}
21
+ };
22
+ connection->request({"serviceName", "actionName"}, args, settings);
23
+ ```
24
+
25
+ ### Correct ✅
26
+ ```cpp
27
+ // Wrap the object in an array
28
+ auto args = {
29
+ nlohmann::json::object({
30
+ {"pairingKey", "123"},
31
+ {"connectionType", "device"}
32
+ })
33
+ };
34
+ connection->request({"serviceName", "actionName"}, args, settings);
35
+ ```
36
+
37
+ Or explicitly:
38
+ ```cpp
39
+ nlohmann::json args = nlohmann::json::array({
40
+ nlohmann::json::object({
41
+ {"pairingKey", "123"},
42
+ {"connectionType", "device"}
43
+ })
44
+ });
45
+ ```
46
+
47
+ Always double-check that your `args` payload is an array before sending it over the DAO connection.
@@ -0,0 +1,30 @@
1
+ ---
2
+ description: LiveChange frontend — use views (live/useFetch) for reads; commands only for mutations
3
+ globs: **/front/**/*.{vue,js,ts}
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # LiveChange frontend — odczyt przez widoki, nie przez command
8
+
9
+ Ten sam **model mentalny** co na backendzie (odczyt vs zapis), ale **inne API**: na froncie nie ma `app.viewGet` — klient korzysta ze **ścieżek widoków** z `usePath()`.
10
+
11
+ ## Odczyt danych
12
+
13
+ - Buduj ścieżkę: `usePath()` (tylko synchronicznie w `setup`) → `path.<service>.<viewName>({ ...params })`; w `computed` używaj już utworzonego obiektu `path`, nie wywołuj ponownie `usePath()` / `path()`.
14
+ - Subskrypcja: `live(computed(() => path...))` albo `await Promise.all([live(...), ...])`.
15
+ - Jednorazowo: `useFetch(path.service.view({ ... }))` (np. w handlerze, po uploadzie).
16
+ - Dla niezależnych odczytów uruchamiaj `live(...)` równolegle (`Promise.all`), a sekwencyjnie tylko gdy drugi path zależy od wyniku pierwszego.
17
+ - W `Path.with(...)` buduj deklaratywnie ścieżki; do branchingu po typie/polu używaj `$switch`, nie runtime `if` na callbacku `.with`.
18
+
19
+ Nie używaj `api.command` ani `useActions()` do „załadowania” danych, podglądu, wyliczenia tylko do UI, ani „jakiego będzie następny numer” — to **zawsze** powinno iść przez **widok** (`definition.view` po stronie serwera) i `live` / `useFetch` po stronie klienta.
20
+
21
+ ## Mutacja stanu
22
+
23
+ - `api.command(['service', 'actionName'], props)` lub `useActions()` → wywołanie **akcji** zmieniającej trwały stan (create/update/delete przez eventy itd.).
24
+ - `workingZone` i spinner przy commandach mają sens przy **zapisie**, nie przy odczycie.
25
+
26
+ ## Antywzorzec
27
+
28
+ - Wywołanie akcji/command tylko po to, żeby dostać zwrócony string (np. podgląd numeru faktury) **bez** zmiany bazy — **źle**. Zamiast tego dodaj **widok** zwracający ten podgląd i pobierz go przez `live` / `useFetch`.
29
+
30
+ Szczegóły API: `live-change-stack/docs/docs/frontend/04-logic-and-data-layer.md`. Backend (`app.viewGet`, triggery): reguła `live-change-backend-views-vs-triggers-for-reads-writes.mdc` — nie stosuj jej w plikach Vue.
@@ -6,10 +6,6 @@ alwaysApply: false
6
6
 
7
7
  # Frontend na live-change-stack – Vue 3 + PrimeVue + Tailwind
8
8
 
9
- ## i18n i `front/locales`
10
-
11
- Za każdym razem gdy dodajesz lub zmieniasz teksty pod tłumaczenia, stosuj **`live-change-frontend-i18n-locales`**: te same klucze muszą trafić do **wszystkich** plików w **`front/locales/`** danej aplikacji (każdy język / wariant — nie tylko jeden plik).
12
-
13
9
  ## Stack
14
10
 
15
11
  - Vue 3 + TypeScript
@@ -23,6 +19,8 @@ Za każdym razem gdy dodajesz lub zmieniasz teksty pod tłumaczenia, stosuj **`l
23
19
  - **Nie** używaj `ref(null)` + `onMounted` do ładowania danych.
24
20
  - Dane ładuj w `setup`/`script setup` przez `await Promise.all(...)` na `live(path()....)`.
25
21
  - Rodzic powinien owijać stronę w `<Suspense>` (w projektach live-change robi to zazwyczaj `ViewRoot` globalnie).
22
+ - Sekwencyjnego `await live(...)` używaj tylko wtedy, gdy drugi path zależy od wyniku pierwszego.
23
+ - Gdy zależność da się opisać przez `.with(...)`, preferuj jedno zapytanie Path DSL zamiast imperatywnego łączenia wyników.
26
24
 
27
25
  Przykład:
28
26
 
@@ -68,6 +66,50 @@ Schemat decyzji:
68
66
  2. Edytuje rekord modelu (create/update)? → **Tak**: użyj `editorData`. **Nie**: użyj `actionData`.
69
67
  3. `<command-form>` tylko do najprostszych jednorazowych przypadków.
70
68
 
69
+ ## Autosave helpery – `synchronized` i `synchronizedList`
70
+
71
+ Używaj helperów dla danych z `live(...)`:
72
+
73
+ - `synchronized` dla pojedynczego obiektu do edycji.
74
+ - `synchronizedList` dla edytowalnych list (wierszy).
75
+ - Kontekst wspólny przekazuj przez `identifiers`, a identyfikatory wiersza przez `objectIdentifiers`.
76
+ - Dla draftów z payloadem zagnieżdżonym używaj `updateDataProperty: 'data'`.
77
+
78
+ Kiedy traktować ekran jako listę:
79
+
80
+ - UI ma `v-for` z wieloma edytowalnymi wierszami (tabela/lista konfiguracji/admin).
81
+ - Użytkownik edytuje wiele rekordów inline, a zapis ma działać per wiersz.
82
+ - Akcje backendowe potrzebują wspólnego kontekstu listy i kluczy konkretnego wiersza.
83
+
84
+ W takich przypadkach stosuj jeden `synchronizedList(...)` i edytuj dane bezpośrednio przez `syncList.value`.
85
+ Dla takich list nie buduj osobnej mapy `id -> synchronized(...)`.
86
+
87
+ ```js
88
+ const sync = synchronized({
89
+ source: sourceRef,
90
+ update: actions.service.updateThing,
91
+ identifiers: { thing: thingId },
92
+ recursive: true,
93
+ autoSave: true,
94
+ debounce: 600
95
+ })
96
+
97
+ const syncList = synchronizedList({
98
+ source: rowsRef,
99
+ update: actions.service.updateRow,
100
+ delete: actions.service.deleteRow,
101
+ identifiers: { object, objectType },
102
+ objectIdentifiers: row => ({ row: row.to, object, objectType }),
103
+ recursive: true
104
+ })
105
+ ```
106
+
107
+ Przykłady ekranów, gdzie to dotyczy:
108
+
109
+ - listy uprawnień i ról,
110
+ - słowniki i konfiguracje wielowierszowe,
111
+ - tabele administracyjne z inline edycją pól.
112
+
71
113
  ### `api.command`
72
114
 
73
115
  ```js
@@ -187,6 +229,8 @@ const articlePath = computed(() => path.blog.article({ article: unref(articleId)
187
229
  const [article] = await Promise.all([live(articlePath)])
188
230
  ```
189
231
 
232
+ `usePath()` wywołuj synchronicznie w `setup` i zapisz wynik w zmiennej `path`. W getterze `computed` buduj tylko `path.<service>.<view>(...)` — nie wywołuj ponownie `usePath()` ani aliasu `path()` w środku `computed` (brak aktywnej instancji → błąd przy `appContext`).
233
+
190
234
  Dla warunkowego ładowania (np. tylko gdy zalogowany), zwróć wartość falsy:
191
235
 
192
236
  ```js
@@ -211,6 +255,21 @@ path.blog.articles({})
211
255
 
212
256
  Dostęp: `article.authorProfile?.firstName`. Działa zarówno z `live()` jak i `RangeViewer`.
213
257
 
258
+ ### Guardrails Path DSL (`.with`, `$switch`)
259
+
260
+ - Callback `.with(item => ...)` traktuj jako deklarację query DSL, nie jako kod biznesowy wykonywany na realnym rekordzie.
261
+ - W callbacku `.with` nie rób side effects, `api.command`, ani imperatywnego `if/else` porównującego pola proxy.
262
+ - Dla warunkowego wyboru ścieżki używaj `item.field.$switch({...}).$bind('target')`.
263
+
264
+ ```js
265
+ path.accounting.settlementsByTransaction({ transactionType, transaction, range })
266
+ .with(settlement => settlement.subjectType.$switch({
267
+ invoice_CostInvoice: path.invoice.costInvoice({ costInvoice: settlement.subject }),
268
+ invoice_IncomeInvoice: path.invoice.incomeInvoice({ incomeInvoice: settlement.subject }),
269
+ hr_CivilContract: path.hr.civilContract({ civilContract: settlement.subject })
270
+ }).$bind('subjectDoc'))
271
+ ```
272
+
214
273
  ## Listy zakresowe z reaktywnymi filtrami
215
274
 
216
275
  Jeśli `pathFunction` dla listy zakresowej zależy od reaktywnych filtrów (np. miesiąc/status/szukaj), preferuj `ReactiveRangeViewer`.
@@ -232,6 +291,13 @@ Zasady:
232
291
  />
233
292
  ```
234
293
 
294
+ ## Guardrails dla kursora zakresu (`RangeViewer` / `rangeBuckets`)
295
+
296
+ - Dla list opartych o indeks backend powinien udostępniać widoki oparte o `sortedIndexRangePath`.
297
+ - Nigdy nie nadpisuj `range.gt/gte/lt/lte` we frontendowym `pathFunction`.
298
+ - Pole `range` zostaw jako kursor paginacji; filtry domenowe (`month`, `year`, `status`) przekazuj osobno.
299
+ - Jeśli pojawia się pokusa ręcznego przepisywania kursora, przenieś logikę do projektu indeksu po stronie backendu (ew. fallback `prefixRange`), nie do hacków we froncie.
300
+
235
301
  ## WorkingZone dla akcji asynchronicznych
236
302
 
237
303
  `ViewRoot` opakowuje każdą stronę w `<WorkingZone>`. Używaj `inject('workingZone')` dla akcji przycisków poza formularzami:
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: LiveChange service file structure — every service must be a directory with separate definition.js, index.js, etc.
3
- globs: server/**/*.js
3
+ globs: **/services/**/*.js, **/server/**/*.js, server/**/*.js
4
4
  alwaysApply: true
5
5
  ---
6
6
 
@@ -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:
@@ -79,6 +79,57 @@ definition.action({
79
79
  - use model paths (`Model.path`, `Model.rangePath`, `Model.sortedIndexRangePath`, `Model.indexObjectPath`)
80
80
  - use `...App.rangeProperties` + `App.extractRange(props)` for range views
81
81
 
82
+ ### Step 2a – RangeViewer/rangeBuckets compatibility
83
+
84
+ When a view is consumed by `RangeViewer` or `rangeBuckets`:
85
+
86
+ - prefer `Model.sortedIndexRangePath(...)` for index-backed list views,
87
+ - keep `App.extractRange(props)` as pagination cursor input,
88
+ - do not reinterpret `gt/gte/lt/lte` as domain filters.
89
+
90
+ Anti-patterns:
91
+
92
+ - using `indexRangePath` for frontend bucket pagination flow,
93
+ - injecting custom month/year bounds into cursor fields in frontend,
94
+ - rewriting cursor values in backend with unrelated filter semantics.
95
+
96
+ Preferred filtering strategy:
97
+
98
+ 1. design index prefix for frequent filters,
99
+ 2. use `App.utils.prefixRange` only as backend fallback,
100
+ 3. keep string min/max hacks as last resort.
101
+
102
+ ### Step 2b – Standalone indexes for union/equal sources
103
+
104
+ When index rows are built from multiple equal tables (union-like flow), do not force the index into one model definition.
105
+
106
+ Use `definition.index(...)` at service level (typically `indexes.js`) when:
107
+
108
+ - index combines rows from two or more source tables,
109
+ - source tables are peer entities (no natural single owner model),
110
+ - index is a projection layer for cross-table reads.
111
+
112
+ Example:
113
+
114
+ ```js
115
+ definition.index({
116
+ name: 'Urls',
117
+ function: async (input, output) => {
118
+ await input.table('url_Redirect').onChange((obj, oldObj) =>
119
+ output.change(obj && mapRedirect(obj), oldObj && mapRedirect(oldObj))
120
+ )
121
+ await input.table('url_Canonical').onChange((obj, oldObj) =>
122
+ output.change(obj && mapCanonical(obj), oldObj && mapCanonical(oldObj))
123
+ )
124
+ }
125
+ })
126
+ ```
127
+
128
+ Decision rule:
129
+
130
+ - model-local index -> `definition.model({ indexes: ... })`,
131
+ - union/peer-source index -> standalone `definition.index(...)` in `indexes.js`.
132
+
82
133
  ### Example: `daoPath` (preferred, DAO-backed)
83
134
 
84
135
  ```js
@@ -62,6 +62,25 @@ properties: {
62
62
  }
63
63
  ```
64
64
 
65
+ ## Step 2b – Relation arity rules (critical)
66
+
67
+ Treat arity on two levels:
68
+
69
+ - **Annotation arity**: can the annotation itself be a list of configs?
70
+ - **Parent tuple arity**: can one config point to multiple parents/dimensions?
71
+
72
+ | Relation | Annotation arity | Parent tuple arity |
73
+ |---|---|---|
74
+ | `propertyOf`, `itemOf`, `boundTo` | single config only | `what` can be one model or `[A, B, ...]` |
75
+ | `relatedTo` | single config or config list | each config uses `what` with one model or `[A, B, ...]` |
76
+ | `propertyOfAny`, `itemOfAny`, `boundToAny` | single config only | `to` can contain one or many names |
77
+ | `relatedToAny` | single config or config list | each config uses `to` with one or many names |
78
+
79
+ Guardrail:
80
+
81
+ - valid: `propertyOf: { what: [A, B] }`
82
+ - invalid: `propertyOf: [configA, configB]`
83
+
65
84
  ## Step 3 – Configure the relation
66
85
 
67
86
  ### `userItem`
@@ -110,7 +129,7 @@ so the relations/CRUD generator can treat it as a relation rather than a plain `
110
129
 
111
130
  Notes:
112
131
 
113
- - Usually you’ll have 1–2 parents, but the `propertyOf` list may contain **any number** of parent models (including 3+).
132
+ - Usually you’ll have 1–2 parents, but `what` may contain **any number** of parent models (including 3+).
114
133
  - If the entity is a relation, avoid adding manual `...Id` fields in `properties` just to represent the link — CRUD generators won’t treat it as a relation.
115
134
 
116
135
  Example:
@@ -124,10 +143,9 @@ definition.model({
124
143
  properties: {
125
144
  // optional extra fields
126
145
  },
127
- propertyOf: [
128
- { what: CostInvoice },
129
- { what: Contractor }
130
- ]
146
+ propertyOf: {
147
+ what: [CostInvoice, Contractor]
148
+ }
131
149
  })
132
150
  ```
133
151
 
@@ -38,6 +38,21 @@ const [article, comments] = await Promise.all([
38
38
  ])
39
39
  ```
40
40
 
41
+ - Call `usePath()` **once** at the top of `setup` (synchronously). Inside `computed`, use only the returned `path` object to build paths — **never** call `usePath()` or the legacy `path()` inside the getter (there is often no active component instance, which breaks `getCurrentInstance()` / `appContext`).
42
+
43
+ Wrong:
44
+
45
+ ```javascript
46
+ computed(() => usePath().blog.article({ article: id }))
47
+ ```
48
+
49
+ Right:
50
+
51
+ ```javascript
52
+ const path = usePath()
53
+ const articlePath = computed(() => path.blog.article({ article: id }))
54
+ ```
55
+
41
56
  In templates access `.value`:
42
57
 
43
58
  ```vue