@live-change/frontend-template 0.9.197 → 0.9.199
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 +184 -0
- package/.claude/rules/live-change-backend-architecture.md +126 -0
- package/.claude/rules/live-change-backend-models-and-relations.md +188 -0
- package/.claude/rules/live-change-frontend-vue-primevue.md +291 -0
- package/.claude/rules/live-change-service-structure.md +89 -0
- package/.claude/skills/create-skills-and-rules.md +196 -0
- package/.claude/skills/live-change-design-actions-views-triggers.md +190 -0
- package/.claude/skills/live-change-design-models-relations.md +173 -0
- package/.claude/skills/live-change-design-service.md +132 -0
- package/.claude/skills/live-change-frontend-action-buttons.md +128 -0
- package/.claude/skills/live-change-frontend-action-form.md +143 -0
- package/.claude/skills/live-change-frontend-analytics.md +146 -0
- package/.claude/skills/live-change-frontend-command-forms.md +215 -0
- package/.claude/skills/live-change-frontend-data-views.md +182 -0
- package/.claude/skills/live-change-frontend-editor-form.md +177 -0
- package/.claude/skills/live-change-frontend-locale-time.md +171 -0
- package/.claude/skills/live-change-frontend-page-list-detail.md +200 -0
- package/.claude/skills/live-change-frontend-range-list.md +128 -0
- package/.claude/skills/live-change-frontend-ssr-setup.md +118 -0
- package/.cursor/rules/live-change-backend-actions-views-triggers.mdc +202 -0
- package/.cursor/rules/live-change-backend-architecture.mdc +131 -0
- package/.cursor/rules/live-change-backend-models-and-relations.mdc +194 -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/live-change-design-actions-views-triggers.md +197 -0
- package/.cursor/skills/live-change-design-models-relations.md +168 -0
- package/.cursor/skills/live-change-design-service.md +75 -0
- package/.cursor/skills/live-change-frontend-action-buttons.md +128 -0
- package/.cursor/skills/live-change-frontend-action-form.md +143 -0
- package/.cursor/skills/live-change-frontend-analytics.md +146 -0
- package/.cursor/skills/live-change-frontend-command-forms.md +215 -0
- package/.cursor/skills/live-change-frontend-data-views.md +182 -0
- package/.cursor/skills/live-change-frontend-editor-form.md +177 -0
- package/.cursor/skills/live-change-frontend-locale-time.md +171 -0
- package/.cursor/skills/live-change-frontend-page-list-detail.md +200 -0
- package/.cursor/skills/live-change-frontend-range-list.md +128 -0
- package/.cursor/skills/live-change-frontend-ssr-setup.md +119 -0
- package/README.md +71 -0
- package/package.json +50 -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 Vue 3, PrimeVue, Tailwind frontend development on LiveChange
|
|
3
|
+
globs: **/front/src/**/*.{vue,js,ts}
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Frontend na live-change-stack – Vue 3 + PrimeVue + Tailwind
|
|
8
|
+
|
|
9
|
+
## Stack
|
|
10
|
+
|
|
11
|
+
- Vue 3 + TypeScript
|
|
12
|
+
- PrimeVue 4 – główna biblioteka komponentów UI
|
|
13
|
+
- Tailwind CSS – podstawowe stylowanie (unikaj zbędnego custom CSS)
|
|
14
|
+
- vite-plugin-pages – file-based routing w `src/pages/`
|
|
15
|
+
- @live-change/vue3-ssr – integracja z backendem LiveChange
|
|
16
|
+
|
|
17
|
+
## Pobieranie danych – `live` + `Promise.all`
|
|
18
|
+
|
|
19
|
+
- **Nie** używaj `ref(null)` + `onMounted` do ładowania danych.
|
|
20
|
+
- Dane ładuj w `setup`/`script setup` przez `await Promise.all(...)` na `live(path()....)`.
|
|
21
|
+
- Rodzic powinien owijać stronę w `<Suspense>` (w projektach live-change robi to zazwyczaj `ViewRoot` globalnie).
|
|
22
|
+
|
|
23
|
+
Przykład:
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
import { path, live, api as useApi } from '@live-change/vue3-ssr'
|
|
27
|
+
|
|
28
|
+
const api = useApi()
|
|
29
|
+
|
|
30
|
+
const [devices] = await Promise.all([
|
|
31
|
+
live(path().deviceManager.myUserDevices({}))
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
const [device, connections] = await Promise.all([
|
|
35
|
+
live(path().deviceManager.myUserDevice({ device: deviceId })),
|
|
36
|
+
live(path().deviceManager.deviceOwnedDeviceConnections({ device: deviceId }))
|
|
37
|
+
])
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Dostęp w template:
|
|
41
|
+
|
|
42
|
+
```vue
|
|
43
|
+
<template>
|
|
44
|
+
<div v-if="device.value">
|
|
45
|
+
{{ device.value.name }}
|
|
46
|
+
</div>
|
|
47
|
+
</template>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Komendy i formularze – wybór wzorca
|
|
51
|
+
|
|
52
|
+
Są 4 sposoby wywoływania akcji backendowych. Używaj właściwego:
|
|
53
|
+
|
|
54
|
+
| Wzorzec | Kiedy używać |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `editorData` | **Edycja rekordów modelu** (create/update). Drafty, walidacja, `AutoField`. Ustawienia, edytory, profile. |
|
|
57
|
+
| `actionData` | **Jednorazowe formularze akcji** (nie CRUD). Submit → done. Publikacja, zaproszenie, import. |
|
|
58
|
+
| `api.command` | **Pojedynczy przycisk lub wywołanie z kodu** (bez pól formularza). Usuwanie, toggle, akcje z kodu. |
|
|
59
|
+
| `<command-form>` | **Unikaj.** Legacy, tylko trywialne prototypy. Preferuj `editorData` lub `actionData`. |
|
|
60
|
+
|
|
61
|
+
Schemat decyzji:
|
|
62
|
+
|
|
63
|
+
1. Użytkownik wypełnia pola formularza? → **Nie**: użyj `api.command` (owijaj w `workingZone.addPromise` dla przycisków).
|
|
64
|
+
2. Edytuje rekord modelu (create/update)? → **Tak**: użyj `editorData`. **Nie**: użyj `actionData`.
|
|
65
|
+
3. `<command-form>` tylko do najprostszych jednorazowych przypadków.
|
|
66
|
+
|
|
67
|
+
### `api.command`
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
await api.command(['deviceManager', 'createMyUserDevice'], {
|
|
71
|
+
name: 'Moje urządzenie'
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
await api.command(['deviceManager', 'deleteMyUserDevice'], {
|
|
75
|
+
device: id
|
|
76
|
+
})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Format komendy:
|
|
80
|
+
|
|
81
|
+
- tablica `[serviceName, actionName]`,
|
|
82
|
+
- payload jako zwykły obiekt.
|
|
83
|
+
|
|
84
|
+
## Routing – blok `<route>` i meta `signedIn`
|
|
85
|
+
|
|
86
|
+
- Każda strona w `src/pages/` może mieć blok `<route>` z metadanymi.
|
|
87
|
+
- Używaj `meta.signedIn` do oznaczania stron wymagających logowania.
|
|
88
|
+
|
|
89
|
+
```vue
|
|
90
|
+
<route>
|
|
91
|
+
{ "name": "devices", "meta": { "signedIn": true } }
|
|
92
|
+
</route>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Dynamiczne ścieżki:
|
|
96
|
+
|
|
97
|
+
- plik `[id].vue` odpowiada ścieżce `/devices/:id`.
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
import { useRoute } from 'vue-router'
|
|
101
|
+
|
|
102
|
+
const route = useRoute()
|
|
103
|
+
const deviceId = route.params.id
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Confirm + Toast – wzorzec dla akcji destrukcyjnych
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
import { useConfirm } from 'primevue/useconfirm'
|
|
110
|
+
import { useToast } from 'primevue/usetoast'
|
|
111
|
+
|
|
112
|
+
const confirm = useConfirm()
|
|
113
|
+
const toast = useToast()
|
|
114
|
+
|
|
115
|
+
function deleteDevice(id) {
|
|
116
|
+
confirm.require({
|
|
117
|
+
message: 'Czy na pewno chcesz usunąć to urządzenie?',
|
|
118
|
+
header: 'Potwierdzenie',
|
|
119
|
+
icon: 'pi pi-exclamation-triangle',
|
|
120
|
+
accept: async () => {
|
|
121
|
+
await api.command(['deviceManager', 'deleteMyUserDevice'], { device: id })
|
|
122
|
+
toast.add({
|
|
123
|
+
severity: 'success',
|
|
124
|
+
summary: 'Usunięto',
|
|
125
|
+
life: 2000
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Wzorce UI – lista i szczegóły
|
|
133
|
+
|
|
134
|
+
### Lista
|
|
135
|
+
|
|
136
|
+
```vue
|
|
137
|
+
<template>
|
|
138
|
+
<div class="container mx-auto p-4">
|
|
139
|
+
<div class="flex items-center justify-between mb-6">
|
|
140
|
+
<h1 class="text-2xl font-bold">Urządzenia</h1>
|
|
141
|
+
<Button label="Dodaj" icon="pi pi-plus" @click="openDialog" />
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<Card v-if="devices.value?.length === 0">
|
|
145
|
+
<template #content>
|
|
146
|
+
<p class="text-center text-gray-500">
|
|
147
|
+
Brak urządzeń
|
|
148
|
+
</p>
|
|
149
|
+
</template>
|
|
150
|
+
</Card>
|
|
151
|
+
|
|
152
|
+
<div class="grid gap-4">
|
|
153
|
+
<Card v-for="device in devices.value" :key="device.id">
|
|
154
|
+
<template #content>
|
|
155
|
+
<!-- zawartość -->
|
|
156
|
+
</template>
|
|
157
|
+
</Card>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</template>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Tag statusu
|
|
164
|
+
|
|
165
|
+
```vue
|
|
166
|
+
<Tag
|
|
167
|
+
:value="conn.status"
|
|
168
|
+
:severity="conn.status === 'online' ? 'success' : 'secondary'"
|
|
169
|
+
/>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Computed paths – reaktywne parametry
|
|
173
|
+
|
|
174
|
+
Gdy ścieżki zależą od reaktywnych wartości (route params, props), owijaj je w `computed()`:
|
|
175
|
+
|
|
176
|
+
```js
|
|
177
|
+
import { computed, unref } from 'vue'
|
|
178
|
+
import { usePath, live } from '@live-change/vue3-ssr'
|
|
179
|
+
|
|
180
|
+
const path = usePath()
|
|
181
|
+
|
|
182
|
+
const articlePath = computed(() => path.blog.article({ article: unref(articleId) }))
|
|
183
|
+
const [article] = await Promise.all([live(articlePath)])
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Dla warunkowego ładowania (np. tylko gdy zalogowany), zwróć wartość falsy:
|
|
187
|
+
|
|
188
|
+
```js
|
|
189
|
+
import { useClient } from '@live-change/vue3-ssr'
|
|
190
|
+
const client = useClient()
|
|
191
|
+
|
|
192
|
+
const myDataPath = computed(() => client.value.user && path.blog.myArticles({}))
|
|
193
|
+
const [myData] = await Promise.all([live(myDataPath)])
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Powiązane dane – `.with()`
|
|
197
|
+
|
|
198
|
+
Dołączaj powiązane obiekty do elementów w jednym reaktywnym zapytaniu:
|
|
199
|
+
|
|
200
|
+
```js
|
|
201
|
+
path.blog.articles({})
|
|
202
|
+
.with(article => path.userIdentification.identification({
|
|
203
|
+
sessionOrUserType: article.authorType,
|
|
204
|
+
sessionOrUser: article.author
|
|
205
|
+
}).bind('authorProfile'))
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Dostęp: `article.authorProfile?.firstName`. Działa zarówno z `live()` jak i `RangeViewer`.
|
|
209
|
+
|
|
210
|
+
## WorkingZone dla akcji asynchronicznych
|
|
211
|
+
|
|
212
|
+
`ViewRoot` opakowuje każdą stronę w `<WorkingZone>`. Używaj `inject('workingZone')` dla akcji przycisków poza formularzami:
|
|
213
|
+
|
|
214
|
+
```js
|
|
215
|
+
import { inject } from 'vue'
|
|
216
|
+
const workingZone = inject('workingZone')
|
|
217
|
+
|
|
218
|
+
function doAction() {
|
|
219
|
+
workingZone.addPromise('actionName', (async () => {
|
|
220
|
+
await actions.blog.publishArticle({ article: id })
|
|
221
|
+
toast.add({ severity: 'success', summary: 'Opublikowano', life: 2000 })
|
|
222
|
+
})())
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Aktywuje globalny spinner/blur na czas trwania promisy.
|
|
227
|
+
|
|
228
|
+
## Kontrola dostępu z `useClient`
|
|
229
|
+
|
|
230
|
+
```js
|
|
231
|
+
import { useClient } from '@live-change/vue3-ssr'
|
|
232
|
+
const client = useClient()
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
- `client.value.user` – truthy gdy zalogowany
|
|
236
|
+
- `client.value.roles` – tablica ról (np. `['admin', 'owner']`)
|
|
237
|
+
|
|
238
|
+
W template:
|
|
239
|
+
|
|
240
|
+
```vue
|
|
241
|
+
<Button v-if="client.roles.includes('admin')" label="Admin" />
|
|
242
|
+
<div v-if="!client.user">Zaloguj się</div>
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Locale i czas
|
|
246
|
+
|
|
247
|
+
- Wywołaj `useLocale().captureLocale()` w `App.vue` żeby zapisać locale przeglądarki do backendu.
|
|
248
|
+
- Używaj `locale.localTime(date)` z `d()` z vue-i18n do wyświetlania dat bezpiecznego dla SSR.
|
|
249
|
+
- `currentTime` z `@live-change/frontend-base` to reaktywny ref który tykuje co 500ms.
|
|
250
|
+
- `useTimeSynchronization()` z `@live-change/vue3-ssr` koryguje różnicę zegarów – używaj gdy timing jest krytyczny (odliczanie, eventy real-time).
|
|
251
|
+
|
|
252
|
+
## Analityka
|
|
253
|
+
|
|
254
|
+
Używaj `analytics` z `@live-change/vue3-components`:
|
|
255
|
+
|
|
256
|
+
```js
|
|
257
|
+
import { analytics } from '@live-change/vue3-components'
|
|
258
|
+
analytics.emit('article:published', { articleId: id })
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Podpinaj providery (PostHog, GA4) w osobnym pliku importowanym z `App.vue`.
|
|
262
|
+
|
|
263
|
+
## SSR i konfiguracja frontendu
|
|
264
|
+
|
|
265
|
+
- Utrzymuj standardowy układ entry points:
|
|
266
|
+
|
|
267
|
+
```js
|
|
268
|
+
// entry-client.js
|
|
269
|
+
import { clientEntry } from '@live-change/frontend-base/client-entry.js'
|
|
270
|
+
export default clientEntry(App, createRouter, config)
|
|
271
|
+
|
|
272
|
+
// entry-server.js
|
|
273
|
+
import { serverEntry, sitemapEntry } from '@live-change/frontend-base/server-entry.js'
|
|
274
|
+
export const render = serverEntry(App, createRouter, config)
|
|
275
|
+
export const sitemap = sitemapEntry(App, createRouter, routerSitemap, config)
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
- Konfigurację PrimeVue trzymaj w jednym miejscu (`config.js`) i używaj `definePreset` do nadpisania motywu.
|
|
279
|
+
|
|
280
|
+
## Odkrywanie widoków i akcji z `describe`
|
|
281
|
+
|
|
282
|
+
Komenda CLI `describe` pozwala znaleźć dostępne widoki (do `live()`) i akcje (do `api.command` / `editorData` / `actionData`):
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
node server/start.js describe --service blog
|
|
286
|
+
node server/start.js describe --service blog --view articlesByCreatedAt --output yaml
|
|
287
|
+
node server/start.js describe --service blog --action createMyUserArticle --output yaml
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
To najszybszy sposób na odkrycie jakie ścieżki i akcje są dostępne, w tym te wygenerowane automatycznie przez relacje.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: LiveChange service file structure — every service must be a directory with separate definition.js, index.js, etc.
|
|
3
|
+
globs: server/**/*.js
|
|
4
|
+
alwaysApply: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# LiveChange Service Structure
|
|
8
|
+
|
|
9
|
+
Every LiveChange service **must** be a directory, not a single file.
|
|
10
|
+
|
|
11
|
+
## Required structure
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
server/<serviceName>/
|
|
15
|
+
definition.js # creates app.createServiceDefinition({ name }) – nothing else
|
|
16
|
+
index.js # imports definition, imports all domain files, exports definition
|
|
17
|
+
config.js # required – resolves definition.config, sets definition.clientConfig, exports plain config object
|
|
18
|
+
<domain>.js # one file per domain area (models, views, actions, triggers)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## definition.js
|
|
22
|
+
|
|
23
|
+
Only creates and exports the definition. No models, no actions here.
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
import App from '@live-change/framework'
|
|
27
|
+
const app = App.app()
|
|
28
|
+
|
|
29
|
+
const definition = app.createServiceDefinition({ name: 'myService' })
|
|
30
|
+
|
|
31
|
+
export default definition
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## index.js
|
|
35
|
+
|
|
36
|
+
Imports definition and all domain files (side-effect imports), then re-exports definition.
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
import App from '@live-change/framework'
|
|
40
|
+
const app = App.app()
|
|
41
|
+
|
|
42
|
+
import definition from './definition.js'
|
|
43
|
+
|
|
44
|
+
import './authenticator.js'
|
|
45
|
+
import './myModel.js'
|
|
46
|
+
import './otherModel.js'
|
|
47
|
+
|
|
48
|
+
export default definition
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## config.js (required)
|
|
52
|
+
|
|
53
|
+
Reads `definition.config` (passed from `app.config.js`), resolves defaults, sets `definition.clientConfig` (for frontend),
|
|
54
|
+
and exports a plain config object used by other domain files.
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
import definition from './definition.js'
|
|
58
|
+
|
|
59
|
+
const {
|
|
60
|
+
defaultAccess = {
|
|
61
|
+
writeAccessControl: ['owner', 'admin'],
|
|
62
|
+
readAllAccess: ['admin']
|
|
63
|
+
}
|
|
64
|
+
} = definition.config || {}
|
|
65
|
+
|
|
66
|
+
definition.clientConfig = {
|
|
67
|
+
// values exposed to frontend, if needed
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default { defaultAccess }
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Domain files (e.g. myModel.js)
|
|
74
|
+
|
|
75
|
+
Each file imports `definition` (and `config` if needed) and registers models/views/actions/triggers.
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
import App from '@live-change/framework'
|
|
79
|
+
const app = App.app()
|
|
80
|
+
|
|
81
|
+
import definition from './definition.js'
|
|
82
|
+
import config from './config.js'
|
|
83
|
+
|
|
84
|
+
export const MyModel = definition.model({ name: 'MyModel', ... })
|
|
85
|
+
|
|
86
|
+
definition.view({ name: 'myList', ... })
|
|
87
|
+
definition.action({ name: 'doThing', ... })
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Rule of thumb:
|
|
91
|
+
|
|
92
|
+
- Domain files should read configuration from `config` (exported from `config.js`), not directly from `definition.config`.
|
|
93
|
+
|
|
94
|
+
## services.list.js import
|
|
95
|
+
|
|
96
|
+
Always import from the directory index, not a flat file:
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
import myService from './myService/index.js' // ✓
|
|
100
|
+
import myService from './myService.js' // ✗
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Why
|
|
104
|
+
|
|
105
|
+
A flat single-file service makes it hard to add new domain files (auth, models, triggers, etc.)
|
|
106
|
+
without the file growing unmanageably. The directory structure mirrors the pattern used in
|
|
107
|
+
`@live-change/live-change-stack/services/*`.
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Design actions, views, triggers with indexes and batch processing patterns
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Skill: live-change-design-actions-views-triggers
|
|
6
|
+
|
|
7
|
+
Ten skill opisuje **krok po kroku**, jak projektować akcje, widoki i triggery w serwisach LiveChange, korzystając z indeksów i unikając pełnych skanów.
|
|
8
|
+
|
|
9
|
+
## Kiedy używać
|
|
10
|
+
|
|
11
|
+
Użyj tego skilla, gdy:
|
|
12
|
+
|
|
13
|
+
- dodajesz nowe akcje do istniejących modeli,
|
|
14
|
+
- tworzysz widoki (szczególnie zakresowe) dla list,
|
|
15
|
+
- implementujesz triggery (online/offline, batchowe przetwarzanie, asynchroniczne wyniki).
|
|
16
|
+
|
|
17
|
+
## 1. Projekt akcji
|
|
18
|
+
|
|
19
|
+
1. **Określ cel akcji**
|
|
20
|
+
- Czy tworzy/aktualizuje/usunie obiekt?
|
|
21
|
+
- Czy ma czekać na zewnętrzny wynik (np. urządzenie)?
|
|
22
|
+
|
|
23
|
+
2. **Zdefiniuj `properties`**
|
|
24
|
+
- Każde pole opisane wieloliniowo,
|
|
25
|
+
- tylko to, co faktycznie potrzebne – resztę pobieraj z bazy na podstawie kluczy.
|
|
26
|
+
|
|
27
|
+
3. **Użyj indeksów do wyszukiwania**
|
|
28
|
+
- Zamiast pełnych skanów, korzystaj z `indexObjectGet` / `indexRangeGet`.
|
|
29
|
+
|
|
30
|
+
4. **Zwróć sensowny wynik**
|
|
31
|
+
- ID utworzonego obiektu,
|
|
32
|
+
- klucze sesyjne, jeśli trzeba je przechować po stronie klienta,
|
|
33
|
+
- dane potrzebne do dalszych kroków.
|
|
34
|
+
|
|
35
|
+
### Szkic wzorca
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
definition.action({
|
|
39
|
+
name: 'someAction',
|
|
40
|
+
properties: {
|
|
41
|
+
someKey: {
|
|
42
|
+
type: String
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
async execute({ someKey }, { client, service }) {
|
|
46
|
+
const obj = await SomeModel.indexObjectGet('bySomeKey', { someKey })
|
|
47
|
+
if(!obj) throw new Error('notFound')
|
|
48
|
+
|
|
49
|
+
const id = app.generateUid()
|
|
50
|
+
|
|
51
|
+
await SomeOtherModel.create({
|
|
52
|
+
id,
|
|
53
|
+
// ...
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
return { id }
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 2. Projekt widoku
|
|
62
|
+
|
|
63
|
+
1. **Zdecyduj, czy to pojedynczy obiekt czy lista**
|
|
64
|
+
- pojedynczy: użyj `get` lub `indexObjectGet`,
|
|
65
|
+
- lista: użyj `indexRangeGet` z indeksem.
|
|
66
|
+
|
|
67
|
+
2. **Zdefiniuj `properties` widoku**
|
|
68
|
+
- tylko parametry potrzebne do wyszukiwania,
|
|
69
|
+
- typy zgodne z modelem (String/Number/itp.).
|
|
70
|
+
|
|
71
|
+
3. **Użyj indeksów**
|
|
72
|
+
|
|
73
|
+
Przykład widoku zakresowego:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
definition.view({
|
|
77
|
+
name: 'myItemsByStatus',
|
|
78
|
+
properties: {
|
|
79
|
+
status: {
|
|
80
|
+
type: String
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
async get({ status }, { client, service }) {
|
|
84
|
+
return MyModel.indexRangeGet('byStatus', {
|
|
85
|
+
status
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## 3. Triggery – online/offline
|
|
92
|
+
|
|
93
|
+
1. **Zidentyfikuj zdarzenie**
|
|
94
|
+
- np. „połączenie online/offline”, „sesja utworzona”, „serwer się uruchomił”.
|
|
95
|
+
|
|
96
|
+
2. **Zdefiniuj trigger z `properties`**
|
|
97
|
+
- triggery online/offline zwykle potrzebują tylko ID obiektu.
|
|
98
|
+
|
|
99
|
+
3. **Aktualizuj minimalny zestaw pól**
|
|
100
|
+
- np. `status`, `lastSeenAt`.
|
|
101
|
+
|
|
102
|
+
Przykład:
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
definition.trigger({
|
|
106
|
+
name: 'sessionConnectionOnline',
|
|
107
|
+
properties: {
|
|
108
|
+
connection: {
|
|
109
|
+
type: String
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
async execute({ connection }, { service }) {
|
|
113
|
+
await Connection.update(connection, {
|
|
114
|
+
status: 'online',
|
|
115
|
+
lastSeenAt: new Date()
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## 4. Triggery batchowe – unikaj pełnych skanów
|
|
122
|
+
|
|
123
|
+
1. **Ustal limit batcha** (np. 32 lub 128 rekordów).
|
|
124
|
+
2. **Wykorzystaj `rangeGet` z `gt: lastId`**
|
|
125
|
+
- inicjalnie `last = ''`,
|
|
126
|
+
- po każdym batchu ustaw `last` na ID ostatniego rekordu.
|
|
127
|
+
3. **Kończ, gdy batch jest pusty**.
|
|
128
|
+
|
|
129
|
+
Przykład:
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
definition.trigger({
|
|
133
|
+
name: 'allOffline',
|
|
134
|
+
async execute({}, { service }) {
|
|
135
|
+
let last = ''
|
|
136
|
+
while(true) {
|
|
137
|
+
const items = await Connection.rangeGet({
|
|
138
|
+
gt: last,
|
|
139
|
+
limit: 32
|
|
140
|
+
})
|
|
141
|
+
if(items.length === 0) break
|
|
142
|
+
|
|
143
|
+
for(const item of items) {
|
|
144
|
+
await Connection.update(item.id, {
|
|
145
|
+
status: 'offline'
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
last = items[items.length - 1].id
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## 5. Wzorzec „pending + resolve” dla asynchronicznych wyników
|
|
156
|
+
|
|
157
|
+
Użyj tego wzorca, gdy:
|
|
158
|
+
|
|
159
|
+
- akcja tworzy zlecenie/komendę,
|
|
160
|
+
- wynik przychodzi później z innego procesu (urządzenie, worker, itp.),
|
|
161
|
+
- chcesz, żeby akcja czekała na wynik z timeoutem.
|
|
162
|
+
|
|
163
|
+
### Kroki
|
|
164
|
+
|
|
165
|
+
1. Utwórz helper `pendingCommands` (Map) w osobnym module.
|
|
166
|
+
2. W akcji tworzącej:
|
|
167
|
+
- utwórz rekord z `status: 'pending'`,
|
|
168
|
+
- wywołaj `waitForCommand(id, timeoutMs)`.
|
|
169
|
+
3. W akcji raportującej:
|
|
170
|
+
- zaktualizuj rekord (`status: 'completed'`, `result`),
|
|
171
|
+
- wywołaj `resolveCommand(id, result)`.
|
|
172
|
+
|
|
173
|
+
### Szkic helpera
|
|
174
|
+
|
|
175
|
+
```js
|
|
176
|
+
const pendingCommands = new Map()
|
|
177
|
+
|
|
178
|
+
export function waitForCommand(commandId, timeoutMs = 115000) {
|
|
179
|
+
return new Promise((resolve, reject) => {
|
|
180
|
+
const timer = setTimeout(() => {
|
|
181
|
+
pendingCommands.delete(commandId)
|
|
182
|
+
reject(new Error('timeout'))
|
|
183
|
+
}, timeoutMs)
|
|
184
|
+
pendingCommands.set(commandId, { resolve, reject, timer })
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function resolveCommand(commandId, result) {
|
|
189
|
+
const pending = pendingCommands.get(commandId)
|
|
190
|
+
if(pending) {
|
|
191
|
+
clearTimeout(pending.timer)
|
|
192
|
+
pendingCommands.delete(commandId)
|
|
193
|
+
pending.resolve(result)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|