@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,118 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Set up SSR entry points, router, PrimeVue theme and Suspense data loading
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Skill: live-change-frontend-ssr-setup (Claude Code)
|
|
6
|
+
|
|
7
|
+
Use this skill to set up or adjust a **LiveChange SSR frontend**:
|
|
8
|
+
|
|
9
|
+
- client/server entry points,
|
|
10
|
+
- router + `meta.signedIn`,
|
|
11
|
+
- PrimeVue theme configuration.
|
|
12
|
+
|
|
13
|
+
## Step 1 – Client and server entry points
|
|
14
|
+
|
|
15
|
+
1. Ensure the frontend has two entry files:
|
|
16
|
+
- `entry-client.js` (or `.ts`),
|
|
17
|
+
- `entry-server.js` (or `.ts`).
|
|
18
|
+
|
|
19
|
+
2. Use helpers from `@live-change/frontend-base`:
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
// entry-client.js
|
|
23
|
+
import { clientEntry } from '@live-change/frontend-base/client-entry.js'
|
|
24
|
+
import App from './App.vue'
|
|
25
|
+
import { createRouter } from './router.js'
|
|
26
|
+
import { config } from './config.js'
|
|
27
|
+
|
|
28
|
+
export default clientEntry(App, createRouter, config)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
// entry-server.js
|
|
33
|
+
import { serverEntry, sitemapEntry } from '@live-change/frontend-base/server-entry.js'
|
|
34
|
+
import App from './App.vue'
|
|
35
|
+
import { createRouter, routerSitemap } from './router.js'
|
|
36
|
+
import { config } from './config.js'
|
|
37
|
+
|
|
38
|
+
export const render = serverEntry(App, createRouter, config)
|
|
39
|
+
export const sitemap = sitemapEntry(App, createRouter, routerSitemap, config)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Step 2 – Router and `meta.signedIn`
|
|
43
|
+
|
|
44
|
+
1. Use `vite-plugin-pages` to auto-generate routes from `src/pages/`.
|
|
45
|
+
2. Add a `<route>` block to each page with basic meta:
|
|
46
|
+
|
|
47
|
+
```vue
|
|
48
|
+
<route>
|
|
49
|
+
{ "name": "devices", "meta": { "signedIn": true } }
|
|
50
|
+
</route>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
3. Add a navigation guard for signed-in pages:
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
router.beforeEach((to) => {
|
|
57
|
+
if(to.meta.signedIn && !isLoggedIn()) {
|
|
58
|
+
localStorage.setItem('redirectAfterLogin', to.fullPath)
|
|
59
|
+
return { name: 'user:signIn' }
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Implement `isLoggedIn()` according to the project’s auth/session model.
|
|
65
|
+
|
|
66
|
+
## Step 3 – PrimeVue theme configuration
|
|
67
|
+
|
|
68
|
+
1. In `config.js`, configure the PrimeVue theme using `definePreset`:
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
import { definePreset } from '@primevue/themes'
|
|
72
|
+
import Aura from '@primevue/themes/aura'
|
|
73
|
+
|
|
74
|
+
const MyPreset = definePreset(Aura, {
|
|
75
|
+
semantic: {
|
|
76
|
+
primary: {
|
|
77
|
+
50: '{indigo.50}',
|
|
78
|
+
100: '{indigo.100}',
|
|
79
|
+
200: '{indigo.200}',
|
|
80
|
+
300: '{indigo.300}',
|
|
81
|
+
400: '{indigo.400}',
|
|
82
|
+
500: '{indigo.500}',
|
|
83
|
+
600: '{indigo.600}',
|
|
84
|
+
700: '{indigo.700}',
|
|
85
|
+
800: '{indigo.800}',
|
|
86
|
+
900: '{indigo.900}',
|
|
87
|
+
950: '{indigo.950}'
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
export const config = {
|
|
93
|
+
theme: {
|
|
94
|
+
preset: MyPreset,
|
|
95
|
+
options: {
|
|
96
|
+
darkModeSelector: '.app-dark-mode'
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
2. Ensure the app uses this config when initializing PrimeVue (usually in `App.vue` / main entry).
|
|
103
|
+
|
|
104
|
+
## Step 4 – Global components and forms
|
|
105
|
+
|
|
106
|
+
1. In `App.vue` (or main setup), register global components used throughout the app:
|
|
107
|
+
- auto-form components,
|
|
108
|
+
- common layout components, etc.
|
|
109
|
+
2. This allows pages to use components like `<command-form>` without local imports.
|
|
110
|
+
|
|
111
|
+
## Step 5 – SSR-friendly data loading
|
|
112
|
+
|
|
113
|
+
1. Ensure the root of the app (e.g. `ViewRoot`) wraps content in `<Suspense>`.
|
|
114
|
+
2. In page components:
|
|
115
|
+
- use `await Promise.all([live(path()....)])` inside `script setup`,
|
|
116
|
+
- read from `.value` in templates,
|
|
117
|
+
- **do not** fetch main data in `onMounted`.
|
|
118
|
+
|
|
@@ -0,0 +1,202 @@
|
|
|
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
|
+
### Wzorzec widoku zakresowego
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
definition.view({
|
|
65
|
+
name: 'pendingCommands',
|
|
66
|
+
properties: {
|
|
67
|
+
connectionId: {
|
|
68
|
+
type: String
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
async get({ connectionId }, { client, service }) {
|
|
72
|
+
return BotCommand.indexRangeGet('byConnectionAndStatus', {
|
|
73
|
+
connection: connectionId,
|
|
74
|
+
status: 'pending'
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Triggery – online/offline i batchowanie
|
|
81
|
+
|
|
82
|
+
- Triggery są do reakcji na zdarzenia (np. zmiana stanu sesji, start serwera).
|
|
83
|
+
- Dwie podstawowe kategorie:
|
|
84
|
+
- triggery dla pojedynczych obiektów (np. połączenie online/offline),
|
|
85
|
+
- triggery „hurtowe” (np. ustawienie wszystkich na offline przy starcie).
|
|
86
|
+
|
|
87
|
+
### Wzorzec triggerów online/offline
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
definition.trigger({
|
|
91
|
+
name: 'sessionDeviceConnectionOnline',
|
|
92
|
+
properties: {
|
|
93
|
+
connection: {
|
|
94
|
+
type: String
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
async execute({ connection }, { service }) {
|
|
98
|
+
await DeviceConnection.update(connection, {
|
|
99
|
+
status: 'online',
|
|
100
|
+
lastSeenAt: new Date()
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
definition.trigger({
|
|
106
|
+
name: 'sessionDeviceConnectionOffline',
|
|
107
|
+
properties: {
|
|
108
|
+
connection: {
|
|
109
|
+
type: String
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
async execute({ connection }, { service }) {
|
|
113
|
+
await DeviceConnection.update(connection, {
|
|
114
|
+
status: 'offline'
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Wzorzec batchowania odczytów (unikaj pełnych skanów)
|
|
121
|
+
|
|
122
|
+
- Przy triggerach, które mają przejść po wielu rekordach, **zawsze** używaj paginacji:
|
|
123
|
+
- stały `limit` (np. 32 lub 128),
|
|
124
|
+
- zakres po kluczu (`gt: lastId`),
|
|
125
|
+
- pętla aż do pustego batcha.
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
definition.trigger({
|
|
129
|
+
name: 'allOffline',
|
|
130
|
+
async execute({}, { service }) {
|
|
131
|
+
let last = ''
|
|
132
|
+
while(true) {
|
|
133
|
+
const connections = await DeviceConnection.rangeGet({
|
|
134
|
+
gt: last,
|
|
135
|
+
limit: 32
|
|
136
|
+
})
|
|
137
|
+
if(connections.length === 0) break
|
|
138
|
+
|
|
139
|
+
for(const conn of connections) {
|
|
140
|
+
await DeviceConnection.update(conn.id, {
|
|
141
|
+
status: 'offline'
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
last = connections[connections.length - 1].id
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Wzorzec „pending + resolve” (asynchroniczny wynik)
|
|
152
|
+
|
|
153
|
+
- Używaj, gdy akcja w serwisie musi poczekać na wynik z zewnętrznego procesu (np. urządzenie, worker).
|
|
154
|
+
- Struktura:
|
|
155
|
+
1. Akcja tworzy rekord „pending”.
|
|
156
|
+
2. Akcja czeka na `Promise` powiązany z ID.
|
|
157
|
+
3. Inna akcja/trigger aktualizuje rekord i rozwiązuje `Promise`.
|
|
158
|
+
|
|
159
|
+
Szkic:
|
|
160
|
+
|
|
161
|
+
```js
|
|
162
|
+
// pendingCommands.js – singleton w pamięci
|
|
163
|
+
const pendingCommands = new Map()
|
|
164
|
+
|
|
165
|
+
export function waitForCommand(commandId, timeoutMs = 115000) {
|
|
166
|
+
return new Promise((resolve, reject) => {
|
|
167
|
+
const timer = setTimeout(() => {
|
|
168
|
+
pendingCommands.delete(commandId)
|
|
169
|
+
reject(new Error('timeout'))
|
|
170
|
+
}, timeoutMs)
|
|
171
|
+
pendingCommands.set(commandId, { resolve, reject, timer })
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function resolveCommand(commandId, result) {
|
|
176
|
+
const pending = pendingCommands.get(commandId)
|
|
177
|
+
if(pending) {
|
|
178
|
+
clearTimeout(pending.timer)
|
|
179
|
+
pendingCommands.delete(commandId)
|
|
180
|
+
pending.resolve(result)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
W akcji wywołującej:
|
|
186
|
+
|
|
187
|
+
```js
|
|
188
|
+
await SomeCommand.create({ id, status: 'pending', ... })
|
|
189
|
+
const result = await waitForCommand(id)
|
|
190
|
+
return result
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
W akcji raportującej:
|
|
194
|
+
|
|
195
|
+
```js
|
|
196
|
+
await SomeCommand.update(id, {
|
|
197
|
+
status: 'completed',
|
|
198
|
+
result
|
|
199
|
+
})
|
|
200
|
+
resolveCommand(id, result)
|
|
201
|
+
```
|
|
202
|
+
|
|
@@ -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,194 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Rules for defining models, relations, indexes and access control in LiveChange
|
|
3
|
+
globs: **/services/**/*.js
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# LiveChange backend – modele i relacje
|
|
8
|
+
|
|
9
|
+
## Ogólne zasady
|
|
10
|
+
|
|
11
|
+
- Modele zapisuj w plikach domenowych (`<domain>.js`) importowanych w `index.js` serwisu.
|
|
12
|
+
- Dbaj o czytelność definicji – **właściwości wieloliniowo**, z jasnymi polami `type`, `default`, `validation` itd.
|
|
13
|
+
- Tam, gdzie to możliwe, używaj relacji (`userItem`, `itemOf`, `propertyOf`, `foreignModel`) zamiast ręcznego klepania CRUD i widoków.
|
|
14
|
+
|
|
15
|
+
## Styl definicji `properties`
|
|
16
|
+
|
|
17
|
+
- Każde pole opisane wieloliniowo, jedna rzecz na linię.
|
|
18
|
+
- Unikaj upychania typu i walidacji w jednej linii, jeśli zmniejsza to czytelność.
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
properties: {
|
|
22
|
+
name: {
|
|
23
|
+
type: String,
|
|
24
|
+
validation: ['nonEmpty']
|
|
25
|
+
},
|
|
26
|
+
status: {
|
|
27
|
+
type: String,
|
|
28
|
+
default: 'offline'
|
|
29
|
+
},
|
|
30
|
+
capabilities: {
|
|
31
|
+
type: Array,
|
|
32
|
+
of: {
|
|
33
|
+
type: String
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## `userItem` – zasób należący do użytkownika
|
|
40
|
+
|
|
41
|
+
Używaj, gdy model należy do zalogowanego użytkownika.
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
definition.model({
|
|
45
|
+
name: 'Device',
|
|
46
|
+
properties: {
|
|
47
|
+
name: {
|
|
48
|
+
type: String
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
userItem: {
|
|
52
|
+
readAccessControl: { roles: ['owner', 'admin'] },
|
|
53
|
+
writeAccessControl: { roles: ['owner', 'admin'] },
|
|
54
|
+
writeableProperties: ['name']
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Skutek:
|
|
60
|
+
|
|
61
|
+
- automatyczne widoki typu „moje X”,
|
|
62
|
+
- automatyczne akcje create/update/delete dla właściciela.
|
|
63
|
+
|
|
64
|
+
## `itemOf` – dziecko należy do rodzica
|
|
65
|
+
|
|
66
|
+
Używaj, gdy model jest listą elementów powiązanych z innym modelem.
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
definition.model({
|
|
70
|
+
name: 'DeviceConnection',
|
|
71
|
+
properties: {
|
|
72
|
+
connectionType: {
|
|
73
|
+
type: String
|
|
74
|
+
},
|
|
75
|
+
status: {
|
|
76
|
+
type: String,
|
|
77
|
+
default: 'offline'
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
itemOf: {
|
|
81
|
+
what: Device,
|
|
82
|
+
readAccessControl: { roles: ['owner', 'admin'] },
|
|
83
|
+
writeAccessControl: { roles: ['owner', 'admin'] }
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Zasady:
|
|
89
|
+
|
|
90
|
+
- rodzic `what` to model zadeklarowany wcześniej w tym lub innym serwisie,
|
|
91
|
+
- relacja generuje standardowe widoki/akcje dla listy i elementów.
|
|
92
|
+
|
|
93
|
+
## `propertyOf` – właściwość z ID równym rodzicowi
|
|
94
|
+
|
|
95
|
+
Używaj, gdy model przechowuje „stan” jednego obiektu (ID dziecka = ID rodzica).
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
definition.model({
|
|
99
|
+
name: 'DeviceCursorState',
|
|
100
|
+
properties: {
|
|
101
|
+
x: { type: Number },
|
|
102
|
+
y: { type: Number }
|
|
103
|
+
},
|
|
104
|
+
propertyOf: {
|
|
105
|
+
what: Device,
|
|
106
|
+
readAccessControl: { roles: ['owner', 'admin'] },
|
|
107
|
+
writeAccessControl: { roles: ['owner', 'admin'] }
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Skutek:
|
|
113
|
+
|
|
114
|
+
- prosty dostęp: `DeviceCursorState.get(deviceId)`,
|
|
115
|
+
- bez potrzeby dodatkowych indeksów po polu `device`.
|
|
116
|
+
|
|
117
|
+
## `propertyOf` dla wielu modeli (relacja 1:1 do każdego z nich)
|
|
118
|
+
|
|
119
|
+
W niektórych domenach model jest „łącznikiem 1:1” pomiędzy rekordami (np. faktura ↔ kontrahent w roli dostawcy/klienta).
|
|
120
|
+
Najczęściej są to 2 modele, ale `propertyOf` może wskazywać na **dowolnie wiele** modeli (np. relacja łącząca 3+ encje), jeśli taka jest semantyka domeny.
|
|
121
|
+
|
|
122
|
+
Wtedy:
|
|
123
|
+
|
|
124
|
+
- **nie** przechowuj „drugiej strony” jako zwykłego `contractorId` (albo ogólnie `someId`) w `properties`
|
|
125
|
+
- **nie** dodawaj ręcznie pól `...Id` w modelu relacyjnym, jeśli to ma być relacja – to utrudnia generatorowi CRUD/relacji poprawne wnioskowanie o powiązaniach
|
|
126
|
+
- zamiast tego zdefiniuj relacje jako `propertyOf` do **każdego** z modeli, które mają być rodzicami relacji
|
|
127
|
+
|
|
128
|
+
To jest istotne, bo generator CRUD/relacji rozumie wtedy, że encja jest powiązaniem pomiędzy dwiema encjami, a nie „zwykłym obiektem z polem id”.
|
|
129
|
+
|
|
130
|
+
Przykład (schematycznie):
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
const CostInvoice = definition.foreignModel('invoice', 'CostInvoice')
|
|
134
|
+
const Contractor = definition.foreignModel('company', 'Contractor')
|
|
135
|
+
|
|
136
|
+
definition.model({
|
|
137
|
+
name: 'Supplier',
|
|
138
|
+
properties: {
|
|
139
|
+
// dodatkowe pola relacji (opcjonalnie)
|
|
140
|
+
},
|
|
141
|
+
propertyOf: [
|
|
142
|
+
{ what: CostInvoice },
|
|
143
|
+
{ what: Contractor }
|
|
144
|
+
]
|
|
145
|
+
})
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## `foreignModel` – relacja do modelu z innego serwisu
|
|
149
|
+
|
|
150
|
+
Używaj, gdy `itemOf`/`propertyOf` ma wskazywać na model spoza aktualnego serwisu.
|
|
151
|
+
|
|
152
|
+
```js
|
|
153
|
+
const Device = definition.foreignModel('deviceManager', 'Device')
|
|
154
|
+
|
|
155
|
+
definition.model({
|
|
156
|
+
name: 'BotSession',
|
|
157
|
+
properties: {
|
|
158
|
+
// ...
|
|
159
|
+
},
|
|
160
|
+
itemOf: {
|
|
161
|
+
what: Device,
|
|
162
|
+
readAccessControl: { roles: ['owner', 'admin'] }
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Zasady:
|
|
168
|
+
|
|
169
|
+
- pierwszy argument to nazwa serwisu,
|
|
170
|
+
- drugi to nazwa modelu w tamtym serwisie.
|
|
171
|
+
|
|
172
|
+
## Indeksy
|
|
173
|
+
|
|
174
|
+
- Definiuj indeksy jawnie w modelu, gdy będziesz często wyszukiwać po danym polu lub kombinacji pól.
|
|
175
|
+
- Nazwy indeksów powinny być opisowe, bez skrótów trudnych do odczytania.
|
|
176
|
+
|
|
177
|
+
```js
|
|
178
|
+
indexes: {
|
|
179
|
+
bySessionKey: {
|
|
180
|
+
property: ['sessionKey']
|
|
181
|
+
},
|
|
182
|
+
byDeviceAndStatus: {
|
|
183
|
+
property: ['device', 'status']
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Poza serwisem indeks bywa widoczny pod nazwą z prefiksem serwisu, np. `myService_Model_byDeviceAndStatus`.
|
|
189
|
+
|
|
190
|
+
## Access control na relacjach
|
|
191
|
+
|
|
192
|
+
- Zawsze ustawiaj `readAccessControl` i `writeAccessControl` na relacjach (`userItem`, `itemOf`, `propertyOf`), zamiast polegać na domyślnym zachowaniu.
|
|
193
|
+
- Traktuj to jako część modelu, nie dodatek na końcu.
|
|
194
|
+
|