@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
|
@@ -1,46 +1,41 @@
|
|
|
1
1
|
---
|
|
2
|
+
name: live-change-design-actions-views-triggers
|
|
2
3
|
description: Design actions, views, triggers with indexes and batch processing patterns
|
|
3
4
|
---
|
|
4
5
|
|
|
5
|
-
# Skill: live-change-design-actions-views-triggers
|
|
6
|
+
# Skill: live-change-design-actions-views-triggers (Claude Code)
|
|
6
7
|
|
|
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.
|
|
8
9
|
|
|
9
|
-
##
|
|
10
|
+
## When to use
|
|
10
11
|
|
|
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).
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
- tworzysz widoki (szczególnie zakresowe) dla list,
|
|
15
|
-
- implementujesz triggery (online/offline, batchowe przetwarzanie, asynchroniczne wyniki).
|
|
16
|
+
## Step 1 – Design an action
|
|
16
17
|
|
|
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.
|
|
18
31
|
|
|
19
|
-
|
|
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
|
|
32
|
+
Example:
|
|
36
33
|
|
|
37
34
|
```js
|
|
38
35
|
definition.action({
|
|
39
36
|
name: 'someAction',
|
|
40
37
|
properties: {
|
|
41
|
-
someKey: {
|
|
42
|
-
type: String
|
|
43
|
-
}
|
|
38
|
+
someKey: { type: String }
|
|
44
39
|
},
|
|
45
40
|
async execute({ someKey }, { client, service }) {
|
|
46
41
|
const obj = await SomeModel.indexObjectGet('bySomeKey', { someKey })
|
|
@@ -49,7 +44,7 @@ definition.action({
|
|
|
49
44
|
const id = app.generateUid()
|
|
50
45
|
|
|
51
46
|
await SomeOtherModel.create({
|
|
52
|
-
id
|
|
47
|
+
id
|
|
53
48
|
// ...
|
|
54
49
|
})
|
|
55
50
|
|
|
@@ -58,56 +53,106 @@ definition.action({
|
|
|
58
53
|
})
|
|
59
54
|
```
|
|
60
55
|
|
|
61
|
-
## 2
|
|
56
|
+
## Step 2 – Design a view
|
|
62
57
|
|
|
63
|
-
1.
|
|
64
|
-
- pojedynczy: użyj `get` lub `indexObjectGet`,
|
|
65
|
-
- lista: użyj `indexRangeGet` z indeksem.
|
|
58
|
+
1. Decide what kind of data source you have, then pick the **view variant** (exactly one):
|
|
66
59
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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`. |
|
|
70
65
|
|
|
71
|
-
|
|
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
|
|
72
75
|
|
|
73
|
-
|
|
76
|
+
### Example: `daoPath` (preferred, DAO-backed)
|
|
74
77
|
|
|
75
78
|
```js
|
|
76
79
|
definition.view({
|
|
77
|
-
name: '
|
|
80
|
+
name: 'costInvoice',
|
|
78
81
|
properties: {
|
|
79
|
-
|
|
82
|
+
costInvoice: {
|
|
80
83
|
type: String
|
|
81
84
|
}
|
|
82
85
|
},
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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)
|
|
87
122
|
}
|
|
88
123
|
})
|
|
89
124
|
```
|
|
90
125
|
|
|
91
|
-
|
|
126
|
+
### Anti-pattern: `get` without `observable` (do not do this)
|
|
92
127
|
|
|
93
|
-
|
|
94
|
-
|
|
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
|
+
```
|
|
95
140
|
|
|
96
|
-
|
|
97
|
-
- triggery online/offline zwykle potrzebują tylko ID obiektu.
|
|
141
|
+
## Step 3 – Online/offline triggers
|
|
98
142
|
|
|
99
|
-
|
|
100
|
-
-
|
|
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.).
|
|
101
148
|
|
|
102
|
-
|
|
149
|
+
Example:
|
|
103
150
|
|
|
104
151
|
```js
|
|
105
152
|
definition.trigger({
|
|
106
153
|
name: 'sessionConnectionOnline',
|
|
107
154
|
properties: {
|
|
108
|
-
connection: {
|
|
109
|
-
type: String
|
|
110
|
-
}
|
|
155
|
+
connection: { type: String }
|
|
111
156
|
},
|
|
112
157
|
async execute({ connection }, { service }) {
|
|
113
158
|
await Connection.update(connection, {
|
|
@@ -116,17 +161,29 @@ definition.trigger({
|
|
|
116
161
|
})
|
|
117
162
|
}
|
|
118
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
|
+
})
|
|
119
176
|
```
|
|
120
177
|
|
|
121
|
-
## 4
|
|
178
|
+
## Step 4 – Batch triggers (avoid full scans)
|
|
122
179
|
|
|
123
|
-
1.
|
|
124
|
-
2.
|
|
125
|
-
-
|
|
126
|
-
-
|
|
127
|
-
|
|
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.
|
|
128
185
|
|
|
129
|
-
|
|
186
|
+
Example:
|
|
130
187
|
|
|
131
188
|
```js
|
|
132
189
|
definition.trigger({
|
|
@@ -141,9 +198,7 @@ definition.trigger({
|
|
|
141
198
|
if(items.length === 0) break
|
|
142
199
|
|
|
143
200
|
for(const item of items) {
|
|
144
|
-
await Connection.update(item.id, {
|
|
145
|
-
status: 'offline'
|
|
146
|
-
})
|
|
201
|
+
await Connection.update(item.id, { status: 'offline' })
|
|
147
202
|
}
|
|
148
203
|
|
|
149
204
|
last = items[items.length - 1].id
|
|
@@ -152,25 +207,69 @@ definition.trigger({
|
|
|
152
207
|
})
|
|
153
208
|
```
|
|
154
209
|
|
|
155
|
-
## 5
|
|
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
|
|
156
255
|
|
|
157
|
-
|
|
256
|
+
## Step 6 – Pending + resolve pattern for async results
|
|
158
257
|
|
|
159
|
-
|
|
160
|
-
- wynik przychodzi później z innego procesu (urządzenie, worker, itp.),
|
|
161
|
-
- chcesz, żeby akcja czekała na wynik z timeoutem.
|
|
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.
|
|
162
259
|
|
|
163
|
-
###
|
|
260
|
+
### Steps
|
|
164
261
|
|
|
165
|
-
1.
|
|
166
|
-
|
|
167
|
-
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
-
|
|
171
|
-
|
|
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)`.
|
|
172
271
|
|
|
173
|
-
|
|
272
|
+
Helper sketch:
|
|
174
273
|
|
|
175
274
|
```js
|
|
176
275
|
const pendingCommands = new Map()
|
|
@@ -1,39 +1,49 @@
|
|
|
1
1
|
---
|
|
2
|
+
name: live-change-design-models-relations
|
|
2
3
|
description: Design models with userItem, itemOf, propertyOf relations and access control
|
|
3
4
|
---
|
|
4
5
|
|
|
5
|
-
# Skill: live-change-design-models-relations
|
|
6
|
+
# Skill: live-change-design-models-relations (Claude Code)
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
Use this skill when you design or refactor **models and relations** in a LiveChange service.
|
|
8
9
|
|
|
9
|
-
##
|
|
10
|
+
## When to use
|
|
10
11
|
|
|
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.
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
- przenosisz dane z ręcznego CRUD na relacje,
|
|
15
|
-
- potrzebujesz spójnego wzorca dla access control i indeksów.
|
|
16
|
+
## Step 1 – Decide the relation type
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
For each new model, decide how it relates to the rest of the domain:
|
|
18
19
|
|
|
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.
|
|
20
24
|
|
|
21
|
-
|
|
22
|
-
- **`itemOf`** – lista elementów należy do innego modelu (np. połączenia urządzenia, pozycje zamówienia).
|
|
23
|
-
- **`propertyOf`** – pojedyncza właściwość/model ze stanem o ID równym rodzicowi (np. stan kursora, ustawienia).
|
|
24
|
-
- **bez relacji** – gdy obiekt jest globalny lub ma inną strukturę (np. globalny config).
|
|
25
|
+
Choose one main relation; other associations can be plain fields + indexes.
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
## Step 2 – Define `properties` clearly
|
|
27
28
|
|
|
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:
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
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` |
|
|
33
40
|
|
|
34
|
-
|
|
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:
|
|
35
44
|
|
|
36
45
|
```js
|
|
46
|
+
// ✅ Only define YOUR fields — 'device' is auto-added by itemOf
|
|
37
47
|
properties: {
|
|
38
48
|
name: {
|
|
39
49
|
type: String,
|
|
@@ -52,13 +62,12 @@ properties: {
|
|
|
52
62
|
}
|
|
53
63
|
```
|
|
54
64
|
|
|
55
|
-
## 3
|
|
65
|
+
## Step 3 – Configure the relation
|
|
56
66
|
|
|
57
67
|
### `userItem`
|
|
58
68
|
|
|
59
|
-
1.
|
|
60
|
-
2.
|
|
61
|
-
3. Ogranicz `writeableProperties` do pól, które użytkownik może zmieniać.
|
|
69
|
+
1. Add a `userItem` block inside the model definition.
|
|
70
|
+
2. Set roles for read/write and list which fields can be written.
|
|
62
71
|
|
|
63
72
|
```js
|
|
64
73
|
userItem: {
|
|
@@ -70,8 +79,8 @@ userItem: {
|
|
|
70
79
|
|
|
71
80
|
### `itemOf`
|
|
72
81
|
|
|
73
|
-
1.
|
|
74
|
-
2.
|
|
82
|
+
1. Decide the parent model.
|
|
83
|
+
2. If the parent is in another service, declare it via `foreignModel` (see next step).
|
|
75
84
|
|
|
76
85
|
```js
|
|
77
86
|
itemOf: {
|
|
@@ -83,8 +92,8 @@ itemOf: {
|
|
|
83
92
|
|
|
84
93
|
### `propertyOf`
|
|
85
94
|
|
|
86
|
-
1.
|
|
87
|
-
2.
|
|
95
|
+
1. Use when the child should share the same id as the parent.
|
|
96
|
+
2. This simplifies lookups and avoids extra indexes.
|
|
88
97
|
|
|
89
98
|
```js
|
|
90
99
|
propertyOf: {
|
|
@@ -94,17 +103,17 @@ propertyOf: {
|
|
|
94
103
|
}
|
|
95
104
|
```
|
|
96
105
|
|
|
97
|
-
### `propertyOf`
|
|
106
|
+
### `propertyOf` with multiple parents (1:1 link to each)
|
|
98
107
|
|
|
99
|
-
|
|
100
|
-
|
|
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.
|
|
101
110
|
|
|
102
|
-
|
|
111
|
+
Notes:
|
|
103
112
|
|
|
104
|
-
-
|
|
105
|
-
-
|
|
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.
|
|
106
115
|
|
|
107
|
-
|
|
116
|
+
Example:
|
|
108
117
|
|
|
109
118
|
```js
|
|
110
119
|
const CostInvoice = definition.foreignModel('invoice', 'CostInvoice')
|
|
@@ -113,7 +122,7 @@ const Contractor = definition.foreignModel('company', 'Contractor')
|
|
|
113
122
|
definition.model({
|
|
114
123
|
name: 'Supplier',
|
|
115
124
|
properties: {
|
|
116
|
-
//
|
|
125
|
+
// optional extra fields
|
|
117
126
|
},
|
|
118
127
|
propertyOf: [
|
|
119
128
|
{ what: CostInvoice },
|
|
@@ -122,20 +131,29 @@ definition.model({
|
|
|
122
131
|
})
|
|
123
132
|
```
|
|
124
133
|
|
|
125
|
-
|
|
134
|
+
## Step 4 – Use `foreignModel` for cross-service relations
|
|
126
135
|
|
|
127
|
-
1.
|
|
136
|
+
1. At the top of the domain file, declare:
|
|
128
137
|
|
|
129
138
|
```js
|
|
130
139
|
const Device = definition.foreignModel('deviceManager', 'Device')
|
|
131
140
|
```
|
|
132
141
|
|
|
133
|
-
2.
|
|
142
|
+
2. Then use `Device` in `itemOf` or `propertyOf`:
|
|
134
143
|
|
|
135
|
-
|
|
144
|
+
```js
|
|
145
|
+
itemOf: {
|
|
146
|
+
what: Device,
|
|
147
|
+
readAccessControl: { roles: ['owner', 'admin'] }
|
|
148
|
+
}
|
|
149
|
+
```
|
|
136
150
|
|
|
137
|
-
|
|
138
|
-
|
|
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:
|
|
139
157
|
|
|
140
158
|
```js
|
|
141
159
|
indexes: {
|
|
@@ -148,21 +166,65 @@ indexes: {
|
|
|
148
166
|
}
|
|
149
167
|
```
|
|
150
168
|
|
|
151
|
-
3.
|
|
169
|
+
3. Use these indexes in views/actions, via `indexObjectGet` / `indexRangeGet`.
|
|
152
170
|
|
|
153
|
-
##
|
|
171
|
+
## Step 6 – Set access control on relations
|
|
154
172
|
|
|
155
|
-
1.
|
|
173
|
+
1. For `userItem`, `itemOf`, and `propertyOf`, always define:
|
|
156
174
|
- `readAccessControl`,
|
|
157
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.
|
|
158
181
|
|
|
159
|
-
|
|
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
|
+
```
|
|
160
219
|
|
|
161
|
-
|
|
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.
|
|
162
221
|
|
|
163
|
-
|
|
222
|
+
## Step 8 – Check auto-generated views/actions
|
|
164
223
|
|
|
165
|
-
1.
|
|
166
|
-
|
|
167
|
-
-
|
|
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.
|
|
168
230
|
|