@stacksjs/defaults 0.72.57 → 0.72.60

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.
@@ -122,12 +122,57 @@ The `Record<EventType, unknown>` intersection allows arbitrary event names beyon
122
122
 
123
123
  ## Model Events
124
124
 
125
- Every model with `observe: true` trait (in defineModel) emits via `afterCreate`/`afterUpdate`/`afterDelete` hooks:
126
- - `'{model}:created'` -- after insert
127
- - `'{model}:updated'` -- after update
128
- - `'{model}:deleted'` -- after delete
125
+ Every model with the `observe: true` trait emits **eight** events:
129
126
 
130
- Model name is lowercased: `'user:created'`, `'post:updated'`, `'order:deleted'`
127
+ | Event | When | Payload |
128
+ |---|---|---|
129
+ | `{model}:saving` | before any write | the model object |
130
+ | `{model}:creating` / `:updating` / `:deleting` | before that write | the model object |
131
+ | `{model}:created` / `:updated` / `:deleted` | after that write | the row |
132
+ | `{model}:saved` | after insert OR update | the row |
133
+
134
+ Model name is lowercased: `'user:created'`, `'post:updated'`, `'teammember:saved'`.
135
+
136
+ A **before** listener can cancel the write by returning `false`:
137
+
138
+ ```ts
139
+ listen('user:deleting', (model) => {
140
+ if (model.attributes.email.endsWith('@example.com'))
141
+ return false // the delete does not happen
142
+ })
143
+ ```
144
+
145
+ Before-events carry the model object (`.attributes` holds the row); after-events
146
+ carry the row itself.
147
+
148
+ ### The payloads are typed, and nothing generates them
149
+
150
+ `listen('user:created', user => user.emial)` is a compile error - the payload is
151
+ the User row, with the columns your model declares.
152
+
153
+ `storage/framework/types/model-events.d.ts` derives the whole map from the models
154
+ barrel with a mapped type:
155
+
156
+ ```ts
157
+ type ModelAfterEvents = {
158
+ [K in keyof Models & string as `${Lowercase<K>}:${AfterEvent}`]: ModelRow<Models[K]>
159
+ }
160
+ ```
161
+
162
+ So a model existing IS its events existing - there is no generated list to keep in
163
+ agreement, and nothing to re-run after adding a model. (It replaced an 817-line
164
+ generated file, and before that a hand-maintained one that listed three events per
165
+ model and typed every payload `Record<string, any>`.)
166
+
167
+ Declare your own events by augmenting `AppEvents`:
168
+
169
+ ```ts
170
+ declare module '@stacksjs/events' {
171
+ interface AppEvents {
172
+ 'invoice:overdue': { id: number, daysLate: number }
173
+ }
174
+ }
175
+ ```
131
176
 
132
177
  Events are dispatched via lazy `import('@stacksjs/events').then(({ dispatch }) => dispatch(...))` to avoid circular dependencies. If the import fails (e.g., browser context), errors are silently caught.
133
178
 
@@ -136,8 +181,9 @@ The `observe` trait can be:
136
181
  - `['create', 'update']` -- emits only specified events
137
182
  - `false` / undefined -- no events
138
183
 
139
- Full model list (45+ with events defined in `storage/framework/types/events.ts`):
140
- Author, Page, Post, User, Activity, Campaign, Cart, CartItem, Category, Comment, Coupon, Customer, DeliveryRoute, DigitalDelivery, Driver, EmailList, GiftCard, LicenseKey, LoyaltyPoint, LoyaltyReward, Manufacturer, Notification, Order, OrderItem, Payment, PrintDevice, Product, ProductUnit, ProductVariant, Receipt, Review, ShippingMethod, ShippingRate, ShippingZone, SocialPost, Subscription, Tag, TaxRate, Transaction, WaitlistProduct, WaitlistRestaurant, Websocket
184
+ There is no model list to keep here. Every model in `storage/framework/auto-imports/models.ts`
185
+ has its eight events, and that barrel is generated from disk for the runtime, so the
186
+ answer to "which models emit events" is "the ones that exist".
141
187
 
142
188
  ## Event-to-Listener Mapping (app/Events.ts)
143
189
 
