@live-change/frontend-template 0.9.198 → 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.
Files changed (46) hide show
  1. package/.claude/rules/live-change-backend-actions-views-triggers.md +184 -0
  2. package/.claude/rules/live-change-backend-architecture.md +126 -0
  3. package/.claude/rules/live-change-backend-models-and-relations.md +188 -0
  4. package/.claude/rules/live-change-frontend-vue-primevue.md +291 -0
  5. package/.claude/rules/live-change-service-structure.md +89 -0
  6. package/.claude/skills/create-skills-and-rules.md +196 -0
  7. package/.claude/skills/live-change-design-actions-views-triggers.md +190 -0
  8. package/.claude/skills/live-change-design-models-relations.md +173 -0
  9. package/.claude/skills/live-change-design-service.md +132 -0
  10. package/.claude/skills/live-change-frontend-action-buttons.md +128 -0
  11. package/.claude/skills/live-change-frontend-action-form.md +143 -0
  12. package/.claude/skills/live-change-frontend-analytics.md +146 -0
  13. package/.claude/skills/live-change-frontend-command-forms.md +215 -0
  14. package/.claude/skills/live-change-frontend-data-views.md +182 -0
  15. package/.claude/skills/live-change-frontend-editor-form.md +177 -0
  16. package/.claude/skills/live-change-frontend-locale-time.md +171 -0
  17. package/.claude/skills/live-change-frontend-page-list-detail.md +200 -0
  18. package/.claude/skills/live-change-frontend-range-list.md +128 -0
  19. package/.claude/skills/live-change-frontend-ssr-setup.md +118 -0
  20. package/.cursor/rules/live-change-backend-actions-views-triggers.mdc +202 -0
  21. package/.cursor/rules/live-change-backend-architecture.mdc +131 -0
  22. package/.cursor/rules/live-change-backend-models-and-relations.mdc +194 -0
  23. package/.cursor/rules/live-change-frontend-vue-primevue.mdc +290 -0
  24. package/.cursor/rules/live-change-service-structure.mdc +107 -0
  25. package/.cursor/skills/live-change-design-actions-views-triggers.md +197 -0
  26. package/.cursor/skills/live-change-design-models-relations.md +168 -0
  27. package/.cursor/skills/live-change-design-service.md +75 -0
  28. package/.cursor/skills/live-change-frontend-action-buttons.md +128 -0
  29. package/.cursor/skills/live-change-frontend-action-form.md +143 -0
  30. package/.cursor/skills/live-change-frontend-analytics.md +146 -0
  31. package/.cursor/skills/live-change-frontend-command-forms.md +215 -0
  32. package/.cursor/skills/live-change-frontend-data-views.md +182 -0
  33. package/.cursor/skills/live-change-frontend-editor-form.md +177 -0
  34. package/.cursor/skills/live-change-frontend-locale-time.md +171 -0
  35. package/.cursor/skills/live-change-frontend-page-list-detail.md +200 -0
  36. package/.cursor/skills/live-change-frontend-range-list.md +128 -0
  37. package/.cursor/skills/live-change-frontend-ssr-setup.md +119 -0
  38. package/README.md +71 -0
  39. package/package.json +50 -50
  40. package/server/app.config.js +35 -0
  41. package/server/services.list.js +2 -0
  42. package/.nx/workspace-data/file-map.json +0 -195
  43. package/.nx/workspace-data/nx_files.nxt +0 -0
  44. package/.nx/workspace-data/project-graph.json +0 -8
  45. package/.nx/workspace-data/project-graph.lock +0 -0
  46. package/.nx/workspace-data/source-maps.json +0 -1
