@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,296 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: live-change-design-actions-views-triggers
|
|
3
|
+
description: Design actions, views, triggers with indexes and batch processing patterns
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Skill: live-change-design-actions-views-triggers (Claude Code)
|
|
7
|
+
|
|
8
|
+
Use this skill to design **actions, views, and triggers** in LiveChange services while making good use of indexes and avoiding full-table scans.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
|
|
12
|
+
- You add or change actions on existing models.
|
|
13
|
+
- You define new views (especially list/range views).
|
|
14
|
+
- You implement triggers (online/offline, batch processing, async result flows).
|
|
15
|
+
|
|
16
|
+
## Step 1 – Design an action
|
|
17
|
+
|
|
18
|
+
1. **Clarify the goal**:
|
|
19
|
+
- create / update / delete a record,
|
|
20
|
+
- or create a “command” that will be completed later.
|
|
21
|
+
2. **Define `properties`** clearly:
|
|
22
|
+
- only include what the client must provide,
|
|
23
|
+
- fetch the rest from the database via indexes.
|
|
24
|
+
3. **Use indexes**, not full scans:
|
|
25
|
+
- `indexObjectGet('bySomething', { ... })` for single-object lookups,
|
|
26
|
+
- `indexRangeGet('bySomething', { ... })` for lists.
|
|
27
|
+
4. **Return a useful result**:
|
|
28
|
+
- new object id,
|
|
29
|
+
- session keys,
|
|
30
|
+
- any data needed for the next step.
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
definition.action({
|
|
36
|
+
name: 'someAction',
|
|
37
|
+
properties: {
|
|
38
|
+
someKey: { type: String }
|
|
39
|
+
},
|
|
40
|
+
async execute({ someKey }, { client, service }) {
|
|
41
|
+
const obj = await SomeModel.indexObjectGet('bySomeKey', { someKey })
|
|
42
|
+
if(!obj) throw new Error('notFound')
|
|
43
|
+
|
|
44
|
+
const id = app.generateUid()
|
|
45
|
+
|
|
46
|
+
await SomeOtherModel.create({
|
|
47
|
+
id
|
|
48
|
+
// ...
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
return { id }
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Step 2 – Design a view
|
|
57
|
+
|
|
58
|
+
1. Decide what kind of data source you have, then pick the **view variant** (exactly one):
|
|
59
|
+
|
|
60
|
+
| Variant | When to use |
|
|
61
|
+
| --- | --- |
|
|
62
|
+
| `daoPath` | Data is stored in the framework DAO (preferred). The framework auto-generates both `get` and `observable` from `daoPath`. |
|
|
63
|
+
| `get` + `observable` | External or custom reactive data source (eg. WebSocket client, RPC stream). **Both are required together.** |
|
|
64
|
+
| `fetch` | Remote, non-reactive request/response data (eg. GeoIP). Often paired with `remote: true`. |
|
|
65
|
+
|
|
66
|
+
2. Decide if you need:
|
|
67
|
+
- a **single** object view, or
|
|
68
|
+
- a **list/range** view.
|
|
69
|
+
3. Define `properties` for the view:
|
|
70
|
+
- only parameters needed for filtering,
|
|
71
|
+
- types consistent with model fields.
|
|
72
|
+
4. Prefer `daoPath` when you are reading from the DAO:
|
|
73
|
+
- use model paths (`Model.path`, `Model.rangePath`, `Model.sortedIndexRangePath`, `Model.indexObjectPath`)
|
|
74
|
+
- use `...App.rangeProperties` + `App.extractRange(props)` for range views
|
|
75
|
+
|
|
76
|
+
### Example: `daoPath` (preferred, DAO-backed)
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
definition.view({
|
|
80
|
+
name: 'costInvoice',
|
|
81
|
+
properties: {
|
|
82
|
+
costInvoice: {
|
|
83
|
+
type: String
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
returns: { type: Object },
|
|
87
|
+
async daoPath({ costInvoice }) {
|
|
88
|
+
return CostInvoice.path(costInvoice)
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Example: `get` + `observable` together (external / reactive)
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
definition.view({
|
|
97
|
+
name: 'session',
|
|
98
|
+
properties: {},
|
|
99
|
+
returns: { type: Number },
|
|
100
|
+
async get(params, { client }) {
|
|
101
|
+
return onlineClient.get(['online', 'session', { ...params, session: client.session }])
|
|
102
|
+
},
|
|
103
|
+
async observable(params, { client }) {
|
|
104
|
+
return onlineClient.observable(
|
|
105
|
+
['online', 'session', { ...params, session: client.session }],
|
|
106
|
+
ReactiveDao.ObservableValue
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Example: `fetch` (remote / non-reactive)
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
definition.view({
|
|
116
|
+
name: 'myCountry',
|
|
117
|
+
properties: {},
|
|
118
|
+
returns: { type: String },
|
|
119
|
+
remote: true,
|
|
120
|
+
async fetch(props, { client }) {
|
|
121
|
+
return await getGeoIp(client.ip)
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Anti-pattern: `get` without `observable` (do not do this)
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
definition.view({
|
|
130
|
+
name: 'brokenView',
|
|
131
|
+
properties: {
|
|
132
|
+
id: { type: String }
|
|
133
|
+
},
|
|
134
|
+
returns: { type: Object },
|
|
135
|
+
async get({ id }) {
|
|
136
|
+
return await SomeModel.get(id)
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Step 3 – Online/offline triggers
|
|
142
|
+
|
|
143
|
+
1. Identify events:
|
|
144
|
+
- session or connection goes online,
|
|
145
|
+
- session or connection goes offline.
|
|
146
|
+
2. Define triggers with minimal `properties` (usually just an id).
|
|
147
|
+
3. Update only the necessary fields (`status`, `lastSeenAt`, etc.).
|
|
148
|
+
|
|
149
|
+
Example:
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
definition.trigger({
|
|
153
|
+
name: 'sessionConnectionOnline',
|
|
154
|
+
properties: {
|
|
155
|
+
connection: { type: String }
|
|
156
|
+
},
|
|
157
|
+
async execute({ connection }, { service }) {
|
|
158
|
+
await Connection.update(connection, {
|
|
159
|
+
status: 'online',
|
|
160
|
+
lastSeenAt: new Date()
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
definition.trigger({
|
|
166
|
+
name: 'sessionConnectionOffline',
|
|
167
|
+
properties: {
|
|
168
|
+
connection: { type: String }
|
|
169
|
+
},
|
|
170
|
+
async execute({ connection }, { service }) {
|
|
171
|
+
await Connection.update(connection, {
|
|
172
|
+
status: 'offline'
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Step 4 – Batch triggers (avoid full scans)
|
|
179
|
+
|
|
180
|
+
1. Pick a **batch size** (e.g. 32 or 128).
|
|
181
|
+
2. Use `rangeGet` with `gt: lastId` in a loop:
|
|
182
|
+
- start with `last = ''`,
|
|
183
|
+
- after each batch, set `last` to the last record’s id,
|
|
184
|
+
- stop when the batch is empty.
|
|
185
|
+
|
|
186
|
+
Example:
|
|
187
|
+
|
|
188
|
+
```js
|
|
189
|
+
definition.trigger({
|
|
190
|
+
name: 'allOffline',
|
|
191
|
+
async execute({}, { service }) {
|
|
192
|
+
let last = ''
|
|
193
|
+
while(true) {
|
|
194
|
+
const items = await Connection.rangeGet({
|
|
195
|
+
gt: last,
|
|
196
|
+
limit: 32
|
|
197
|
+
})
|
|
198
|
+
if(items.length === 0) break
|
|
199
|
+
|
|
200
|
+
for(const item of items) {
|
|
201
|
+
await Connection.update(item.id, { status: 'offline' })
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
last = items[items.length - 1].id
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Step 5 – Grant access on entity creation
|
|
211
|
+
|
|
212
|
+
When a model uses `entity` with `writeAccessControl` / `readAccessControl`, the auto-generated CRUD checks roles but does **not** grant them. Add a change trigger to grant the creator `'owner'` after creation:
|
|
213
|
+
|
|
214
|
+
```js
|
|
215
|
+
definition.trigger({
|
|
216
|
+
name: 'changeMyService_MyModel',
|
|
217
|
+
properties: {
|
|
218
|
+
object: { type: MyModel, validation: ['nonEmpty'] },
|
|
219
|
+
data: { type: Object },
|
|
220
|
+
oldData: { type: Object }
|
|
221
|
+
},
|
|
222
|
+
async execute({ object, data, oldData }, { client, triggerService }) {
|
|
223
|
+
if (!data || oldData) return // only on create (data present, no oldData)
|
|
224
|
+
if (!client?.user) return
|
|
225
|
+
|
|
226
|
+
await triggerService({ service: 'accessControl', type: 'accessControl_setAccess' }, {
|
|
227
|
+
objectType: 'myService_MyModel', // format: serviceName_ModelName
|
|
228
|
+
object,
|
|
229
|
+
roles: ['owner'],
|
|
230
|
+
sessionOrUserType: 'user_User',
|
|
231
|
+
sessionOrUser: client.user,
|
|
232
|
+
lastUpdate: new Date()
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
For publicly accessible objects, also call `accessControl_setPublicAccess`:
|
|
239
|
+
|
|
240
|
+
```js
|
|
241
|
+
await triggerService({ service: 'accessControl', type: 'accessControl_setPublicAccess' }, {
|
|
242
|
+
objectType: 'myService_MyModel',
|
|
243
|
+
object,
|
|
244
|
+
userRoles: ['reader'], // roles for all logged-in users
|
|
245
|
+
sessionRoles: ['reader'], // roles for all sessions (including anonymous)
|
|
246
|
+
lastUpdate: new Date()
|
|
247
|
+
})
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Key points:
|
|
251
|
+
- `objectType` format: `serviceName_ModelName` (e.g. `company_Company`, `uploadedFiles_File`)
|
|
252
|
+
- `sessionOrUserType`: `'user_User'` for logged-in users, `'session_Session'` for anonymous
|
|
253
|
+
- For anonymous users: `sessionOrUser: client.session`
|
|
254
|
+
- Use `Promise.all([...])` when setting both public and per-user access
|
|
255
|
+
|
|
256
|
+
## Step 6 – Pending + resolve pattern for async results
|
|
257
|
+
|
|
258
|
+
Use this pattern when an action initiates a command that will be completed by an external process (device, worker, etc.) and you want the action to wait with a timeout.
|
|
259
|
+
|
|
260
|
+
### Steps
|
|
261
|
+
|
|
262
|
+
1. Implement a helper module with an in-memory `Map`:
|
|
263
|
+
- `waitForCommand(id, timeoutMs)` – returns a Promise,
|
|
264
|
+
- `resolveCommand(id, result)` – resolves and clears timeout.
|
|
265
|
+
2. In the main action:
|
|
266
|
+
- create a record with `status: 'pending'`,
|
|
267
|
+
- call `waitForCommand(id, timeoutMs)` and `return` the result.
|
|
268
|
+
3. In the reporting action:
|
|
269
|
+
- update the record (`status: 'completed'`, `result`),
|
|
270
|
+
- call `resolveCommand(id, result)`.
|
|
271
|
+
|
|
272
|
+
Helper sketch:
|
|
273
|
+
|
|
274
|
+
```js
|
|
275
|
+
const pendingCommands = new Map()
|
|
276
|
+
|
|
277
|
+
export function waitForCommand(commandId, timeoutMs = 115000) {
|
|
278
|
+
return new Promise((resolve, reject) => {
|
|
279
|
+
const timer = setTimeout(() => {
|
|
280
|
+
pendingCommands.delete(commandId)
|
|
281
|
+
reject(new Error('timeout'))
|
|
282
|
+
}, timeoutMs)
|
|
283
|
+
pendingCommands.set(commandId, { resolve, reject, timer })
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function resolveCommand(commandId, result) {
|
|
288
|
+
const pending = pendingCommands.get(commandId)
|
|
289
|
+
if(pending) {
|
|
290
|
+
clearTimeout(pending.timer)
|
|
291
|
+
pendingCommands.delete(commandId)
|
|
292
|
+
pending.resolve(result)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: live-change-design-models-relations
|
|
3
|
+
description: Design models with userItem, itemOf, propertyOf relations and access control
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Skill: live-change-design-models-relations (Claude Code)
|
|
7
|
+
|
|
8
|
+
Use this skill when you design or refactor **models and relations** in a LiveChange service.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
|
|
12
|
+
- You are adding a new model to a service.
|
|
13
|
+
- You want to switch from manual CRUD/views to proper relations.
|
|
14
|
+
- You need consistent access control and index usage.
|
|
15
|
+
|
|
16
|
+
## Step 1 – Decide the relation type
|
|
17
|
+
|
|
18
|
+
For each new model, decide how it relates to the rest of the domain:
|
|
19
|
+
|
|
20
|
+
- **`userItem`** – the object belongs to the signed-in user (e.g. user’s device).
|
|
21
|
+
- **`itemOf`** – a list of children belonging to a parent model (e.g. device connections).
|
|
22
|
+
- **`propertyOf`** – a single state object with the same id as the parent (e.g. cursor state).
|
|
23
|
+
- **no relation** – for global data or other special cases.
|
|
24
|
+
|
|
25
|
+
Choose one main relation; other associations can be plain fields + indexes.
|
|
26
|
+
|
|
27
|
+
## Step 2 – Define `properties` clearly
|
|
28
|
+
|
|
29
|
+
1. Use a **multi-line** style for properties, with clear `type`, `default`, `validation`, etc.
|
|
30
|
+
2. Avoid unreadable one-liners combining everything.
|
|
31
|
+
3. **Do NOT re-declare fields that are auto-added by relations.** Each relation automatically adds identifier fields and indexes:
|
|
32
|
+
|
|
33
|
+
| Relation | Auto-added field(s) | Auto-added index(es) |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| `itemOf: { what: Device }` | `device` | `byDevice` |
|
|
36
|
+
| `propertyOf: { what: Device }` | `device` | `byDevice` |
|
|
37
|
+
| `userItem` | `user` | `byUser` |
|
|
38
|
+
| `sessionOrUserProperty` | `sessionOrUserType`, `sessionOrUser` | `bySessionOrUser` |
|
|
39
|
+
| `propertyOfAny: { to: ['owner'] }` | `ownerType`, `owner` | `byOwner` |
|
|
40
|
+
|
|
41
|
+
Naming convention: parent model name with first letter lowercased (`Device` → `device`, `CostInvoice` → `costInvoice`). Polymorphic relations add a `Type` + value pair (e.g. `ownerType` + `owner`).
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
// ✅ Only define YOUR fields — 'device' is auto-added by itemOf
|
|
47
|
+
properties: {
|
|
48
|
+
name: {
|
|
49
|
+
type: String,
|
|
50
|
+
validation: ['nonEmpty']
|
|
51
|
+
},
|
|
52
|
+
status: {
|
|
53
|
+
type: String,
|
|
54
|
+
default: 'offline'
|
|
55
|
+
},
|
|
56
|
+
capabilities: {
|
|
57
|
+
type: Array,
|
|
58
|
+
of: {
|
|
59
|
+
type: String
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Step 3 – Configure the relation
|
|
66
|
+
|
|
67
|
+
### `userItem`
|
|
68
|
+
|
|
69
|
+
1. Add a `userItem` block inside the model definition.
|
|
70
|
+
2. Set roles for read/write and list which fields can be written.
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
userItem: {
|
|
74
|
+
readAccessControl: { roles: ['owner', 'admin'] },
|
|
75
|
+
writeAccessControl: { roles: ['owner', 'admin'] },
|
|
76
|
+
writeableProperties: ['name']
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### `itemOf`
|
|
81
|
+
|
|
82
|
+
1. Decide the parent model.
|
|
83
|
+
2. If the parent is in another service, declare it via `foreignModel` (see next step).
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
itemOf: {
|
|
87
|
+
what: Device,
|
|
88
|
+
readAccessControl: { roles: ['owner', 'admin'] },
|
|
89
|
+
writeAccessControl: { roles: ['owner', 'admin'] }
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### `propertyOf`
|
|
94
|
+
|
|
95
|
+
1. Use when the child should share the same id as the parent.
|
|
96
|
+
2. This simplifies lookups and avoids extra indexes.
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
propertyOf: {
|
|
100
|
+
what: Device,
|
|
101
|
+
readAccessControl: { roles: ['owner', 'admin'] },
|
|
102
|
+
writeAccessControl: { roles: ['owner', 'admin'] }
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### `propertyOf` with multiple parents (1:1 link to each)
|
|
107
|
+
|
|
108
|
+
Use this when a model should act as a dedicated 1:1 link between multiple entities (e.g. invoice ↔ contractor role links),
|
|
109
|
+
so the relations/CRUD generator can treat it as a relation rather than a plain `someId` property.
|
|
110
|
+
|
|
111
|
+
Notes:
|
|
112
|
+
|
|
113
|
+
- Usually you’ll have 1–2 parents, but the `propertyOf` list may contain **any number** of parent models (including 3+).
|
|
114
|
+
- If the entity is a relation, avoid adding manual `...Id` fields in `properties` just to represent the link — CRUD generators won’t treat it as a relation.
|
|
115
|
+
|
|
116
|
+
Example:
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
const CostInvoice = definition.foreignModel('invoice', 'CostInvoice')
|
|
120
|
+
const Contractor = definition.foreignModel('company', 'Contractor')
|
|
121
|
+
|
|
122
|
+
definition.model({
|
|
123
|
+
name: 'Supplier',
|
|
124
|
+
properties: {
|
|
125
|
+
// optional extra fields
|
|
126
|
+
},
|
|
127
|
+
propertyOf: [
|
|
128
|
+
{ what: CostInvoice },
|
|
129
|
+
{ what: Contractor }
|
|
130
|
+
]
|
|
131
|
+
})
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Step 4 – Use `foreignModel` for cross-service relations
|
|
135
|
+
|
|
136
|
+
1. At the top of the domain file, declare:
|
|
137
|
+
|
|
138
|
+
```js
|
|
139
|
+
const Device = definition.foreignModel('deviceManager', 'Device')
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
2. Then use `Device` in `itemOf` or `propertyOf`:
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
itemOf: {
|
|
146
|
+
what: Device,
|
|
147
|
+
readAccessControl: { roles: ['owner', 'admin'] }
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Step 5 – Add indexes
|
|
152
|
+
|
|
153
|
+
1. Identify frequent queries:
|
|
154
|
+
- by a single field (e.g. `sessionKey`),
|
|
155
|
+
- by combinations (e.g. `(device, status)`).
|
|
156
|
+
2. Declare indexes in the model:
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
indexes: {
|
|
160
|
+
bySessionKey: {
|
|
161
|
+
property: ['sessionKey']
|
|
162
|
+
},
|
|
163
|
+
byDeviceAndStatus: {
|
|
164
|
+
property: ['device', 'status']
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
3. Use these indexes in views/actions, via `indexObjectGet` / `indexRangeGet`.
|
|
170
|
+
|
|
171
|
+
## Step 6 – Set access control on relations
|
|
172
|
+
|
|
173
|
+
1. For `userItem`, `itemOf`, and `propertyOf`, always define:
|
|
174
|
+
- `readAccessControl`,
|
|
175
|
+
- `writeAccessControl`.
|
|
176
|
+
2. Don’t rely on unspecified defaults; access rules should be explicit in the model.
|
|
177
|
+
|
|
178
|
+
## Step 7 – Grant access on `entity` model creation
|
|
179
|
+
|
|
180
|
+
Models with `entity` and `writeAccessControl` / `readAccessControl` check roles on every CRUD operation, but do **not** auto-grant roles to the creator. Without granting roles, the creator cannot even read their own object.
|
|
181
|
+
|
|
182
|
+
Add a change trigger that grants `'owner'` on creation:
|
|
183
|
+
|
|
184
|
+
```js
|
|
185
|
+
definition.trigger({
|
|
186
|
+
name: 'changeMyService_MyModel',
|
|
187
|
+
properties: {
|
|
188
|
+
object: { type: MyModel, validation: ['nonEmpty'] },
|
|
189
|
+
data: { type: Object },
|
|
190
|
+
oldData: { type: Object }
|
|
191
|
+
},
|
|
192
|
+
async execute({ object, data, oldData }, { client, triggerService }) {
|
|
193
|
+
if (!data || oldData) return // only on create
|
|
194
|
+
if (!client?.user) return
|
|
195
|
+
|
|
196
|
+
await triggerService({ service: 'accessControl', type: 'accessControl_setAccess' }, {
|
|
197
|
+
objectType: 'myService_MyModel', // format: serviceName_ModelName
|
|
198
|
+
object,
|
|
199
|
+
roles: ['owner'],
|
|
200
|
+
sessionOrUserType: 'user_User',
|
|
201
|
+
sessionOrUser: client.user,
|
|
202
|
+
lastUpdate: new Date()
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
For objects that should also be publicly readable, add `accessControl_setPublicAccess`:
|
|
209
|
+
|
|
210
|
+
```js
|
|
211
|
+
await triggerService({ service: 'accessControl', type: 'accessControl_setPublicAccess' }, {
|
|
212
|
+
objectType: 'myService_MyModel',
|
|
213
|
+
object,
|
|
214
|
+
userRoles: ['reader'], // roles for all logged-in users
|
|
215
|
+
sessionRoles: ['reader'], // roles for all sessions (including anonymous)
|
|
216
|
+
lastUpdate: new Date()
|
|
217
|
+
})
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**Note:** `userItem` and `itemOf`/`propertyOf` relations automatically handle access through the parent — you typically don't need manual `triggerService` calls for those. This step applies primarily to `entity` models.
|
|
221
|
+
|
|
222
|
+
## Step 8 – Check auto-generated views/actions
|
|
223
|
+
|
|
224
|
+
1. After adding relations, review the auto-generated views/actions:
|
|
225
|
+
- “my X” views and CRUD for `userItem`,
|
|
226
|
+
- parent-scoped lists for `itemOf`/`propertyOf`.
|
|
227
|
+
2. Only add custom views/actions when:
|
|
228
|
+
- you need special filters,
|
|
229
|
+
- or custom logic not covered by the generated ones.
|
|
230
|
+
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: live-change-design-service
|
|
3
|
+
description: Create or restructure a LiveChange backend service with proper directory layout
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Skill: live-change-design-service
|
|
7
|
+
|
|
8
|
+
Ten skill opisuje **krok po kroku**, jak zaprojektować nowy serwis w LiveChange / live-change-stack albo sensownie rozbudować istniejący.
|
|
9
|
+
|
|
10
|
+
## Kiedy używać
|
|
11
|
+
|
|
12
|
+
Użyj tego skilla, gdy:
|
|
13
|
+
|
|
14
|
+
- dodajesz **nowy serwis domenowy** do projektu,
|
|
15
|
+
- przenosisz większy kawałek logiki z innego serwisu,
|
|
16
|
+
- potrzebujesz upewnić się, że struktura plików i rejestracja serwisu są zgodne z konwencją.
|
|
17
|
+
|
|
18
|
+
## Kroki – nowy serwis
|
|
19
|
+
|
|
20
|
+
1. **Nazwij serwis**
|
|
21
|
+
- Wybierz zwięzłą, domenową nazwę, np. `payments`, `notifications`, `deviceManager`.
|
|
22
|
+
- Nazwa będzie używana jako `name` w `createServiceDefinition` oraz w `app.config.js`.
|
|
23
|
+
|
|
24
|
+
2. **Utwórz katalog serwisu**
|
|
25
|
+
- Ścieżka: `server/services/<serviceName>/`.
|
|
26
|
+
- W katalogu utwórz pliki:
|
|
27
|
+
- `definition.js`
|
|
28
|
+
- `index.js`
|
|
29
|
+
- opcjonalnie `config.js`
|
|
30
|
+
- pliki domenowe (np. `models.js`, `authenticator.js`, `actions.js`), jeśli potrzebne.
|
|
31
|
+
|
|
32
|
+
3. **Zaimplementuj `definition.js`**
|
|
33
|
+
- Importuj `app` z `@live-change/framework`.
|
|
34
|
+
- Jeśli serwis korzysta z relacji lub access control:
|
|
35
|
+
- importuj `relationsPlugin` z `@live-change/relations-plugin`,
|
|
36
|
+
- importuj `accessControlService` z `@live-change/access-control-service`.
|
|
37
|
+
- Wywołaj `app.createServiceDefinition({ name, use })`.
|
|
38
|
+
- **Nie** deklaruj tu modeli, akcji ani widoków.
|
|
39
|
+
|
|
40
|
+
4. **Zaimplementuj `index.js`**
|
|
41
|
+
- Importuj `definition` z `./definition.js`.
|
|
42
|
+
- Importuj wszystkie pliki domenowe (np. `./models.js`, `./authenticator.js`).
|
|
43
|
+
- Eksportuj `definition` jako `default`.
|
|
44
|
+
- Nie dodawaj innej logiki do `index.js`.
|
|
45
|
+
|
|
46
|
+
5. **(Opcjonalnie) utwórz `config.js`**
|
|
47
|
+
- Importuj `definition`.
|
|
48
|
+
- Odczytaj `definition.config` i rozwiąż wartości domyślne.
|
|
49
|
+
- Eksportuj plain object z konfiguracją serwisu.
|
|
50
|
+
|
|
51
|
+
6. **Dodaj serwis do `services.list.js`**
|
|
52
|
+
- Importuj z katalogu serwisu, nie z pojedynczego pliku.
|
|
53
|
+
- Dodaj serwis do eksportowanego obiektu.
|
|
54
|
+
|
|
55
|
+
7. **Dodaj serwis do `app.config.js`**
|
|
56
|
+
- W sekcji `services` dodaj `{ name: '<serviceName>' }`.
|
|
57
|
+
- Upewnij się, że kolejność jest sensowna:
|
|
58
|
+
- serwisy bazowe/plugins (user, session, accessControl) na początku,
|
|
59
|
+
- serwisy domenowe zależne od nich – dalej.
|
|
60
|
+
|
|
61
|
+
8. **Sprawdź zależności**
|
|
62
|
+
- Jeśli serwis korzysta z modeli w innych serwisach:
|
|
63
|
+
- użyj `definition.foreignModel` wewnątrz domenowych plików serwisu,
|
|
64
|
+
- nie importuj bezpośrednio ich plików modeli.
|
|
65
|
+
- Upewnij się, że serwisy, od których zależysz, są wcześniejsze w `app.config.js`.
|
|
66
|
+
|
|
67
|
+
## Kroki – rozbudowa istniejącego serwisu
|
|
68
|
+
|
|
69
|
+
1. Przejrzyj istniejący katalog `server/services/<serviceName>/`.
|
|
70
|
+
2. Sprawdź, czy `definition.js` ma poprawne `use` (relacje, accessControl).
|
|
71
|
+
3. Nowe modele/akcje/widoki/triggery dodaj do **osobnych plików domenowych**:
|
|
72
|
+
- jeśli logika jest powiązana z istniejącym modelem – do jego pliku,
|
|
73
|
+
- jeśli tworzysz większy nowy obszar – do nowego pliku (np. `notifications.js`).
|
|
74
|
+
4. Upewnij się, że nowy plik jest importowany w `index.js`.
|
|
75
|
+
5. Nie dodawaj ciężkiej logiki do `definition.js` ani `index.js`.
|
|
76
|
+
|