@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.
Files changed (44) hide show
  1. package/.claude/rules/live-change-backend-actions-views-triggers.md +62 -0
  2. package/.claude/rules/live-change-backend-event-sourcing.md +186 -0
  3. package/.claude/rules/live-change-backend-models-and-relations.md +72 -0
  4. package/.claude/rules/live-change-frontend-vue-primevue.md +26 -0
  5. package/.claude/settings.json +32 -0
  6. package/.claude/skills/create-skills-and-rules/SKILL.md +248 -0
  7. package/.claude/skills/live-change-backend-change-triggers/SKILL.md +186 -0
  8. package/.claude/skills/live-change-design-actions-views-triggers/SKILL.md +462 -0
  9. package/.claude/skills/live-change-design-models-relations/SKILL.md +230 -0
  10. package/.claude/skills/live-change-design-service/SKILL.md +133 -0
  11. package/.claude/skills/live-change-frontend-accessible-objects/SKILL.md +384 -0
  12. package/.claude/skills/live-change-frontend-accessible-objects.md +383 -0
  13. package/.claude/skills/live-change-frontend-action-buttons/SKILL.md +129 -0
  14. package/.claude/skills/live-change-frontend-action-form/SKILL.md +149 -0
  15. package/.claude/skills/live-change-frontend-analytics/SKILL.md +147 -0
  16. package/.claude/skills/live-change-frontend-command-forms/SKILL.md +216 -0
  17. package/.claude/skills/live-change-frontend-data-views/SKILL.md +183 -0
  18. package/.claude/skills/live-change-frontend-editor-form/SKILL.md +240 -0
  19. package/.claude/skills/live-change-frontend-locale-time/SKILL.md +172 -0
  20. package/.claude/skills/live-change-frontend-page-list-detail/SKILL.md +201 -0
  21. package/.claude/skills/live-change-frontend-range-list/SKILL.md +129 -0
  22. package/.claude/skills/live-change-frontend-ssr-setup/SKILL.md +119 -0
  23. package/.cursor/rules/live-change-backend-actions-views-triggers.mdc +88 -0
  24. package/.cursor/rules/live-change-backend-event-sourcing.mdc +185 -0
  25. package/.cursor/rules/live-change-backend-models-and-relations.mdc +62 -0
  26. package/.cursor/skills/create-skills-and-rules.md +248 -0
  27. package/.cursor/skills/live-change-backend-change-triggers.md +186 -0
  28. package/.cursor/skills/live-change-design-actions-views-triggers.md +178 -79
  29. package/.cursor/skills/live-change-design-models-relations.md +112 -50
  30. package/.cursor/skills/live-change-design-service.md +1 -0
  31. package/.cursor/skills/live-change-frontend-accessible-objects.md +384 -0
  32. package/.cursor/skills/live-change-frontend-action-buttons.md +1 -0
  33. package/.cursor/skills/live-change-frontend-action-form.md +9 -3
  34. package/.cursor/skills/live-change-frontend-analytics.md +1 -0
  35. package/.cursor/skills/live-change-frontend-command-forms.md +1 -0
  36. package/.cursor/skills/live-change-frontend-data-views.md +1 -0
  37. package/.cursor/skills/live-change-frontend-editor-form.md +135 -72
  38. package/.cursor/skills/live-change-frontend-locale-time.md +1 -0
  39. package/.cursor/skills/live-change-frontend-page-list-detail.md +1 -0
  40. package/.cursor/skills/live-change-frontend-range-list.md +1 -0
  41. package/.cursor/skills/live-change-frontend-ssr-setup.md +1 -0
  42. package/front/src/router.js +2 -1
  43. package/opencode.json +10 -0
  44. package/package.json +52 -50
