@live-change/frontend-template 0.9.199 → 0.9.201
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 +62 -0
- package/.claude/rules/live-change-backend-event-sourcing.md +186 -0
- package/.claude/rules/live-change-backend-models-and-relations.md +72 -0
- package/.claude/rules/live-change-frontend-vue-primevue.md +26 -0
- package/.claude/settings.json +32 -0
- package/.claude/skills/create-skills-and-rules/SKILL.md +248 -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-models-relations/SKILL.md +230 -0
- package/.claude/skills/live-change-design-service/SKILL.md +133 -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-form/SKILL.md +149 -0
- package/.claude/skills/live-change-frontend-analytics/SKILL.md +147 -0
- package/.claude/skills/live-change-frontend-command-forms/SKILL.md +216 -0
- package/.claude/skills/live-change-frontend-data-views/SKILL.md +183 -0
- package/.claude/skills/live-change-frontend-editor-form/SKILL.md +240 -0
- package/.claude/skills/live-change-frontend-locale-time/SKILL.md +172 -0
- package/.claude/skills/live-change-frontend-page-list-detail/SKILL.md +201 -0
- package/.claude/skills/live-change-frontend-range-list/SKILL.md +129 -0
- package/.claude/skills/live-change-frontend-ssr-setup/SKILL.md +119 -0
- package/.cursor/rules/live-change-backend-actions-views-triggers.mdc +88 -0
- package/.cursor/rules/live-change-backend-event-sourcing.mdc +185 -0
- package/.cursor/rules/live-change-backend-models-and-relations.mdc +62 -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 +178 -79
- package/.cursor/skills/live-change-design-models-relations.md +112 -50
- package/.cursor/skills/live-change-design-service.md +1 -0
- package/.cursor/skills/live-change-frontend-accessible-objects.md +384 -0
- package/.cursor/skills/live-change-frontend-action-buttons.md +1 -0
- package/.cursor/skills/live-change-frontend-action-form.md +9 -3
- package/.cursor/skills/live-change-frontend-analytics.md +1 -0
- package/.cursor/skills/live-change-frontend-command-forms.md +1 -0
- package/.cursor/skills/live-change-frontend-data-views.md +1 -0
- package/.cursor/skills/live-change-frontend-editor-form.md +135 -72
- package/.cursor/skills/live-change-frontend-locale-time.md +1 -0
- package/.cursor/skills/live-change-frontend-page-list-detail.md +1 -0
- package/.cursor/skills/live-change-frontend-range-list.md +1 -0
- package/.cursor/skills/live-change-frontend-ssr-setup.md +1 -0
- package/front/src/router.js +2 -1
- package/opencode.json +10 -0
- package/package.json +52 -50
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: live-change-frontend-range-list
|
|
3
|
+
description: Build paginated scrollable lists with RangeViewer, rangeBuckets and .with()
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Skill: live-change-frontend-range-list (Claude Code)
|
|
7
|
+
|
|
8
|
+
Use this skill when you build **paginated, scrollable lists** backed by DAO ranges in a LiveChange frontend.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
|
|
12
|
+
- You need a list that loads items in pages (infinite scroll).
|
|
13
|
+
- The list is backed by a DAO range view (e.g. `articlesByCreatedAt`).
|
|
14
|
+
- You want to attach related objects to each item via `.with()`.
|
|
15
|
+
|
|
16
|
+
## Step 1 – Define the path function
|
|
17
|
+
|
|
18
|
+
The path function receives a `range` object (with `gt`, `gte`, `lt`, `lte`, `limit`, `reverse`) and returns a DAO path.
|
|
19
|
+
|
|
20
|
+
Use `reverseRange()` to display items newest-first:
|
|
21
|
+
|
|
22
|
+
```javascript
|
|
23
|
+
import { reverseRange } from '@live-change/vue3-ssr'
|
|
24
|
+
|
|
25
|
+
function articlesPathRange(range) {
|
|
26
|
+
return path.blog.articlesByCreatedAt({ ...reverseRange(range) })
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Step 2 – Attach related objects with `.with()`
|
|
31
|
+
|
|
32
|
+
Chain `.with()` calls to load related data for each item:
|
|
33
|
+
|
|
34
|
+
```javascript
|
|
35
|
+
function articlesPathRange(range) {
|
|
36
|
+
return path.blog.articlesByCreatedAt({ ...reverseRange(range) })
|
|
37
|
+
.with(article => path.userIdentification.identification({
|
|
38
|
+
sessionOrUserType: article.authorType,
|
|
39
|
+
sessionOrUser: article.author
|
|
40
|
+
}).bind('authorProfile'))
|
|
41
|
+
.with(article => path.blog.articleStats({ article: article.id }).bind('stats'))
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Each `.with()` call:
|
|
46
|
+
- receives a proxy of the item,
|
|
47
|
+
- builds a path to the related data,
|
|
48
|
+
- calls `.bind('fieldName')` to attach the result under that field name.
|
|
49
|
+
|
|
50
|
+
Nested `.with()` is also supported:
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
function eventsPathRange(range) {
|
|
54
|
+
return path.myService.allEvents({ ...reverseRange(range) })
|
|
55
|
+
.with(event => path.myService.eventState({ event: event.id }).bind('state')
|
|
56
|
+
.with(state => path.myService.roundPairs({ event: event.id, round: state.round }).bind('roundPairs'))
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Step 3 – Use `<RangeViewer>` in the template
|
|
62
|
+
|
|
63
|
+
```vue
|
|
64
|
+
<template>
|
|
65
|
+
<RangeViewer
|
|
66
|
+
:pathFunction="articlesPathRange"
|
|
67
|
+
:canLoadTop="false"
|
|
68
|
+
canDropBottom
|
|
69
|
+
loadBottomSensorSize="3000px"
|
|
70
|
+
dropBottomSensorSize="5000px"
|
|
71
|
+
>
|
|
72
|
+
<template #empty>
|
|
73
|
+
<p class="text-center text-surface-500 my-4">No articles yet.</p>
|
|
74
|
+
</template>
|
|
75
|
+
<template #default="{ item: article }">
|
|
76
|
+
<Card class="mb-2">
|
|
77
|
+
<template #content>
|
|
78
|
+
<h3>{{ article.title }}</h3>
|
|
79
|
+
<p class="text-sm text-surface-500">By {{ article.authorProfile?.firstName }}</p>
|
|
80
|
+
</template>
|
|
81
|
+
</Card>
|
|
82
|
+
</template>
|
|
83
|
+
</RangeViewer>
|
|
84
|
+
</template>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Key props:
|
|
88
|
+
|
|
89
|
+
| Prop | Default | Description |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `pathFunction` | required | `(range) => path` – builds the DAO path for a given range |
|
|
92
|
+
| `bucketSize` | `20` | Items per page |
|
|
93
|
+
| `canLoadTop` / `canLoadBottom` | `true` | Whether loading in each direction is allowed |
|
|
94
|
+
| `canDropTop` / `canDropBottom` | `false` | Whether to drop pages that scrolled far out of view |
|
|
95
|
+
| `loadBottomSensorSize` | `'500px'` | How far before the bottom to trigger loading (increase for smoother UX) |
|
|
96
|
+
| `dropBottomSensorSize` | `'5000px'` | How far to keep before dropping |
|
|
97
|
+
| `frozen` | `false` | Pause live updates |
|
|
98
|
+
|
|
99
|
+
Slots:
|
|
100
|
+
|
|
101
|
+
| Slot | Props | Description |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| `default` | `{ item, bucket, itemIndex, bucketIndex }` | Render each item |
|
|
104
|
+
| `empty` | – | Shown when there are no items |
|
|
105
|
+
| `loadingTop` / `loadingBottom` | – | Loading spinners |
|
|
106
|
+
| `changedTop` / `changedBottom` | – | Indicators when data changed while frozen |
|
|
107
|
+
|
|
108
|
+
## Step 4 – Low-level `rangeBuckets` (optional)
|
|
109
|
+
|
|
110
|
+
For advanced control, use `rangeBuckets` directly:
|
|
111
|
+
|
|
112
|
+
```javascript
|
|
113
|
+
import { rangeBuckets, reverseRange } from '@live-change/vue3-ssr'
|
|
114
|
+
|
|
115
|
+
const { buckets, loadBottom, dropTop, freeze, unfreeze } = await rangeBuckets(
|
|
116
|
+
(range) => path.blog.articlesByCreatedAt({ ...reverseRange(range) }),
|
|
117
|
+
{ bucketSize: 20 }
|
|
118
|
+
)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Iterate in the template:
|
|
122
|
+
|
|
123
|
+
```vue
|
|
124
|
+
<template v-for="(bucket, bi) in buckets.value" :key="bi">
|
|
125
|
+
<div v-for="(item, ii) in bucket.data.value" :key="item.id">
|
|
126
|
+
<!-- render item -->
|
|
127
|
+
</div>
|
|
128
|
+
</template>
|
|
129
|
+
```
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: live-change-frontend-ssr-setup
|
|
3
|
+
description: Set up SSR entry points, router, PrimeVue theme and Suspense data loading
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Skill: live-change-frontend-ssr-setup (Claude Code)
|
|
7
|
+
|
|
8
|
+
Use this skill to set up or adjust a **LiveChange SSR frontend**:
|
|
9
|
+
|
|
10
|
+
- client/server entry points,
|
|
11
|
+
- router + `meta.signedIn`,
|
|
12
|
+
- PrimeVue theme configuration.
|
|
13
|
+
|
|
14
|
+
## Step 1 – Client and server entry points
|
|
15
|
+
|
|
16
|
+
1. Ensure the frontend has two entry files:
|
|
17
|
+
- `entry-client.js` (or `.ts`),
|
|
18
|
+
- `entry-server.js` (or `.ts`).
|
|
19
|
+
|
|
20
|
+
2. Use helpers from `@live-change/frontend-base`:
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
// entry-client.js
|
|
24
|
+
import { clientEntry } from '@live-change/frontend-base/client-entry.js'
|
|
25
|
+
import App from './App.vue'
|
|
26
|
+
import { createRouter } from './router.js'
|
|
27
|
+
import { config } from './config.js'
|
|
28
|
+
|
|
29
|
+
export default clientEntry(App, createRouter, config)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
// entry-server.js
|
|
34
|
+
import { serverEntry, sitemapEntry } from '@live-change/frontend-base/server-entry.js'
|
|
35
|
+
import App from './App.vue'
|
|
36
|
+
import { createRouter, routerSitemap } from './router.js'
|
|
37
|
+
import { config } from './config.js'
|
|
38
|
+
|
|
39
|
+
export const render = serverEntry(App, createRouter, config)
|
|
40
|
+
export const sitemap = sitemapEntry(App, createRouter, routerSitemap, config)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Step 2 – Router and `meta.signedIn`
|
|
44
|
+
|
|
45
|
+
1. Use `vite-plugin-pages` to auto-generate routes from `src/pages/`.
|
|
46
|
+
2. Add a `<route>` block to each page with basic meta:
|
|
47
|
+
|
|
48
|
+
```vue
|
|
49
|
+
<route>
|
|
50
|
+
{ "name": "devices", "meta": { "signedIn": true } }
|
|
51
|
+
</route>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
3. Add a navigation guard for signed-in pages:
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
router.beforeEach((to) => {
|
|
58
|
+
if(to.meta.signedIn && !isLoggedIn()) {
|
|
59
|
+
localStorage.setItem('redirectAfterLogin', to.fullPath)
|
|
60
|
+
return { name: 'user:signIn' }
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Implement `isLoggedIn()` according to the project’s auth/session model.
|
|
66
|
+
|
|
67
|
+
## Step 3 – PrimeVue theme configuration
|
|
68
|
+
|
|
69
|
+
1. In `config.js`, configure the PrimeVue theme using `definePreset`:
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
import { definePreset } from '@primevue/themes'
|
|
73
|
+
import Aura from '@primevue/themes/aura'
|
|
74
|
+
|
|
75
|
+
const MyPreset = definePreset(Aura, {
|
|
76
|
+
semantic: {
|
|
77
|
+
primary: {
|
|
78
|
+
50: '{indigo.50}',
|
|
79
|
+
100: '{indigo.100}',
|
|
80
|
+
200: '{indigo.200}',
|
|
81
|
+
300: '{indigo.300}',
|
|
82
|
+
400: '{indigo.400}',
|
|
83
|
+
500: '{indigo.500}',
|
|
84
|
+
600: '{indigo.600}',
|
|
85
|
+
700: '{indigo.700}',
|
|
86
|
+
800: '{indigo.800}',
|
|
87
|
+
900: '{indigo.900}',
|
|
88
|
+
950: '{indigo.950}'
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
export const config = {
|
|
94
|
+
theme: {
|
|
95
|
+
preset: MyPreset,
|
|
96
|
+
options: {
|
|
97
|
+
darkModeSelector: '.app-dark-mode'
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
2. Ensure the app uses this config when initializing PrimeVue (usually in `App.vue` / main entry).
|
|
104
|
+
|
|
105
|
+
## Step 4 – Global components and forms
|
|
106
|
+
|
|
107
|
+
1. In `App.vue` (or main setup), register global components used throughout the app:
|
|
108
|
+
- auto-form components,
|
|
109
|
+
- common layout components, etc.
|
|
110
|
+
2. This allows pages to use components like `<command-form>` without local imports.
|
|
111
|
+
|
|
112
|
+
## Step 5 – SSR-friendly data loading
|
|
113
|
+
|
|
114
|
+
1. Ensure the root of the app (e.g. `ViewRoot`) wraps content in `<Suspense>`.
|
|
115
|
+
2. In page components:
|
|
116
|
+
- use `await Promise.all([live(path()....)])` inside `script setup`,
|
|
117
|
+
- read from `.value` in templates,
|
|
118
|
+
- **do not** fetch main data in `onMounted`.
|
|
119
|
+
|
|
@@ -58,6 +58,67 @@ definition.action({
|
|
|
58
58
|
- korzystaj z indeksów (`indexObjectGet`, `indexRangeGet`),
|
|
59
59
|
- nie implementuj skomplikowanej logiki w widokach, jeśli można ją przenieść do akcji.
|
|
60
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
|
+
|
|
61
122
|
### Wzorzec widoku zakresowego
|
|
62
123
|
|
|
63
124
|
```js
|
|
@@ -148,6 +209,33 @@ definition.trigger({
|
|
|
148
209
|
})
|
|
149
210
|
```
|
|
150
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
|
+
|
|
151
239
|
## Wzorzec „pending + resolve” (asynchroniczny wynik)
|
|
152
240
|
|
|
153
241
|
- Używaj, gdy akcja w serwisie musi poczekać na wynik z zewnętrznego procesu (np. urządzenie, worker).
|
|
@@ -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 |
|
|
@@ -169,6 +169,68 @@ Zasady:
|
|
|
169
169
|
- pierwszy argument to nazwa serwisu,
|
|
170
170
|
- drugi to nazwa modelu w tamtym serwisie.
|
|
171
171
|
|
|
172
|
+
## Automatycznie dodawane pola z relacji
|
|
173
|
+
|
|
174
|
+
Relacje automatycznie dodają **pola identyfikatorów** i **indeksy** do modelu. **Nie definiuj ich ponownie** w `properties`.
|
|
175
|
+
|
|
176
|
+
**Konwencja nazw:** nazwa pola = nazwa modelu rodzica z małą pierwszą literą (`Device` → `device`, `CostInvoice` → `costInvoice`).
|
|
177
|
+
|
|
178
|
+
| Relacja | Dodane pole/pola | Dodane indeksy |
|
|
179
|
+
|---|---|---|
|
|
180
|
+
| `itemOf: { what: Device }` | `device` | `byDevice` |
|
|
181
|
+
| `propertyOf: { what: Device }` | `device` | `byDevice` |
|
|
182
|
+
| `userItem` | `user` | `byUser` |
|
|
183
|
+
| `userProperty` | `user` | `byUser` |
|
|
184
|
+
| `sessionOrUserProperty` | `sessionOrUserType`, `sessionOrUser` | `bySessionOrUser` (hash) |
|
|
185
|
+
| `sessionOrUserProperty: { extendedWith: ['object'] }` | + `objectType`, `object` | indeksy złożone |
|
|
186
|
+
| `propertyOfAny: { ownerTypes: [...] }` | `ownerType`, `owner` | `byOwner` (hash) |
|
|
187
|
+
| `boundTo: { what: Device }` | `device` | `byDevice` (hash) |
|
|
188
|
+
|
|
189
|
+
Dla relacji z wieloma rodzicami (np. `propertyOf: [{ what: A }, { what: B }]`) tworzone są wszystkie kombinacje indeksów (`byA`, `byB`, `byAAndB`).
|
|
190
|
+
|
|
191
|
+
## `propertyOfAny` — typy rodzica (`{name}Types`)
|
|
192
|
+
|
|
193
|
+
`propertyOfAny` jest relacją polimorficzną. Lista dozwolonych typów jest podawana w polach `{name}Types`, gdzie `{name}` pochodzi z `to`.
|
|
194
|
+
|
|
195
|
+
- Gdy `to` nie jest podane, domyślnie jest `['owner']`, więc użyj `ownerTypes`.
|
|
196
|
+
- Gdy `to: ['invoice']`, użyj `invoiceTypes`.
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
// domyślnie to: ['owner']
|
|
200
|
+
propertyOfAny: {
|
|
201
|
+
ownerTypes: ['invoice_CostInvoice', 'invoice_IncomeInvoice']
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// jawnie
|
|
205
|
+
propertyOfAny: {
|
|
206
|
+
to: ['invoice'],
|
|
207
|
+
invoiceTypes: ['invoice_CostInvoice', 'invoice_IncomeInvoice']
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
// ✅ Poprawnie — definiuj tylko SWOJE pola
|
|
213
|
+
definition.model({
|
|
214
|
+
name: 'Connection',
|
|
215
|
+
properties: {
|
|
216
|
+
status: { type: String } // 'device' NIE jest tutaj — dodaje go itemOf
|
|
217
|
+
},
|
|
218
|
+
itemOf: { what: Device } // automatycznie dodaje pole 'device' + indeks 'byDevice'
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
// ❌ Źle — redundantne pole
|
|
222
|
+
definition.model({
|
|
223
|
+
name: 'Connection',
|
|
224
|
+
properties: {
|
|
225
|
+
device: { type: String }, // ❌ już dodane przez itemOf
|
|
226
|
+
status: { type: String }
|
|
227
|
+
},
|
|
228
|
+
itemOf: { what: Device }
|
|
229
|
+
})
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Użyj `node server/start.js describe --service myService --model MyModel --output yaml` żeby zobaczyć wszystkie pola łącznie z automatycznie dodanymi.
|
|
233
|
+
|
|
172
234
|
## Indeksy
|
|
173
235
|
|
|
174
236
|
- Definiuj indeksy jawnie w modelu, gdy będziesz często wyszukiwać po danym polu lub kombinacji pól.
|