@live-change/frontend-template 0.9.198 → 0.9.200

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 (71) hide show
  1. package/.claude/rules/live-change-backend-actions-views-triggers.md +246 -0
  2. package/.claude/rules/live-change-backend-architecture.md +126 -0
  3. package/.claude/rules/live-change-backend-event-sourcing.md +186 -0
  4. package/.claude/rules/live-change-backend-models-and-relations.md +260 -0
  5. package/.claude/rules/live-change-frontend-vue-primevue.md +317 -0
  6. package/.claude/rules/live-change-service-structure.md +89 -0
  7. package/.claude/settings.json +32 -0
  8. package/.claude/skills/create-skills-and-rules/SKILL.md +248 -0
  9. package/.claude/skills/create-skills-and-rules.md +196 -0
  10. package/.claude/skills/live-change-backend-change-triggers/SKILL.md +186 -0
  11. package/.claude/skills/live-change-design-actions-views-triggers/SKILL.md +462 -0
  12. package/.claude/skills/live-change-design-actions-views-triggers.md +190 -0
  13. package/.claude/skills/live-change-design-models-relations/SKILL.md +230 -0
  14. package/.claude/skills/live-change-design-models-relations.md +173 -0
  15. package/.claude/skills/live-change-design-service/SKILL.md +133 -0
  16. package/.claude/skills/live-change-design-service.md +132 -0
  17. package/.claude/skills/live-change-frontend-accessible-objects/SKILL.md +384 -0
  18. package/.claude/skills/live-change-frontend-accessible-objects.md +383 -0
  19. package/.claude/skills/live-change-frontend-action-buttons/SKILL.md +129 -0
  20. package/.claude/skills/live-change-frontend-action-buttons.md +128 -0
  21. package/.claude/skills/live-change-frontend-action-form/SKILL.md +149 -0
  22. package/.claude/skills/live-change-frontend-action-form.md +143 -0
  23. package/.claude/skills/live-change-frontend-analytics/SKILL.md +147 -0
  24. package/.claude/skills/live-change-frontend-analytics.md +146 -0
  25. package/.claude/skills/live-change-frontend-command-forms/SKILL.md +216 -0
  26. package/.claude/skills/live-change-frontend-command-forms.md +215 -0
  27. package/.claude/skills/live-change-frontend-data-views/SKILL.md +183 -0
  28. package/.claude/skills/live-change-frontend-data-views.md +182 -0
  29. package/.claude/skills/live-change-frontend-editor-form/SKILL.md +240 -0
  30. package/.claude/skills/live-change-frontend-editor-form.md +177 -0
  31. package/.claude/skills/live-change-frontend-locale-time/SKILL.md +172 -0
  32. package/.claude/skills/live-change-frontend-locale-time.md +171 -0
  33. package/.claude/skills/live-change-frontend-page-list-detail/SKILL.md +201 -0
  34. package/.claude/skills/live-change-frontend-page-list-detail.md +200 -0
  35. package/.claude/skills/live-change-frontend-range-list/SKILL.md +129 -0
  36. package/.claude/skills/live-change-frontend-range-list.md +128 -0
  37. package/.claude/skills/live-change-frontend-ssr-setup/SKILL.md +119 -0
  38. package/.claude/skills/live-change-frontend-ssr-setup.md +118 -0
  39. package/.cursor/rules/live-change-backend-actions-views-triggers.mdc +290 -0
  40. package/.cursor/rules/live-change-backend-architecture.mdc +131 -0
  41. package/.cursor/rules/live-change-backend-event-sourcing.mdc +185 -0
  42. package/.cursor/rules/live-change-backend-models-and-relations.mdc +256 -0
  43. package/.cursor/rules/live-change-frontend-vue-primevue.mdc +290 -0
  44. package/.cursor/rules/live-change-service-structure.mdc +107 -0
  45. package/.cursor/skills/create-skills-and-rules.md +248 -0
  46. package/.cursor/skills/live-change-backend-change-triggers.md +186 -0
  47. package/.cursor/skills/live-change-design-actions-views-triggers.md +296 -0
  48. package/.cursor/skills/live-change-design-models-relations.md +230 -0
  49. package/.cursor/skills/live-change-design-service.md +76 -0
  50. package/.cursor/skills/live-change-frontend-accessible-objects.md +384 -0
  51. package/.cursor/skills/live-change-frontend-action-buttons.md +129 -0
  52. package/.cursor/skills/live-change-frontend-action-form.md +149 -0
  53. package/.cursor/skills/live-change-frontend-analytics.md +147 -0
  54. package/.cursor/skills/live-change-frontend-command-forms.md +216 -0
  55. package/.cursor/skills/live-change-frontend-data-views.md +183 -0
  56. package/.cursor/skills/live-change-frontend-editor-form.md +240 -0
  57. package/.cursor/skills/live-change-frontend-locale-time.md +172 -0
  58. package/.cursor/skills/live-change-frontend-page-list-detail.md +201 -0
  59. package/.cursor/skills/live-change-frontend-range-list.md +129 -0
  60. package/.cursor/skills/live-change-frontend-ssr-setup.md +120 -0
  61. package/README.md +71 -0
  62. package/front/src/router.js +2 -1
  63. package/opencode.json +10 -0
  64. package/package.json +52 -50
  65. package/server/app.config.js +35 -0
  66. package/server/services.list.js +2 -0
  67. package/.nx/workspace-data/file-map.json +0 -195
  68. package/.nx/workspace-data/nx_files.nxt +0 -0
  69. package/.nx/workspace-data/project-graph.json +0 -8
  70. package/.nx/workspace-data/project-graph.lock +0 -0
  71. package/.nx/workspace-data/source-maps.json +0 -1
