@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.
- package/.claude/rules/live-change-backend-actions-views-triggers.md +246 -0
- package/.claude/rules/live-change-backend-architecture.md +126 -0
- package/.claude/rules/live-change-backend-event-sourcing.md +186 -0
- package/.claude/rules/live-change-backend-models-and-relations.md +260 -0
- package/.claude/rules/live-change-frontend-vue-primevue.md +317 -0
- package/.claude/rules/live-change-service-structure.md +89 -0
- package/.claude/settings.json +32 -0
- package/.claude/skills/create-skills-and-rules/SKILL.md +248 -0
- package/.claude/skills/create-skills-and-rules.md +196 -0
- package/.claude/skills/live-change-backend-change-triggers/SKILL.md +186 -0
- package/.claude/skills/live-change-design-actions-views-triggers/SKILL.md +462 -0
- package/.claude/skills/live-change-design-actions-views-triggers.md +190 -0
- package/.claude/skills/live-change-design-models-relations/SKILL.md +230 -0
- package/.claude/skills/live-change-design-models-relations.md +173 -0
- package/.claude/skills/live-change-design-service/SKILL.md +133 -0
- package/.claude/skills/live-change-design-service.md +132 -0
- package/.claude/skills/live-change-frontend-accessible-objects/SKILL.md +384 -0
- package/.claude/skills/live-change-frontend-accessible-objects.md +383 -0
- package/.claude/skills/live-change-frontend-action-buttons/SKILL.md +129 -0
- package/.claude/skills/live-change-frontend-action-buttons.md +128 -0
- package/.claude/skills/live-change-frontend-action-form/SKILL.md +149 -0
- package/.claude/skills/live-change-frontend-action-form.md +143 -0
- package/.claude/skills/live-change-frontend-analytics/SKILL.md +147 -0
- package/.claude/skills/live-change-frontend-analytics.md +146 -0
- package/.claude/skills/live-change-frontend-command-forms/SKILL.md +216 -0
- package/.claude/skills/live-change-frontend-command-forms.md +215 -0
- package/.claude/skills/live-change-frontend-data-views/SKILL.md +183 -0
- package/.claude/skills/live-change-frontend-data-views.md +182 -0
- package/.claude/skills/live-change-frontend-editor-form/SKILL.md +240 -0
- package/.claude/skills/live-change-frontend-editor-form.md +177 -0
- package/.claude/skills/live-change-frontend-locale-time/SKILL.md +172 -0
- package/.claude/skills/live-change-frontend-locale-time.md +171 -0
- package/.claude/skills/live-change-frontend-page-list-detail/SKILL.md +201 -0
- package/.claude/skills/live-change-frontend-page-list-detail.md +200 -0
- package/.claude/skills/live-change-frontend-range-list/SKILL.md +129 -0
- package/.claude/skills/live-change-frontend-range-list.md +128 -0
- package/.claude/skills/live-change-frontend-ssr-setup/SKILL.md +119 -0
- package/.claude/skills/live-change-frontend-ssr-setup.md +118 -0
- package/.cursor/rules/live-change-backend-actions-views-triggers.mdc +290 -0
- package/.cursor/rules/live-change-backend-architecture.mdc +131 -0
- package/.cursor/rules/live-change-backend-event-sourcing.mdc +185 -0
- package/.cursor/rules/live-change-backend-models-and-relations.mdc +256 -0
- package/.cursor/rules/live-change-frontend-vue-primevue.mdc +290 -0
- package/.cursor/rules/live-change-service-structure.mdc +107 -0
- package/.cursor/skills/create-skills-and-rules.md +248 -0
- package/.cursor/skills/live-change-backend-change-triggers.md +186 -0
- package/.cursor/skills/live-change-design-actions-views-triggers.md +296 -0
- package/.cursor/skills/live-change-design-models-relations.md +230 -0
- package/.cursor/skills/live-change-design-service.md +76 -0
- package/.cursor/skills/live-change-frontend-accessible-objects.md +384 -0
- package/.cursor/skills/live-change-frontend-action-buttons.md +129 -0
- package/.cursor/skills/live-change-frontend-action-form.md +149 -0
- package/.cursor/skills/live-change-frontend-analytics.md +147 -0
- package/.cursor/skills/live-change-frontend-command-forms.md +216 -0
- package/.cursor/skills/live-change-frontend-data-views.md +183 -0
- package/.cursor/skills/live-change-frontend-editor-form.md +240 -0
- package/.cursor/skills/live-change-frontend-locale-time.md +172 -0
- package/.cursor/skills/live-change-frontend-page-list-detail.md +201 -0
- package/.cursor/skills/live-change-frontend-range-list.md +129 -0
- package/.cursor/skills/live-change-frontend-ssr-setup.md +120 -0
- package/README.md +71 -0
- package/front/src/router.js +2 -1
- package/opencode.json +10 -0
- package/package.json +52 -50
- package/server/app.config.js +35 -0
- package/server/services.list.js +2 -0
- package/.nx/workspace-data/file-map.json +0 -195
- package/.nx/workspace-data/nx_files.nxt +0 -0
- package/.nx/workspace-data/project-graph.json +0 -8
- package/.nx/workspace-data/project-graph.lock +0 -0
- 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 |
|