@@ -0,0 +1,186 @@
1
+ ---
2
+ name: live-change-backend-change-triggers
3
+ description: React to model changes with automatic change triggers from the relations plugin
4
+ ---
5
+
6
+ # Skill: live-change-backend-change-triggers (Claude Code)
7
+
8
+ Use this skill when you need to **react to model changes** — run logic when a record is created, updated, or deleted.
9
+
10
+ ## When to use
11
+
12
+ - You need to keep derived data in sync when a model changes (e.g. create/cancel a timer when a schedule is created/updated/deleted).
13
+ - You want to initialize related resources when a model is created.
14
+ - You need cross-service reactions to model lifecycle events.
15
+ - You want custom cleanup logic on delete.
16
+
17
+ ## How it works
18
+
19
+ The relations plugin automatically fires change triggers for every model that uses relations (`propertyOf`, `itemOf`, `userItem`, `propertyOfAny`, etc.). You just define a trigger with the matching name.
20
+
21
+ ## Step 1 – Understand the naming convention
22
+
23
+ For a model `MyModel` in service `myService`, these triggers are fired automatically:
24
+
25
+ | Trigger name | When |
26
+ |---|---|
27
+ | `createMyService_MyModel` | On create |
28
+ | `updateMyService_MyModel` | On update |
29
+ | `deleteMyService_MyModel` | On delete |
30
+ | `changeMyService_MyModel` | On any change |
31
+ | `createObject` / `updateObject` / `deleteObject` / `changeObject` | Generic (all models) |
32
+
33
+ The pattern is: `{changeType}{ServiceName}_{ModelName}` where service name is capitalized.
34
+
35
+ ## Step 2 – Define a change trigger (recommended: use `change*`)
36
+
37
+ The `change*` variant covers all cases. Check `data` and `oldData` to distinguish create/update/delete:
38
+
39
+ ```javascript
40
+ definition.trigger({
41
+ name: 'changeMyService_MyModel',
42
+ properties: {
43
+ object: {
44
+ type: MyModel,
45
+ validation: ['nonEmpty'],
46
+ },
47
+ data: {
48
+ type: Object,
49
+ },
50
+ oldData: {
51
+ type: Object,
52
+ }
53
+ },
54
+ async execute({ object, data, oldData }, { service, trigger, triggerService }, emit) {
55
+ if(oldData) {
56
+ // Updated or deleted — clean up old state
57
+ }
58
+ if(data) {
59
+ // Created or updated — set up new state
60
+ }
61
+ }
62
+ })
63
+ ```
64
+
65
+ How to distinguish:
66
+
67
+ | `oldData` | `data` | Meaning |
68
+ |---|---|---|
69
+ | `null` | `{...}` | Created |
70
+ | `{...}` | `{...}` | Updated |
71
+ | `{...}` | `null` | Deleted |
72
+
73
+ ## Step 3 – Real example: cron-service reacting to Schedule changes
74
+
75
+ The cron-service uses `changeCron_Schedule` to automatically manage timers when schedules are created, updated, or deleted:
76
+
77
+ ```javascript
78
+ // Source: live-change-stack/services/cron-service/schedule.js
79
+
80
+ definition.trigger({
81
+ name: 'changeCron_Schedule',
82
+ properties: {
83
+ object: {
84
+ type: Schedule,
85
+ validation: ['nonEmpty'],
86
+ },
87
+ data: {
88
+ type: Object,
89
+ },
90
+ oldData: {
91
+ type: Object,
92
+ }
93
+ },
94
+ execute: async ({ object, data, oldData }, { service, trigger, triggerService }, emit) => {
95
+ if(oldData) {
96
+ // Cancel old timer on update or delete
97
+ await triggerService({
98
+ service: 'timer',
99
+ type: 'cancelTimerIfExists',
100
+ }, {
101
+ timer: 'cron_Schedule_' + object
102
+ })
103
+ await ScheduleInfo.delete(object)
104
+ }
105
+ if(data) {
106
+ // Create new timer on create or update
107
+ await processSchedule({ id: object, ...data }, { triggerService })
108
+ }
109
+ }
110
+ })
111
+ ```
112
+
113
+ This means: when a user creates a Schedule via the UI or API, the timer is automatically set up. When they update it, the old timer is canceled and a new one created. When they delete it, the timer is canceled.
114
+
115
+ ## Step 4 – Specific lifecycle triggers (alternative)
116
+
117
+ If you only care about one lifecycle event, use the specific variant:
118
+
119
+ ```javascript
120
+ // React only to creation
121
+ definition.trigger({
122
+ name: 'createBilling_Billing',
123
+ properties: {
124
+ object: { type: Billing }
125
+ },
126
+ async execute({ object }, { triggerService }, emit) {
127
+ // Initialize balance when billing is created
128
+ const existingBalance = await app.serviceViewGet('balance', 'balance', {
129
+ ownerType: 'billing_Billing', owner: object
130
+ })
131
+ if(!existingBalance) {
132
+ await triggerService({
133
+ service: 'balance',
134
+ type: 'balance_setOrUpdateBalance',
135
+ }, { ownerType: 'billing_Billing', owner: object })
136
+ }
137
+ }
138
+ })
139
+ ```
140
+
141
+ ## Step 5 – Full trigger parameters
142
+
143
+ All change triggers receive:
144
+
145
+ ```javascript
146
+ {
147
+ objectType, // e.g. 'cron_Schedule' (service_Model)
148
+ object, // record ID
149
+ identifiers, // parent identifiers from the model's relations
150
+ data, // new data (null on delete)
151
+ oldData, // old data (null on create)
152
+ changeType // 'create', 'update', or 'delete'
153
+ }
154
+ ```
155
+
156
+ The `identifiers` object contains the parent references defined in the model's relations (e.g. for `itemOf: { what: Device }`, identifiers would include `{ device: '...' }`).
157
+
158
+ ## Step 6 – Cross-service triggers
159
+
160
+ Change triggers work across services. Define the trigger in any service — the framework routes it by name:
161
+
162
+ ```javascript
163
+ // In serviceA, react to changes in serviceB's Model
164
+ const SomeModel = definition.foreignModel('serviceB', 'SomeModel')
165
+
166
+ definition.trigger({
167
+ name: 'changeServiceB_SomeModel',
168
+ properties: {
169
+ object: { type: SomeModel },
170
+ data: { type: Object },
171
+ oldData: { type: Object }
172
+ },
173
+ async execute({ object, data, oldData }, { triggerService }) {
174
+ // React to changes in SomeModel from serviceB
175
+ }
176
+ })
177
+ ```
178
+
179
+ ## Common patterns
180
+
181
+ | Pattern | Trigger to use | Example |
182
+ |---|---|---|
183
+ | Keep derived data in sync | `changeSvc_Model` | Cron: cancel/create timers on schedule change |
184
+ | Initialize on creation | `createSvc_Model` | Billing: create balance when billing created |
185
+ | Custom cleanup on delete | `deleteSvc_Model` | Custom: archive or notify before deletion |
186
+ | React to any model change | `changeObject` | Audit: log all changes across all models |
@@ -0,0 +1,462 @@
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
+
297
+ ## Step 7 – Enable access-control indexes for accessible objects
298
+
299
+ When you need **global listings of objects accessible to a user** (not limited to direct `userItem` relations), enable the indexed access-control pipeline and use the specialized views it exposes.
300
+
301
+ ### 7.1 Enable `indexed: true` in app config
302
+
303
+ In the app server config, set `indexed: true` on the access-control service:
304
+
305
+ ```js
306
+ // app.server/app.config.js
307
+ {
308
+ name: 'accessControl',
309
+ createSessionOnUpdate: true,
310
+ contactTypes,
311
+ indexed: true
312
+ }
313
+ ```
314
+
315
+ This activates the index pipeline and views defined in `services/access-control-service/indexes.js`.
316
+
317
+ ### 7.2 Index pipeline overview
318
+
319
+ When `indexed: true` is enabled, the following indexes are created:
320
+
321
+ 1. `childByParent` – objects by parent (`parentType`, `parent`, `childType`, `child`, `property`)
322
+ 2. `parentByChild` – parents by child (reverse of `childByParent`)
323
+ 3. `pathsByAncestorDescendantRelation` – all ancestor/descendant paths in the object tree
324
+ 4. `expandedRoles` – propagates roles assigned on ancestors down to all descendants
325
+ 5. `roleByOwnerAndObject` – deduplicated roles per `(sessionOrUser, object, role)`
326
+ 6. `objectByOwnerAndRole` – objects by `(sessionOrUser, role, objectType, object)`
327
+ 7. `ownerByObjectAndRole` – owners by `(objectType, object, role, sessionOrUser)`
328
+
329
+ These are maintained automatically based on:
330
+
331
+ - `Access` records (per-user roles),
332
+ - `PublicAccess` records (public roles),
333
+ - parent relations registered via the relations plugin.
334
+
335
+ ### 7.3 Self-service views (current user)
336
+
337
+ The following views do **not** need explicit `sessionOrUser` parameters – they infer the current user or session from `client`:
338
+
339
+ - `myAccessibleObjects({ objectType?, ...range })`
340
+ - `myAccessibleObjectsCount({ objectType?, ...range })`
341
+ - `myAccessibleObjectsByRole({ role, objectType?, ...range })`
342
+ - `myAccessibleObjectsByRoleCount({ role, objectType?, ...range })`
343
+
344
+ Use them for:
345
+
346
+ - “all companies I can access” (`objectType: 'company_Company'`),
347
+ - “all projects where I am owner” (`role: 'owner'`, `objectType: 'project_Project'`),
348
+ - dashboards listing entities across services.
349
+
350
+ On the frontend, the typical pattern is:
351
+
352
+ ```js
353
+ const accessibleObjectsPath = computed(() =>
354
+ client.value.user
355
+ ? path.accessControl.myAccessibleObjects({
356
+ objectType: 'myService_MyModel'
357
+ }).with(accessible =>
358
+ path.myService.myModel({ myModel: accessible.object }).bind('entity')
359
+ )
360
+ : null
361
+ )
362
+ ```
363
+
364
+ ### 7.4 Admin views (explicit user / session)
365
+
366
+ For administrative tools you can use:
367
+
368
+ - `accessibleObjects({ sessionOrUserType, sessionOrUser, objectType?, ...range })`
369
+ - `accessibleObjectsCount(...)`
370
+ - `accessibleObjectsByRole({ sessionOrUserType, sessionOrUser, role, objectType?, ...range })`
371
+ - `accessibleObjectsByRoleCount(...)`
372
+ - `objectAccesses({ objectType, object, role?, ...range })`
373
+ - `objectAccessesCount(...)`
374
+
375
+ These views:
376
+
377
+ - require the caller to have `admin` role (checked in access-control service),
378
+ - allow you to inspect which objects a user can access, and who can access a given object.
379
+
380
+ Example DAO path for a list view:
381
+
382
+ ```js
383
+ definition.view({
384
+ name: 'userProjectAccesses',
385
+ properties: {
386
+ user: { type: String }
387
+ },
388
+ daoPath({ user }, { client, service }) {
389
+ if(!client.roles.includes('admin')) throw new Error('forbidden')
390
+ const range = App.extractRange({})
391
+ return accessibleObjectsByRoleIndex.rangePath(
392
+ ['user_User', user, 'member', 'project_Project'],
393
+ range
394
+ )
395
+ }
396
+ })
397
+ ```
398
+
399
+ ### 7.5 Non-indexed views that are always available
400
+
401
+ Even without `indexed: true`, `services/access-control-service/view.js` defines views that work on the raw `Access` and invitation tables:
402
+
403
+ - `myAccessesByObjectType({ objectType, ...range })`
404
+ - `myAccessesByObjectTypeAndRole({ objectType, role, ...range })`
405
+ - `myAccessInvitationsByObjectType({ objectType, ...range })`
406
+ - `myAccessInvitationsByObjectTypeAndRole({ objectType, role, ...range })`
407
+
408
+ Use them when:
409
+
410
+ - you do not want to enable the full indexed pipeline yet,
411
+ - you mostly need **per-type** lists, not cross-type queries,
412
+ - you are building invitation lists or paginated access-based lists (`RangeViewer`).
413
+
414
+ These views are the backend counterparts of the frontend patterns described in `live-change-frontend-accessible-objects`.
415
+
416
+ Remember:
417
+
418
+ - `objectType` format is always `serviceName_ModelName`,
419
+ - for anonymous users use `sessionOrUserType: 'session_Session'` and `sessionOrUser: client.session`,
420
+ - enable `indexed: true` when you need efficient “all my objects” listings and admin inspection tools across large datasets.
421
+ ```
422
+ ## Step 6 – Pending + resolve pattern for async results
423
+
424
+ 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.
425
+
426
+ ### Steps
427
+
428
+ 1. Implement a helper module with an in-memory `Map`:
429
+ - `waitForCommand(id, timeoutMs)` – returns a Promise,
430
+ - `resolveCommand(id, result)` – resolves and clears timeout.
431
+ 2. In the main action:
432
+ - create a record with `status: 'pending'`,
433
+ - call `waitForCommand(id, timeoutMs)` and `return` the result.
434
+ 3. In the reporting action:
435
+ - update the record (`status: 'completed'`, `result`),
436
+ - call `resolveCommand(id, result)`.
437
+
438
+ Helper sketch:
439
+
440
+ ```js
441
+ const pendingCommands = new Map()
442
+
443
+ export function waitForCommand(commandId, timeoutMs = 115000) {
444
+ return new Promise((resolve, reject) => {
445
+ const timer = setTimeout(() => {
446
+ pendingCommands.delete(commandId)
447
+ reject(new Error('timeout'))
448
+ }, timeoutMs)
449
+ pendingCommands.set(commandId, { resolve, reject, timer })
450
+ })
451
+ }
452
+
453
+ export function resolveCommand(commandId, result) {
454
+ const pending = pendingCommands.get(commandId)
455
+ if(pending) {
456
+ clearTimeout(pending.timer)
457
+ pendingCommands.delete(commandId)
458
+ pending.resolve(result)
459
+ }
460
+ }
461
+ ```
462
+