@@ -0,0 +1,290 @@
1
+ ---
2
+ description: Rules for implementing actions, views, and triggers in LiveChange services
3
+ globs: **/services/**/*.js
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # LiveChange backend – akcje, widoki, triggery
8
+
9
+ ## Akcje – ogólne zasady
10
+
11
+ - Akcje umieszczaj w plikach domenowych, razem z modelami, których dotyczą.
12
+ - Każda akcja powinna:
13
+ - jasno określać wejście (`properties`),
14
+ - wykonywać minimalną potrzebną walidację,
15
+ - używać indeksów zamiast pełnych skanów,
16
+ - zwracać sensowny wynik (ID obiektu, fragment stanu itp.).
17
+
18
+ ### Wzorzec prostej akcji
19
+
20
+ ```js
21
+ definition.action({
22
+ name: 'registerDeviceConnection',
23
+ properties: {
24
+ pairingKey: {
25
+ type: String
26
+ },
27
+ connectionType: {
28
+ type: String
29
+ },
30
+ capabilities: {
31
+ type: Array
32
+ }
33
+ },
34
+ async execute({ pairingKey, connectionType, capabilities }, { client, service }) {
35
+ const device = await Device.indexObjectGet('byPairingKey', { pairingKey })
36
+ if(!device) throw new Error('notFound')
37
+
38
+ const id = app.generateUid()
39
+ const sessionKey = app.generateUid() + app.generateUid()
40
+
41
+ await DeviceConnection.create({
42
+ id,
43
+ device: device.id,
44
+ connectionType,
45
+ capabilities,
46
+ sessionKey,
47
+ status: 'offline'
48
+ })
49
+
50
+ return { connectionId: id, sessionKey }
51
+ }
52
+ })
53
+ ```
54
+
55
+ ## Widoki – ogólne zasady
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.
60
+
61
+ ## Widoki – reguła: `get` + `observable` zawsze razem
62
+
63
+ Widok musi być dokładnie jednym z trzech wariantów:
64
+
65
+ - `daoPath` (preferowane) — framework wygeneruje zarówno `get`, jak i `observable`
66
+ - `get` + `observable` — oba wymagane, jeśli źródło danych jest zewnętrzne lub custom-reactive
67
+ - `fetch` — jednorazowy request/response (często `remote: true`), bez reaktywnego strumienia
68
+
69
+ Nigdy nie definiuj samego `get` bez `observable` ani samego `observable` bez `get`. To psuje użycie reaktywne i może kończyć się błędami w processorach, które owijają oba callbacki (np. access control).
70
+
71
+ ### Poprawnie: `daoPath` zamiast ręcznego `get`/`observable`
72
+
73
+ ```js
74
+ definition.view({
75
+ name: 'costInvoice',
76
+ properties: {
77
+ costInvoice: {
78
+ type: String
79
+ }
80
+ },
81
+ returns: { type: Object },
82
+ async daoPath({ costInvoice }) {
83
+ return CostInvoice.path(costInvoice)
84
+ }
85
+ })
86
+ ```
87
+
88
+ ### Poprawnie: `get` + `observable` razem (zewnętrzne źródło)
89
+
90
+ ```js
91
+ definition.view({
92
+ name: 'session',
93
+ properties: {},
94
+ returns: { type: Number },
95
+ async get(params, { client }) {
96
+ return onlineClient.get(['online', 'session', { ...params, session: client.session }])
97
+ },
98
+ async observable(params, { client }) {
99
+ return onlineClient.observable(
100
+ ['online', 'session', { ...params, session: client.session }],
101
+ ReactiveDao.ObservableValue
102
+ )
103
+ }
104
+ })
105
+ ```
106
+
107
+ ### Źle: samo `get` bez `observable`
108
+
109
+ ```js
110
+ definition.view({
111
+ name: 'brokenView',
112
+ properties: {
113
+ id: { type: String }
114
+ },
115
+ returns: { type: Object },
116
+ async get({ id }) {
117
+ return await SomeModel.get(id)
118
+ }
119
+ })
120
+ ```
121
+
122
+ ### Wzorzec widoku zakresowego
123
+
124
+ ```js
125
+ definition.view({
126
+ name: 'pendingCommands',
127
+ properties: {
128
+ connectionId: {
129
+ type: String
130
+ }
131
+ },
132
+ async get({ connectionId }, { client, service }) {
133
+ return BotCommand.indexRangeGet('byConnectionAndStatus', {
134
+ connection: connectionId,
135
+ status: 'pending'
136
+ })
137
+ }
138
+ })
139
+ ```
140
+
141
+ ## Triggery – online/offline i batchowanie
142
+
143
+ - Triggery są do reakcji na zdarzenia (np. zmiana stanu sesji, start serwera).
144
+ - Dwie podstawowe kategorie:
145
+ - triggery dla pojedynczych obiektów (np. połączenie online/offline),
146
+ - triggery „hurtowe” (np. ustawienie wszystkich na offline przy starcie).
147
+
148
+ ### Wzorzec triggerów online/offline
149
+
150
+ ```js
151
+ definition.trigger({
152
+ name: 'sessionDeviceConnectionOnline',
153
+ properties: {
154
+ connection: {
155
+ type: String
156
+ }
157
+ },
158
+ async execute({ connection }, { service }) {
159
+ await DeviceConnection.update(connection, {
160
+ status: 'online',
161
+ lastSeenAt: new Date()
162
+ })
163
+ }
164
+ })
165
+
166
+ definition.trigger({
167
+ name: 'sessionDeviceConnectionOffline',
168
+ properties: {
169
+ connection: {
170
+ type: String
171
+ }
172
+ },
173
+ async execute({ connection }, { service }) {
174
+ await DeviceConnection.update(connection, {
175
+ status: 'offline'
176
+ })
177
+ }
178
+ })
179
+ ```
180
+
181
+ ### Wzorzec batchowania odczytów (unikaj pełnych skanów)
182
+
183
+ - Przy triggerach, które mają przejść po wielu rekordach, **zawsze** używaj paginacji:
184
+ - stały `limit` (np. 32 lub 128),
185
+ - zakres po kluczu (`gt: lastId`),
186
+ - pętla aż do pustego batcha.
187
+
188
+ ```js
189
+ definition.trigger({
190
+ name: 'allOffline',
191
+ async execute({}, { service }) {
192
+ let last = ''
193
+ while(true) {
194
+ const connections = await DeviceConnection.rangeGet({
195
+ gt: last,
196
+ limit: 32
197
+ })
198
+ if(connections.length === 0) break
199
+
200
+ for(const conn of connections) {
201
+ await DeviceConnection.update(conn.id, {
202
+ status: 'offline'
203
+ })
204
+ }
205
+
206
+ last = connections[connections.length - 1].id
207
+ }
208
+ }
209
+ })
210
+ ```
211
+
212
+ ## Change triggers – reakcja na zmiany modeli
213
+
214
+ Modele z relacjami (`propertyOf`, `itemOf`, `userItem`, itp.) automatycznie odpalają change triggery przy każdym create/update/delete. Konwencja nazw: `{changeType}{ServiceName}_{ModelName}`:
215
+
216
+ - `changeSvc_Model` — odpala się przy każdej zmianie (rekomendowane, obsługuje wszystkie przypadki)
217
+ - `createSvc_Model` / `updateSvc_Model` / `deleteSvc_Model` — konkretne zdarzenia cyklu życia
218
+
219
+ Parametry: `{ objectType, object, identifiers, data, oldData, changeType }`.
220
+
221
+ ```js
222
+ // Reaguj na dowolną zmianę modelu Schedule z serwisu cron
223
+ definition.trigger({
224
+ name: 'changeCron_Schedule',
225
+ properties: {
226
+ object: { type: Schedule, validation: ['nonEmpty'] },
227
+ data: { type: Object },
228
+ oldData: { type: Object }
229
+ },
230
+ async execute({ object, data, oldData }, { triggerService }) {
231
+ if(oldData) { /* wyczyść stary stan */ }
232
+ if(data) { /* ustaw nowy stan */ }
233
+ }
234
+ })
235
+ ```
236
+
237
+ Sprawdzaj `data`/`oldData`: oba obecne = update, tylko `data` = create, tylko `oldData` = delete.
238
+
239
+ ## Wzorzec „pending + resolve” (asynchroniczny wynik)
240
+
241
+ - Używaj, gdy akcja w serwisie musi poczekać na wynik z zewnętrznego procesu (np. urządzenie, worker).
242
+ - Struktura:
243
+ 1. Akcja tworzy rekord „pending”.
244
+ 2. Akcja czeka na `Promise` powiązany z ID.
245
+ 3. Inna akcja/trigger aktualizuje rekord i rozwiązuje `Promise`.
246
+
247
+ Szkic:
248
+
249
+ ```js
250
+ // pendingCommands.js – singleton w pamięci
251
+ const pendingCommands = new Map()
252
+
253
+ export function waitForCommand(commandId, timeoutMs = 115000) {
254
+ return new Promise((resolve, reject) => {
255
+ const timer = setTimeout(() => {
256
+ pendingCommands.delete(commandId)
257
+ reject(new Error('timeout'))
258
+ }, timeoutMs)
259
+ pendingCommands.set(commandId, { resolve, reject, timer })
260
+ })
261
+ }
262
+
263
+ export function resolveCommand(commandId, result) {
264
+ const pending = pendingCommands.get(commandId)
265
+ if(pending) {
266
+ clearTimeout(pending.timer)
267
+ pendingCommands.delete(commandId)
268
+ pending.resolve(result)
269
+ }
270
+ }
271
+ ```
272
+
273
+ W akcji wywołującej:
274
+
275
+ ```js
276
+ await SomeCommand.create({ id, status: 'pending', ... })
277
+ const result = await waitForCommand(id)
278
+ return result
279
+ ```
280
+
281
+ W akcji raportującej:
282
+
283
+ ```js
284
+ await SomeCommand.update(id, {
285
+ status: 'completed',
286
+ result
287
+ })
288
+ resolveCommand(id, result)
289
+ ```
290
+
@@ -0,0 +1,131 @@
1
+ ---
2
+ description: Rules for LiveChange backend service architecture and directory structure
3
+ globs: **/services/**/*.js
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # LiveChange backend – architektura serwisów
8
+
9
+ ## Główna zasada
10
+
11
+ - Każdy serwis LiveChange **musi być katalogiem**, nie pojedynczym plikiem.
12
+ - Serwis ma czytelną, powtarzalną strukturę plików.
13
+
14
+ ## Struktura katalogu serwisu
15
+
16
+ ```
17
+ server/services/<serviceName>/
18
+ definition.js # createServiceDefinition + use – żadnych modeli/akcji
19
+ index.js # import definition, import plików domenowych, export definition
20
+ config.js # opcjonalnie – rozwiązywanie definition.config do plain object
21
+ <domain>.js # pliki domenowe: modele, widoki, akcje, triggery
22
+ ```
23
+
24
+ ### `definition.js`
25
+
26
+ - Importuj `app` z `@live-change/framework`.
27
+ - Twórz definicję serwisu **bez** modeli/akcji/widoków w tym pliku.
28
+ - Jeśli serwis używa relacji lub kontroli dostępu, **od razu** podłącz pluginy w `use`.
29
+
30
+ Przykład:
31
+
32
+ ```js
33
+ import { app } from '@live-change/framework'
34
+ import relationsPlugin from '@live-change/relations-plugin'
35
+ import accessControlService from '@live-change/access-control-service'
36
+
37
+ const definition = app.createServiceDefinition({
38
+ name: 'myService',
39
+ use: [relationsPlugin, accessControlService]
40
+ })
41
+
42
+ export default definition
43
+ ```
44
+
45
+ ### `index.js`
46
+
47
+ - Importuj `definition`.
48
+ - Importuj wszystkie pliki domenowe tylko po to, żeby wykonały się side-effecty (rejestracja modeli/akcji/widoków/triggerów).
49
+ - Eksportuj `definition` jako default.
50
+
51
+ ```js
52
+ import definition from './definition.js'
53
+
54
+ import './modelA.js'
55
+ import './modelB.js'
56
+ import './authenticator.js'
57
+
58
+ export default definition
59
+ ```
60
+
61
+ ### `config.js` (opcjonalnie)
62
+
63
+ - Czyta `definition.config` (ustawiane w `app.config.js`).
64
+ - Nadaje wartości domyślne i eksportuje zwykły obiekt.
65
+
66
+ ```js
67
+ import definition from './definition.js'
68
+
69
+ const {
70
+ someOption = 'default'
71
+ } = definition.config
72
+
73
+ export default { someOption }
74
+ ```
75
+
76
+ ## Rejestracja serwisu w projekcie
77
+
78
+ ### `services.list.js`
79
+
80
+ - Importuj serwis z jego `index.js` w katalogu, nie z płaskiego pliku.
81
+
82
+ ```js
83
+ import myService from './services/myService/index.js'
84
+
85
+ export default {
86
+ // ...
87
+ myService
88
+ }
89
+ ```
90
+
91
+ ### `app.config.js`
92
+
93
+ - Upewnij się, że nazwa serwisu w configu odpowiada nazwie z `createServiceDefinition`.
94
+ - Zachowaj **sensowną kolejność** serwisów:
95
+ - na początku serwisy bazowe, wspólne i pluginy,
96
+ - na końcu serwisy właściwe dla aplikacji, które z nich korzystają.
97
+
98
+ ```js
99
+ services: [
100
+ // serwisy bazowe / wspólne
101
+ { name: 'user' },
102
+ { name: 'session' },
103
+ { name: 'accessControl' },
104
+ // ...
105
+ // serwisy własne aplikacji
106
+ { name: 'myService' }
107
+ ]
108
+ ```
109
+
110
+ ## Podgląd serwisów komendą `describe`
111
+
112
+ Komenda CLI `describe` pokazuje co framework wygenerował z twoich definicji (modele, widoki, akcje, triggery, indeksy, eventy):
113
+
114
+ ```bash
115
+ # Przegląd wszystkich serwisów
116
+ node server/start.js describe
117
+
118
+ # Jeden serwis w YAML (z wygenerowanym kodem)
119
+ node server/start.js describe --service myService --output yaml
120
+
121
+ # Konkretna encja
122
+ node server/start.js describe --service myService --model MyModel --output yaml
123
+ ```
124
+
125
+ Szczególnie przydatne po użyciu relacji (`userItem`, `itemOf`, `propertyOf`) — `describe` pokaże wszystkie automatycznie wygenerowane widoki, akcje, triggery i indeksy.
126
+
127
+ ## Kiedy tworzyć nowy serwis
128
+
129
+ - Gdy masz wyraźnie wydzieloną domenę (np. urządzenia, płatności, powiadomienia).
130
+ - Gdy zestaw modeli/akcji/widoków ma własną konfigurację i zależności.
131
+ - Gdy logika nie mieści się sensownie w istniejącym serwisie bez mieszania odpowiedzialności.
@@ -0,0 +1,185 @@
1
+ ---
2
+ description: Event-sourcing data flow rules — emit events for DB writes, use triggerService for cross-service writes
3
+ globs: **/services/**/*.js
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # LiveChange backend – event-sourcing i przepływ danych
8
+
9
+ ## Jak płyną dane w LiveChange
10
+
11
+ LiveChange stosuje wzorzec event-sourcing:
12
+
13
+ 1. **Akcje i triggery** walidują dane i publikują eventy przez `emit()`.
14
+ 2. **Eventy** (`definition.event()`) wykonują faktyczne zapisy do bazy (`Model.create`, `Model.update`, `Model.delete`).
15
+ 3. Dla modeli z **relacjami** (`userItem`, `itemOf`, `propertyOf`, itp.) relations plugin auto-generuje eventy i triggery CRUD — używaj ich przez `triggerService()`.
16
+ 4. Dla **zapisów między serwisami** zawsze używaj `triggerService()` — `foreignModel` jest tylko do odczytu.
17
+
18
+ ```
19
+ Akcja/Trigger ──emit()──▶ Event handler ──▶ Model.create/update/delete
20
+
21
+ └──triggerService()──▶ Trigger innego serwisu ──emit()──▶ ...
22
+ ```
23
+
24
+ ## Reguła 1: Nie używaj Model.create/update/delete bezpośrednio w akcjach i triggerach
25
+
26
+ Akcje i triggery **nie powinny** wywoływać `Model.create()`, `Model.update()` ani `Model.delete()` bezpośrednio. Zamiast tego:
27
+
28
+ - **Jeśli model ma relacje** (auto-generowane CRUD) → użyj `triggerService()` do wywołania triggera relacji (np. `serviceName_createModelName`, `serviceName_updateModelName`, `serviceName_setModelName`).
29
+ - **Jeśli nie ma triggera relacji** → użyj `emit()` do opublikowania eventu, a potem zdefiniuj `definition.event()`, który wykona zapis.
30
+
31
+ ### Poprawnie: emit z akcji
32
+
33
+ ```js
34
+ definition.action({
35
+ name: 'createImage',
36
+ properties: { /* ... */ },
37
+ waitForEvents: true,
38
+ async execute({ image, name, width, height }, { client, service }, emit) {
39
+ const id = image || app.generateUid()
40
+ // walidacja, logika biznesowa...
41
+ emit({
42
+ type: 'ImageCreated',
43
+ image: id,
44
+ data: { name, width, height }
45
+ })
46
+ return id
47
+ }
48
+ })
49
+
50
+ definition.event({
51
+ name: 'ImageCreated',
52
+ async execute({ image, data }) {
53
+ await Image.create({ ...data, id: image })
54
+ }
55
+ })
56
+ ```
57
+
58
+ ### Poprawnie: triggerService dla triggerów relacji
59
+
60
+ ```js
61
+ definition.action({
62
+ name: 'giveCard',
63
+ properties: { /* ... */ },
64
+ async execute({ receiverType, receiver }, { client, triggerService }, emit) {
65
+ // Użyj auto-generowanego triggera z relations plugin
66
+ await triggerService({
67
+ service: definition.name,
68
+ type: 'businessCard_setReceivedCard',
69
+ }, {
70
+ sessionOrUserType: receiverType,
71
+ sessionOrUser: receiver,
72
+ // ...
73
+ })
74
+ }
75
+ })
76
+ ```
77
+
78
+ ### Źle: bezpośredni zapis w akcji
79
+
80
+ ```js
81
+ // ❌ NIE RÓB TEGO
82
+ definition.action({
83
+ name: 'createSomething',
84
+ async execute({ name }, { client }, emit) {
85
+ const id = app.generateUid()
86
+ await Something.create({ id, name }) // ❌ bezpośredni zapis w akcji
87
+ return id
88
+ }
89
+ })
90
+ ```
91
+
92
+ ## Reguła 2: Eventy mogą modyfikować tylko modele tego samego serwisu
93
+
94
+ Handlery eventów (`definition.event()`) mogą zapisywać tylko do modeli zdefiniowanych w **tym samym serwisie**. Nie próbuj zapisywać do `foreignModel` — jest tylko do odczytu (`.get()`, `.indexObjectGet()`, `.indexRangeGet()`, ale nie `.create()`, `.update()`, `.delete()`).
95
+
96
+ Do zapisów między serwisami używaj `triggerService()` z akcji lub triggera:
97
+
98
+ ```js
99
+ // ✅ Poprawnie: zapis między serwisami przez triggerService
100
+ definition.trigger({
101
+ name: 'chargeCollected_billing_TopUp',
102
+ async execute(props, { triggerService }, emit) {
103
+ // Zapis do innego serwisu przez jego zadeklarowany trigger
104
+ await triggerService({
105
+ service: 'balance',
106
+ type: 'balance_setOrUpdateBalance',
107
+ }, { ownerType: 'billing_Billing', owner: props.cause })
108
+
109
+ // Zapis do własnego serwisu przez triggerService (trigger relacji)
110
+ await triggerService({
111
+ service: definition.name,
112
+ type: 'billing_updateTopUp'
113
+ }, { topUp: props.cause, state: 'paid' })
114
+ }
115
+ })
116
+ ```
117
+
118
+ ```js
119
+ // ❌ NIE RÓB TEGO — foreignModel jest tylko do odczytu
120
+ const ExternalModel = definition.foreignModel('otherService', 'SomeModel')
121
+
122
+ definition.event({
123
+ name: 'SomethingHappened',
124
+ async execute({ id }) {
125
+ await ExternalModel.update(id, { status: 'done' }) // ❌ nie zadziała
126
+ }
127
+ })
128
+ ```
129
+
130
+ ## `waitForEvents: true`
131
+
132
+ Gdy akcja lub trigger emituje eventy i musi poczekać na ich przetworzenie przed zwróceniem wyniku, ustaw `waitForEvents: true`:
133
+
134
+ ```js
135
+ definition.action({
136
+ name: 'createNotification',
137
+ waitForEvents: true,
138
+ async execute({ message }, { client }, emit) {
139
+ const id = app.generateUid()
140
+ emit({
141
+ type: 'created',
142
+ notification: id,
143
+ data: { message, sessionOrUserType: 'user_User', sessionOrUser: client.user }
144
+ })
145
+ return id // event jest przetworzony zanim to się zwróci
146
+ }
147
+ })
148
+ ```
149
+
150
+ Bez `waitForEvents: true` akcja zwraca wynik natychmiast, a eventy są przetwarzane asynchronicznie.
151
+
152
+ ## Triggery relacji (auto-generowane)
153
+
154
+ Gdy model ma relacje (`userItem`, `itemOf`, `propertyOf`, itp.), relations plugin auto-generuje triggery CRUD. Użyj `describe`, żeby je odkryć:
155
+
156
+ ```bash
157
+ node server/start.js describe --service myService --output yaml
158
+ ```
159
+
160
+ Typowe auto-generowane triggery:
161
+ - `serviceName_createModelName` — tworzenie rekordu
162
+ - `serviceName_updateModelName` — aktualizacja rekordu
163
+ - `serviceName_deleteModelName` — usuwanie rekordu
164
+ - `serviceName_setModelName` — upsert (utwórz lub nadpisz)
165
+ - `serviceName_setOrUpdateModelName` — ustaw jeśli nie istnieje, zaktualizuj jeśli istnieje
166
+
167
+ Wywołuj je przez `triggerService()`:
168
+
169
+ ```js
170
+ await triggerService({
171
+ service: 'myService',
172
+ type: 'myService_createMyModel'
173
+ }, {
174
+ // właściwości pasujące do pól modelu
175
+ })
176
+ ```
177
+
178
+ ## Podsumowanie
179
+
180
+ | Gdzie | Może wywoływać Model.create/update/delete? | Jak zmieniać dane |
181
+ |---|---|---|
182
+ | `definition.event()` | ✅ Tak (tylko modele tego samego serwisu) | Bezpośredni zapis |
183
+ | `definition.action()` | ❌ Nie | `emit()` lub `triggerService()` |
184
+ | `definition.trigger()` | ❌ Nie | `emit()` lub `triggerService()` |
185
+ | Między serwisami | ❌ Nigdy przez foreignModel | `triggerService()` do docelowego serwisu |