@@ -0,0 +1,190 @@
1
+ ---
2
+ description: Design actions, views, triggers with indexes and batch processing patterns
3
+ ---
4
+
5
+ # Skill: live-change-design-actions-views-triggers (Claude Code)
6
+
7
+ 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
+ ## When to use
10
+
11
+ - You add or change actions on existing models.
12
+ - You define new views (especially list/range views).
13
+ - You implement triggers (online/offline, batch processing, async result flows).
14
+
15
+ ## Step 1 – Design an action
16
+
17
+ 1. **Clarify the goal**:
18
+ - create / update / delete a record,
19
+ - or create a “command” that will be completed later.
20
+ 2. **Define `properties`** clearly:
21
+ - only include what the client must provide,
22
+ - fetch the rest from the database via indexes.
23
+ 3. **Use indexes**, not full scans:
24
+ - `indexObjectGet('bySomething', { ... })` for single-object lookups,
25
+ - `indexRangeGet('bySomething', { ... })` for lists.
26
+ 4. **Return a useful result**:
27
+ - new object id,
28
+ - session keys,
29
+ - any data needed for the next step.
30
+
31
+ Example:
32
+
33
+ ```js
34
+ definition.action({
35
+ name: 'someAction',
36
+ properties: {
37
+ someKey: { type: String }
38
+ },
39
+ async execute({ someKey }, { client, service }) {
40
+ const obj = await SomeModel.indexObjectGet('bySomeKey', { someKey })
41
+ if(!obj) throw new Error('notFound')
42
+
43
+ const id = app.generateUid()
44
+
45
+ await SomeOtherModel.create({
46
+ id
47
+ // ...
48
+ })
49
+
50
+ return { id }
51
+ }
52
+ })
53
+ ```
54
+
55
+ ## Step 2 – Design a view
56
+
57
+ 1. Decide if you need:
58
+ - a **single** object view, or
59
+ - a **list/range** view.
60
+ 2. Define `properties` for the view:
61
+ - only parameters needed for filtering,
62
+ - types consistent with model fields.
63
+ 3. Use the right primitive:
64
+ - `get` / `indexObjectGet` for single object,
65
+ - `indexRangeGet` for lists.
66
+
67
+ Range view example:
68
+
69
+ ```js
70
+ definition.view({
71
+ name: 'myItemsByStatus',
72
+ properties: {
73
+ status: { type: String }
74
+ },
75
+ async get({ status }, { client, service }) {
76
+ return MyModel.indexRangeGet('byStatus', { status })
77
+ }
78
+ })
79
+ ```
80
+
81
+ ## Step 3 – Online/offline triggers
82
+
83
+ 1. Identify events:
84
+ - session or connection goes online,
85
+ - session or connection goes offline.
86
+ 2. Define triggers with minimal `properties` (usually just an id).
87
+ 3. Update only the necessary fields (`status`, `lastSeenAt`, etc.).
88
+
89
+ Example:
90
+
91
+ ```js
92
+ definition.trigger({
93
+ name: 'sessionConnectionOnline',
94
+ properties: {
95
+ connection: { type: String }
96
+ },
97
+ async execute({ connection }, { service }) {
98
+ await Connection.update(connection, {
99
+ status: 'online',
100
+ lastSeenAt: new Date()
101
+ })
102
+ }
103
+ })
104
+
105
+ definition.trigger({
106
+ name: 'sessionConnectionOffline',
107
+ properties: {
108
+ connection: { type: String }
109
+ },
110
+ async execute({ connection }, { service }) {
111
+ await Connection.update(connection, {
112
+ status: 'offline'
113
+ })
114
+ }
115
+ })
116
+ ```
117
+
118
+ ## Step 4 – Batch triggers (avoid full scans)
119
+
120
+ 1. Pick a **batch size** (e.g. 32 or 128).
121
+ 2. Use `rangeGet` with `gt: lastId` in a loop:
122
+ - start with `last = ''`,
123
+ - after each batch, set `last` to the last record’s id,
124
+ - stop when the batch is empty.
125
+
126
+ Example:
127
+
128
+ ```js
129
+ definition.trigger({
130
+ name: 'allOffline',
131
+ async execute({}, { service }) {
132
+ let last = ''
133
+ while(true) {
134
+ const items = await Connection.rangeGet({
135
+ gt: last,
136
+ limit: 32
137
+ })
138
+ if(items.length === 0) break
139
+
140
+ for(const item of items) {
141
+ await Connection.update(item.id, { status: 'offline' })
142
+ }
143
+
144
+ last = items[items.length - 1].id
145
+ }
146
+ }
147
+ })
148
+ ```
149
+
150
+ ## Step 5 – Pending + resolve pattern for async results
151
+
152
+ 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.
153
+
154
+ ### Steps
155
+
156
+ 1. Implement a helper module with an in-memory `Map`:
157
+ - `waitForCommand(id, timeoutMs)` – returns a Promise,
158
+ - `resolveCommand(id, result)` – resolves and clears timeout.
159
+ 2. In the main action:
160
+ - create a record with `status: 'pending'`,
161
+ - call `waitForCommand(id, timeoutMs)` and `return` the result.
162
+ 3. In the reporting action:
163
+ - update the record (`status: 'completed'`, `result`),
164
+ - call `resolveCommand(id, result)`.
165
+
166
+ Helper sketch:
167
+
168
+ ```js
169
+ const pendingCommands = new Map()
170
+
171
+ export function waitForCommand(commandId, timeoutMs = 115000) {
172
+ return new Promise((resolve, reject) => {
173
+ const timer = setTimeout(() => {
174
+ pendingCommands.delete(commandId)
175
+ reject(new Error('timeout'))
176
+ }, timeoutMs)
177
+ pendingCommands.set(commandId, { resolve, reject, timer })
178
+ })
179
+ }
180
+
181
+ export function resolveCommand(commandId, result) {
182
+ const pending = pendingCommands.get(commandId)
183
+ if(pending) {
184
+ clearTimeout(pending.timer)
185
+ pendingCommands.delete(commandId)
186
+ pending.resolve(result)
187
+ }
188
+ }
189
+ ```
190
+
@@ -0,0 +1,173 @@
1
+ ---
2
+ description: Design models with userItem, itemOf, propertyOf relations and access control
3
+ ---
4
+
5
+ # Skill: live-change-design-models-relations (Claude Code)
6
+
7
+ Use this skill when you design or refactor **models and relations** in a LiveChange service.
8
+
9
+ ## When to use
10
+
11
+ - You are adding a new model to a service.
12
+ - You want to switch from manual CRUD/views to proper relations.
13
+ - You need consistent access control and index usage.
14
+
15
+ ## Step 1 – Decide the relation type
16
+
17
+ For each new model, decide how it relates to the rest of the domain:
18
+
19
+ - **`userItem`** – the object belongs to the signed-in user (e.g. user’s device).
20
+ - **`itemOf`** – a list of children belonging to a parent model (e.g. device connections).
21
+ - **`propertyOf`** – a single state object with the same id as the parent (e.g. cursor state).
22
+ - **no relation** – for global data or other special cases.
23
+
24
+ Choose one main relation; other associations can be plain fields + indexes.
25
+
26
+ ## Step 2 – Define `properties` clearly
27
+
28
+ 1. Use a **multi-line** style for properties, with clear `type`, `default`, `validation`, etc.
29
+ 2. Avoid unreadable one-liners combining everything.
30
+
31
+ Example:
32
+
33
+ ```js
34
+ properties: {
35
+ name: {
36
+ type: String,
37
+ validation: ['nonEmpty']
38
+ },
39
+ status: {
40
+ type: String,
41
+ default: 'offline'
42
+ },
43
+ capabilities: {
44
+ type: Array,
45
+ of: {
46
+ type: String
47
+ }
48
+ }
49
+ }
50
+ ```
51
+
52
+ ## Step 3 – Configure the relation
53
+
54
+ ### `userItem`
55
+
56
+ 1. Add a `userItem` block inside the model definition.
57
+ 2. Set roles for read/write and list which fields can be written.
58
+
59
+ ```js
60
+ userItem: {
61
+ readAccessControl: { roles: ['owner', 'admin'] },
62
+ writeAccessControl: { roles: ['owner', 'admin'] },
63
+ writeableProperties: ['name']
64
+ }
65
+ ```
66
+
67
+ ### `itemOf`
68
+
69
+ 1. Decide the parent model.
70
+ 2. If the parent is in another service, declare it via `foreignModel` (see next step).
71
+
72
+ ```js
73
+ itemOf: {
74
+ what: Device,
75
+ readAccessControl: { roles: ['owner', 'admin'] },
76
+ writeAccessControl: { roles: ['owner', 'admin'] }
77
+ }
78
+ ```
79
+
80
+ ### `propertyOf`
81
+
82
+ 1. Use when the child should share the same id as the parent.
83
+ 2. This simplifies lookups and avoids extra indexes.
84
+
85
+ ```js
86
+ propertyOf: {
87
+ what: Device,
88
+ readAccessControl: { roles: ['owner', 'admin'] },
89
+ writeAccessControl: { roles: ['owner', 'admin'] }
90
+ }
91
+ ```
92
+
93
+ ### `propertyOf` with multiple parents (1:1 link to each)
94
+
95
+ Use this when a model should act as a dedicated 1:1 link between multiple entities (e.g. invoice ↔ contractor role links),
96
+ so the relations/CRUD generator can treat it as a relation rather than a plain `someId` property.
97
+
98
+ Notes:
99
+
100
+ - Usually you’ll have 1–2 parents, but the `propertyOf` list may contain **any number** of parent models (including 3+).
101
+ - 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.
102
+
103
+ Example:
104
+
105
+ ```js
106
+ const CostInvoice = definition.foreignModel('invoice', 'CostInvoice')
107
+ const Contractor = definition.foreignModel('company', 'Contractor')
108
+
109
+ definition.model({
110
+ name: 'Supplier',
111
+ properties: {
112
+ // optional extra fields
113
+ },
114
+ propertyOf: [
115
+ { what: CostInvoice },
116
+ { what: Contractor }
117
+ ]
118
+ })
119
+ ```
120
+
121
+ ## Step 4 – Use `foreignModel` for cross-service relations
122
+
123
+ 1. At the top of the domain file, declare:
124
+
125
+ ```js
126
+ const Device = definition.foreignModel('deviceManager', 'Device')
127
+ ```
128
+
129
+ 2. Then use `Device` in `itemOf` or `propertyOf`:
130
+
131
+ ```js
132
+ itemOf: {
133
+ what: Device,
134
+ readAccessControl: { roles: ['owner', 'admin'] }
135
+ }
136
+ ```
137
+
138
+ ## Step 5 – Add indexes
139
+
140
+ 1. Identify frequent queries:
141
+ - by a single field (e.g. `sessionKey`),
142
+ - by combinations (e.g. `(device, status)`).
143
+ 2. Declare indexes in the model:
144
+
145
+ ```js
146
+ indexes: {
147
+ bySessionKey: {
148
+ property: ['sessionKey']
149
+ },
150
+ byDeviceAndStatus: {
151
+ property: ['device', 'status']
152
+ }
153
+ }
154
+ ```
155
+
156
+ 3. Use these indexes in views/actions, via `indexObjectGet` / `indexRangeGet`.
157
+
158
+ ## Step 6 – Set access control on relations
159
+
160
+ 1. For `userItem`, `itemOf`, and `propertyOf`, always define:
161
+ - `readAccessControl`,
162
+ - `writeAccessControl`.
163
+ 2. Don’t rely on unspecified defaults; access rules should be explicit in the model.
164
+
165
+ ## Step 7 – Check auto-generated views/actions
166
+
167
+ 1. After adding relations, review the auto-generated views/actions:
168
+ - “my X” views and CRUD for `userItem`,
169
+ - parent-scoped lists for `itemOf`/`propertyOf`.
170
+ 2. Only add custom views/actions when:
171
+ - you need special filters,
172
+ - or custom logic not covered by the generated ones.
173
+
@@ -0,0 +1,132 @@
1
+ ---
2
+ description: Create or restructure a LiveChange backend service with proper directory layout
3
+ ---
4
+
5
+ # Skill: live-change-design-service (Claude Code)
6
+
7
+ Use this skill when you need to **create or restructure a LiveChange service** in this project or any other live-change-stack project.
8
+
9
+ ## When to use
10
+
11
+ - You are adding a **new domain service** (payments, devices, notifications, etc.).
12
+ - You are splitting logic out of an existing service.
13
+ - You want to make sure the service structure follows the project conventions.
14
+
15
+ ## Step 1 – Choose the service name
16
+
17
+ 1. Pick a short, domain-oriented name, e.g. `deviceManager`, `payments`, `notifications`.
18
+ 2. This name will be used:
19
+ - as `name` in `app.createServiceDefinition({ name })`,
20
+ - as the `name` entry in `app.config.js` (`services: [{ name: '...' }]`),
21
+ - in `services.list.js` as the property key.
22
+
23
+ ## Step 2 – Create the service directory
24
+
25
+ 1. Create `server/services/<serviceName>/`.
26
+ 2. Inside, create at least:
27
+ - `definition.js`
28
+ - `index.js`
29
+ 3. Optionally also:
30
+ - `config.js` – for resolving `definition.config`,
31
+ - domain files like `models.js`, `authenticator.js`, `actions.js`, or more fine-grained files.
32
+
33
+ ## Step 3 – Implement `definition.js`
34
+
35
+ 1. Import `app` from `@live-change/framework`.
36
+ 2. If the service uses relations or access control:
37
+ - import `relationsPlugin` from `@live-change/relations-plugin`,
38
+ - import `accessControlService` from `@live-change/access-control-service`.
39
+ 3. Call `app.createServiceDefinition({ name: '...', use: [...] })`.
40
+ 4. **Do not register models, actions or views** in this file – only the definition.
41
+
42
+ Example:
43
+
44
+ ```js
45
+ import { app } from '@live-change/framework'
46
+ import relationsPlugin from '@live-change/relations-plugin'
47
+ import accessControlService from '@live-change/access-control-service'
48
+
49
+ const definition = app.createServiceDefinition({
50
+ name: 'myService',
51
+ use: [relationsPlugin, accessControlService]
52
+ })
53
+
54
+ export default definition
55
+ ```
56
+
57
+ ## Step 4 – Implement `index.js`
58
+
59
+ 1. Import `definition` from `./definition.js`.
60
+ 2. Import all domain files (models, views, actions, triggers, authenticators) as side-effect imports.
61
+ 3. Export `definition` as the default export.
62
+ 4. Do **not** put heavy logic into `index.js`.
63
+
64
+ Example:
65
+
66
+ ```js
67
+ import definition from './definition.js'
68
+
69
+ import './models.js'
70
+ import './authenticator.js'
71
+ import './actions.js'
72
+
73
+ export default definition
74
+ ```
75
+
76
+ ## Step 5 – (Optional) Implement `config.js`
77
+
78
+ 1. Import `definition`.
79
+ 2. Read `definition.config` and apply default values.
80
+ 3. Export a plain object.
81
+
82
+ Example:
83
+
84
+ ```js
85
+ import definition from './definition.js'
86
+
87
+ const {
88
+ someOption = 'default'
89
+ } = definition.config
90
+
91
+ export default { someOption }
92
+ ```
93
+
94
+ ## Step 6 – Register the service in `services.list.js`
95
+
96
+ 1. Import from the service directory `index.js`:
97
+
98
+ ```js
99
+ import myService from './services/myService/index.js'
100
+ ```
101
+
102
+ 2. Add the service to the exported object:
103
+
104
+ ```js
105
+ export default {
106
+ // ...
107
+ myService
108
+ }
109
+ ```
110
+
111
+ ## Step 7 – Register the service in `app.config.js`
112
+
113
+ 1. Ensure there is an entry in `services`:
114
+
115
+ ```js
116
+ services: [
117
+ // ...
118
+ { name: 'myService' }
119
+ ]
120
+ ```
121
+
122
+ 2. Keep a sensible order:
123
+ - core/common services and plugins (user, session, accessControl, etc.) first,
124
+ - domain-specific application services later.
125
+
126
+ ## Step 8 – Handle dependencies on other services
127
+
128
+ 1. If the service needs to reference models from other services:
129
+ - use `definition.foreignModel('otherService', 'ModelName')` in domain files,
130
+ - do **not** import their model files directly.
131
+ 2. Make sure the other services are listed **before** this one in `app.config.js`.
132
+
@@ -0,0 +1,128 @@
1
+ ---
2
+ description: Build async action buttons with workingZone, toast and confirm dialogs
3
+ ---
4
+
5
+ # Skill: live-change-frontend-action-buttons (Claude Code)
6
+
7
+ Use this skill when you build **buttons that trigger async actions** outside of forms, using `workingZone`, `toast`, and optionally `confirm`.
8
+
9
+ ## When to use
10
+
11
+ - A button triggers a backend action (delete, approve, toggle, etc.) — **no form fields**.
12
+ - You want the global loading spinner to appear while the action runs.
13
+ - Destructive actions need a confirmation dialog before executing.
14
+
15
+ **Need a form with fields?** Use `editorData` (model editing) or `actionData` (one-shot actions) instead.
16
+
17
+ ## Step 1 – Inject workingZone, set up toast/confirm
18
+
19
+ ```javascript
20
+ import { inject } from 'vue'
21
+ import { useToast } from 'primevue/usetoast'
22
+ import { useConfirm } from 'primevue/useconfirm'
23
+ import { useApi, useActions } from '@live-change/vue3-ssr'
24
+
25
+ const workingZone = inject('workingZone')
26
+ const toast = useToast()
27
+ const confirm = useConfirm()
28
+ const api = useApi()
29
+ const actions = useActions()
30
+ ```
31
+
32
+ `workingZone` is provided by `ViewRoot` (which wraps every page in `<WorkingZone>`). When you call `workingZone.addPromise(name, promise)`, the global spinner/blur activates until the promise resolves.
33
+
34
+ ## Step 2 – Simple action button
35
+
36
+ Wrap the async operation in `workingZone.addPromise()`:
37
+
38
+ ```javascript
39
+ function createItem() {
40
+ workingZone.addPromise('createItem', (async () => {
41
+ const result = await actions.blog.createArticle({})
42
+ toast.add({ severity: 'success', summary: 'Article created', life: 2000 })
43
+ router.push({ name: 'article:edit', params: { article: result } })
44
+ })())
45
+ }
46
+ ```
47
+
48
+ **Important:** Note the `(async () => { ... })()` pattern – you must invoke the async IIFE immediately so `addPromise` receives a Promise, not a function.
49
+
50
+ Template:
51
+
52
+ ```vue
53
+ <Button label="Create article" icon="pi pi-plus" @click="createItem" />
54
+ ```
55
+
56
+ ## Step 3 – Destructive action with confirm
57
+
58
+ Use `confirm.require()` before the action:
59
+
60
+ ```javascript
61
+ function deleteItem(item) {
62
+ confirm.require({
63
+ message: 'Are you sure you want to delete this article?',
64
+ header: 'Confirmation',
65
+ icon: 'pi pi-trash',
66
+ acceptClass: 'p-button-danger',
67
+ accept: async () => {
68
+ workingZone.addPromise('deleteArticle', (async () => {
69
+ await actions.blog.deleteArticle({ article: item.id })
70
+ toast.add({ severity: 'success', summary: 'Deleted', life: 2000 })
71
+ })())
72
+ },
73
+ reject: () => {
74
+ toast.add({ severity: 'info', summary: 'Cancelled', life: 1500 })
75
+ }
76
+ })
77
+ }
78
+ ```
79
+
80
+ Template:
81
+
82
+ ```vue
83
+ <Button label="Delete" icon="pi pi-trash" severity="danger" @click="deleteItem(article)" />
84
+ ```
85
+
86
+ ## Step 4 – Error handling
87
+
88
+ Add try/catch inside the async IIFE:
89
+
90
+ ```javascript
91
+ function toggleStatus(item) {
92
+ workingZone.addPromise('toggleStatus', (async () => {
93
+ try {
94
+ await actions.blog.toggleArticleStatus({ article: item.id })
95
+ toast.add({ severity: 'success', summary: 'Status updated', life: 2000 })
96
+ } catch(e) {
97
+ toast.add({ severity: 'error', summary: 'Error', detail: e?.message ?? e, life: 5000 })
98
+ }
99
+ })())
100
+ }
101
+ ```
102
+
103
+ ## Step 5 – Using api.command instead of actions
104
+
105
+ Both work. `actions` is shorthand for typed service actions:
106
+
107
+ ```javascript
108
+ // Using actions (preferred when available):
109
+ await actions.blog.deleteArticle({ article: id })
110
+
111
+ // Using api.command (always works):
112
+ await api.command(['blog', 'deleteArticle'], { article: id })
113
+ ```
114
+
115
+ ## Pattern summary
116
+
117
+ ```
118
+ Button click
119
+ → confirm.require() (if destructive)
120
+ → workingZone.addPromise('name', (async () => {
121
+ try {
122
+ await actions.service.action({ ... })
123
+ toast.add({ severity: 'success', ... })
124
+ } catch(e) {
125
+ toast.add({ severity: 'error', ... })
126
+ }
127
+ })())
128
+ ```