@@ -13,7 +13,7 @@ export default new Action({
13
13
 
14
14
  validations: {
15
15
  question: {
16
- rule: schema.string().min(3).max(255),
16
+ rule: schema.string().min(3).max(255).required(),
17
17
  message: 'The question must be between 3 and 255 characters long.',
18
18
  },
19
19
  },
@@ -13,7 +13,7 @@ export default new Action({
13
13
 
14
14
  validations: {
15
15
  text: {
16
- rule: schema.string().min(3),
16
+ rule: schema.string().min(3).required(),
17
17
  message: 'The text must be at least 3 characters long.',
18
18
  },
19
19
  },
@@ -10,7 +10,7 @@ export default new Action({
10
10
 
11
11
  validations: {
12
12
  password: {
13
- rule: schema.string().min(1),
13
+ rule: schema.string().min(1).required(),
14
14
  message: 'Password is required to disable two-factor authentication.',
15
15
  },
16
16
  },
@@ -10,7 +10,7 @@ export default new Action({
10
10
 
11
11
  validations: {
12
12
  code: {
13
- rule: schema.string().min(6).max(6),
13
+ rule: schema.string().min(6).max(6).required(),
14
14
  message: 'Code must be a 6-digit TOTP code.',
15
15
  },
16
16
  },
@@ -12,7 +12,7 @@ export default new Action({
12
12
 
13
13
  validations: {
14
14
  email: {
15
- rule: schema.string().email(),
15
+ rule: schema.string().email().required(),
16
16
  message: 'Email must be a valid email address.',
17
17
  },
18
18
  // Presence only, NOT the creation policy. Enforcing a minimum length on
@@ -21,7 +21,7 @@ export default new Action({
21
21
  // credentials are ever checked. The policy belongs on the paths that SET a
22
22
  // password (#2226).
23
23
  password: {
24
- rule: schema.string().min(1).max(PASSWORD_MAX_LENGTH),
24
+ rule: schema.string().min(1).max(PASSWORD_MAX_LENGTH).required(),
25
25
  message: PASSWORD_PRESENCE_MESSAGE,
26
26
  },
27
27
  },
@@ -11,7 +11,7 @@ export default new Action({
11
11
 
12
12
  validations: {
13
13
  token: {
14
- rule: schema.string().min(16).max(255),
14
+ rule: schema.string().min(16).max(255).required(),
15
15
  message: 'Token is required.',
16
16
  },
17
17
  },
@@ -11,7 +11,7 @@ export default new Action({
11
11
 
12
12
  validations: {
13
13
  email: {
14
- rule: schema.string().email(),
14
+ rule: schema.string().email().required(),
15
15
  message: 'Email must be a valid email address.',
16
16
  },
17
17
  },
@@ -12,7 +12,7 @@ export default new Action({
12
12
 
13
13
  await request.validate({
14
14
  refresh_token: {
15
- rule: schema.string().min(1),
15
+ rule: schema.string().min(1).required(),
16
16
  message: {
17
17
  min: 'Refresh token is required',
18
18
  },
@@ -12,15 +12,15 @@ export default new Action({
12
12
 
13
13
  validations: {
14
14
  email: {
15
- rule: schema.string().email(),
15
+ rule: schema.string().email().required(),
16
16
  message: 'Email must be a valid email address.',
17
17
  },
18
18
  password: {
19
- rule: schema.string().min(PASSWORD_MIN_LENGTH).max(PASSWORD_MAX_LENGTH),
19
+ rule: schema.string().min(PASSWORD_MIN_LENGTH).max(PASSWORD_MAX_LENGTH).required(),
20
20
  message: PASSWORD_POLICY_MESSAGE,
21
21
  },
22
22
  name: {
23
- rule: schema.string().min(2).max(255),
23
+ rule: schema.string().min(2).max(255).required(),
24
24
  message: 'Name must be between 2 and 255 characters.',
25
25
  },
26
26
  },
@@ -10,11 +10,11 @@ export default new Action({
10
10
 
11
11
  validations: {
12
12
  challenge_token: {
13
- rule: schema.string().min(1),
13
+ rule: schema.string().min(1).required(),
14
14
  message: 'A challenge token is required.',
15
15
  },
16
16
  code: {
17
- rule: schema.string().min(6).max(6),
17
+ rule: schema.string().min(6).max(6).required(),
18
18
  message: 'Code must be a 6-digit TOTP code.',
19
19
  },
20
20
  },
@@ -10,20 +10,20 @@ export default new Action({
10
10
  async handle(request: RequestInstance) {
11
11
  await request.validate({
12
12
  name: {
13
- rule: schema.string(),
13
+ rule: schema.string().required(),
14
14
  message: {
15
15
  name: 'Name is required',
16
16
  },
17
17
  },
18
18
  description: {
19
- rule: schema.string(),
19
+ rule: schema.string().required(),
20
20
  message: {
21
21
  description: 'Description is required',
22
22
  },
23
23
  },
24
24
 
25
25
  categorizable_type: {
26
- rule: schema.string(),
26
+ rule: schema.string().required(),
27
27
  message: {
28
28
  categorizable_type: 'Categorizable type is required',
29
29
  },
@@ -8,17 +8,23 @@ export default new Action({
8
8
  description: 'Category Update ORM Action',
9
9
  method: 'PATCH',
10
10
  async handle(request: RequestInstance) {
11
+ /*
12
+ * PATCH, so these are NOT required: a partial update sends the fields it
13
+ * means to change. The messages used to say "X is required", which fires on
14
+ * a type failure and never on absence - a message describing a rule the
15
+ * block does not have.
16
+ */
11
17
  await request.validate({
12
18
  name: {
13
19
  rule: schema.string(),
14
20
  message: {
15
- name: 'Name is required',
21
+ name: 'Name must be a string.',
16
22
  },
17
23
  },
18
24
  description: {
19
25
  rule: schema.string(),
20
26
  message: {
21
- description: 'Description is required',
27
+ description: 'Description must be a string.',
22
28
  },
23
29
  },
24
30
  })
@@ -10,13 +10,13 @@ export default new Action({
10
10
  async handle(request: RequestInstance) {
11
11
  await request.validate({
12
12
  title: {
13
- rule: schema.string(),
13
+ rule: schema.string().required(),
14
14
  message: {
15
15
  title: 'Title is required',
16
16
  },
17
17
  },
18
18
  body: {
19
- rule: schema.string(),
19
+ rule: schema.string().required(),
20
20
  message: {
21
21
  body: 'Body is required',
22
22
  },
@@ -8,17 +8,23 @@ export default new Action({
8
8
  description: 'Comment Update ORM Action',
9
9
  method: 'PATCH',
10
10
  async handle(request: RequestInstance) {
11
+ /*
12
+ * PATCH, so these are NOT required: a partial update sends the fields it
13
+ * means to change. The messages used to say "X is required", which fires on
14
+ * a type failure and never on absence - a message describing a rule the
15
+ * block does not have.
16
+ */
11
17
  await request.validate({
12
18
  title: {
13
19
  rule: schema.string(),
14
20
  message: {
15
- title: 'Title is required',
21
+ title: 'Title must be a string.',
16
22
  },
17
23
  },
18
24
  body: {
19
25
  rule: schema.string(),
20
26
  message: {
21
- body: 'Body is required',
27
+ body: 'Body must be a string.',
22
28
  },
23
29
  },
24
30
  })
@@ -8,11 +8,17 @@ export default new Action({
8
8
  description: 'Updates the view count for a post',
9
9
  method: 'PATCH',
10
10
  async handle(request: RequestInstance) {
11
+ /*
12
+ * PATCH, so these are NOT required: a partial update sends the fields it
13
+ * means to change. The messages used to say "X is required", which fires on
14
+ * a type failure and never on absence - a message describing a rule the
15
+ * block does not have.
16
+ */
11
17
  await request.validate({
12
18
  views: {
13
19
  rule: schema.number(),
14
20
  message: {
15
- views: 'Views is required',
21
+ views: 'Views must be a number.',
16
22
  },
17
23
  },
18
24
  })
@@ -10,13 +10,13 @@ export default new Action({
10
10
  async handle(request: RequestInstance) {
11
11
  await request.validate({
12
12
  name: {
13
- rule: schema.string(),
13
+ rule: schema.string().required(),
14
14
  message: {
15
15
  name: 'Name is required',
16
16
  },
17
17
  },
18
18
  description: {
19
- rule: schema.string(),
19
+ rule: schema.string().required(),
20
20
  message: {
21
21
  description: 'Description is required',
22
22
  },
@@ -8,17 +8,23 @@ export default new Action({
8
8
  description: 'Tag Update ORM Action',
9
9
  method: 'PATCH',
10
10
  async handle(request: RequestInstance) {
11
+ /*
12
+ * PATCH, so these are NOT required: a partial update sends the fields it
13
+ * means to change. The messages used to say "X is required", which fires on
14
+ * a type failure and never on absence - a message describing a rule the
15
+ * block does not have.
16
+ */
11
17
  await request.validate({
12
18
  name: {
13
19
  rule: schema.string(),
14
20
  message: {
15
- name: 'Name is required',
21
+ name: 'Name must be a string.',
16
22
  },
17
23
  },
18
24
  description: {
19
25
  rule: schema.string(),
20
26
  message: {
21
- description: 'Description is required',
27
+ description: 'Description must be a string.',
22
28
  },
23
29
  },
24
30
  })
@@ -1,7 +1,7 @@
1
1
  import { Action } from '@stacksjs/actions'
2
2
  import { db } from '@stacksjs/database'
3
3
  import { modelBoolean } from './kanban-model'
4
- import { kanbanActionError, kanbanError } from './kanban-response'
4
+ import { kanbanActionError } from './kanban-response'
5
5
 
6
6
  interface BoardRow {
7
7
  id: number
@@ -1,6 +1,6 @@
1
1
  import { Action } from '@stacksjs/actions'
2
2
  import { db } from '@stacksjs/database'
3
- import { kanbanActionError, kanbanError } from './kanban-response'
3
+ import { kanbanActionError } from './kanban-response'
4
4
 
5
5
  /**
6
6
  * `GET /api/dashboard/kanban/users`.
@@ -15,7 +15,7 @@ export default new Action({
15
15
  // the request object is optional, but if it is provided, it will be used for validation
16
16
  validations: {
17
17
  message: {
18
- rule: schema.string().min(3).max(255),
18
+ rule: schema.string().min(3).max(255).required(),
19
19
  message: 'The message must be between 3 and 255 characters long.',
20
20
  },
21
21
 
@@ -25,7 +25,7 @@ export default new Action({
25
25
  // is not a function" at module evaluation. Use `schema.enum([...])`
26
26
  // — that's the working enum primitive used throughout the framework
27
27
  // defaults and in the typical project's models.
28
- rule: schema.enum(['info', 'warn', 'error']),
28
+ rule: schema.enum(['info', 'warn', 'error']).required(),
29
29
  message: 'The log level must be one of "info", "warn", or "error".',
30
30
  },
31
31
  },
@@ -1,5 +1,4 @@
1
1
  import { Action } from '@stacksjs/actions'
2
- import { Storage } from '@stacksjs/storage'
3
2
 
4
3
  interface Request {
5
4
  file: (key: string) => any
@@ -1,5 +1,4 @@
1
1
  import { Auth, authCookieName, sessionUser } from '@stacksjs/auth'
2
- import { config } from '@stacksjs/config'
3
2
  import { HttpError } from '@stacksjs/error-handling'
4
3
  import { Middleware } from '@stacksjs/router'
5
4
 
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.72.57",
5
+ "version": "0.72.60",
6
6
  "description": "A modern Stacks development environment.",
7
7
  "license": "MIT",
8
8
  "funding": "https://github.com/sponsors/chrisbbreuer",
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/defaults",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.72.57",
5
+ "version": "0.72.60",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/stacksjs/stacks.git",
@@ -51,7 +51,7 @@
51
51
  "dependencies": {
52
52
  "@iconify-json/f7": "^1.2.2",
53
53
  "@iconify-json/hugeicons": "^1.2.27",
54
- "@stacksjs/mobile": "^0.72.57",
54
+ "@stacksjs/mobile": "^0.72.60",
55
55
  "@stacksjs/sanitizer": "^0.2.113"
56
56
  },
57
57
  "scripts": {
@@ -16,7 +16,7 @@
16
16
  */
17
17
 
18
18
  import process from 'node:process'
19
- import { response, route } from '@stacksjs/router'
19
+ import { route } from '@stacksjs/router'
20
20
 
21
21
  // ============================================================================
22
22
  // Email