@stacksjs/defaults 0.75.422 → 0.75.431

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 (37) hide show
  1. package/ai/skills/stacks-alias/SKILL.md +8 -0
  2. package/ai/skills/stacks-arrays/SKILL.md +3 -1
  3. package/ai/skills/stacks-build/SKILL.md +3 -0
  4. package/ai/skills/stacks-cache/SKILL.md +13 -9
  5. package/ai/skills/stacks-calendar/SKILL.md +20 -12
  6. package/ai/skills/stacks-chat/SKILL.md +22 -9
  7. package/ai/skills/stacks-database/SKILL.md +10 -0
  8. package/ai/skills/stacks-desktop/SKILL.md +7 -0
  9. package/ai/skills/stacks-events/SKILL.md +6 -2
  10. package/ai/skills/stacks-i18n/SKILL.md +9 -10
  11. package/ai/skills/stacks-native/CATALOG.md +70 -62
  12. package/ai/skills/stacks-notifications/SKILL.md +11 -2
  13. package/ai/skills/stacks-objects/SKILL.md +6 -1
  14. package/ai/skills/stacks-payments/SKILL.md +5 -3
  15. package/ai/skills/stacks-push/SKILL.md +14 -9
  16. package/ai/skills/stacks-queue/SKILL.md +7 -0
  17. package/ai/skills/stacks-slug/SKILL.md +7 -1
  18. package/ai/skills/stacks-strings/SKILL.md +19 -10
  19. package/ai/skills/stacks-testing/SKILL.md +5 -1
  20. package/ai/skills/stacks-utils/SKILL.md +1 -1
  21. package/ai/skills/stacks-validation/SKILL.md +20 -4
  22. package/ai/skills/stacks-whois/SKILL.md +5 -4
  23. package/app/Actions/Dashboard/Content/post-input.ts +1 -1
  24. package/app/Actions/InstallAction.ts +3 -3
  25. package/app/Models/Content/Post.ts +1 -1
  26. package/app/Models/Error.ts +1 -1
  27. package/app/Models/Log.ts +1 -1
  28. package/app/Models/PaymentMethod.ts +3 -3
  29. package/app/Models/PaymentProduct.ts +1 -1
  30. package/app/Models/Subscription.ts +1 -1
  31. package/app/Models/commerce/PrintDevice.ts +1 -1
  32. package/app/Models/commerce/Receipt.ts +1 -1
  33. package/app/Models/realtime/Websocket.ts +2 -2
  34. package/ide/vscode/package.json +1 -1
  35. package/package.json +2 -2
  36. package/resources/views/cms/blocks/form.stx +31 -36
  37. package/routes/forms.ts +14 -5
@@ -210,10 +210,15 @@ Retained evidence: `core/objects/tests/transform.test.ts`.
210
210
  - `objectPick` creates a NEW object -- original is not affected.
211
211
  - `objectMap` creates a NEW object via `Object.fromEntries`.
212
212
  - `deepMerge` does NOT deeply merge arrays -- arrays from source replace arrays in target entirely.
213
+ - `deepMerge` merges only plain objects key by key: a Date, Map, Set or class instance from a source is assigned as
214
+ is, and a string, array or number in the target is replaced by a source's object.
213
215
  - `deepMerge` uses `isObject()` from `@stacksjs/validation` combined with `!Array.isArray()` to determine mergeability.
216
+ - A `"__proto__"` key is data, never a prototype. `JSON.parse` and the router's request input keep one as an own key:
217
+ `deepMerge` skips it and merges only into the target's own properties, and the transform helpers keep it as an own
218
+ key (`Object.fromEntries`), so input that carries one cannot set a prototype.
214
219
  - `objectMap` can filter entries by returning `undefined` from the callback -- filtered via `notNullish` from `@stacksjs/utils`.
215
220
  - `objectKeys` return type uses a template literal type to handle non-string keys.
216
221
  - `clearUndefined` only removes `undefined` -- `null` values are preserved.
217
222
  - `hasOwnProperty` accepts `null`/`undefined` objects without throwing.
218
223
  - `objectPick` with `omitUndefined=false` (default) includes keys even if their values are `undefined`, as long as the key exists in the object.
219
- - These utilities are also re-exported through `@stacksjs/utils`.
224
+ - `@stacksjs/utils` does not re-export these. Its `merge` / `defu` is a separate merge that builds a new object.
@@ -717,8 +717,10 @@ when the app declares no portal.
717
717
 
718
718
  ```typescript
719
719
  Payment.formatAmount(2999, 'usd') // '$29.99' (uses Intl.NumberFormat)
720
- Payment.toCents(29.99) // 2999
721
- Payment.toDollars(2999) // 29.99
720
+ Payment.formatAmount(1000, 'jpy') // '¥1,000': JPY has no minor unit
721
+ Payment.minorUnitExponent('jpy') // 0 (Stripe's table on the Stripe driver; ISO 4217 otherwise)
722
+ Payment.toCents(29.99) // 2999, two-decimal currencies only
723
+ Payment.toDollars(2999) // 29.99, two-decimal currencies only
722
724
  ```
723
725
 
724
726
  ### Client-Side Billing (useBillable)
@@ -918,7 +920,7 @@ The instance methods are exactly the keys of `createBillableMethods` in
918
920
  its original raw payload before applying domain events.
919
921
  - Stripe API keys MUST be in `.env` as `STRIPE_SECRET_KEY` and `STRIPE_PUBLISHABLE_KEY` -- never hardcode them in config files
920
922
  - The Stripe client is created lazily, on first use: importing `@stacksjs/payments` without `STRIPE_SECRET_KEY` (or without the opt-in `stripe` package) is fine, and the first Stripe call throws naming what is missing
921
- - All amounts are in cents -- use `toCents()` and `toDollars()` for conversion
923
+ - All amounts are in the currency's minor unit, and which minor unit depends on the API. A provider-neutral `Money` (the driver, the billable trait's `createPayment()`/`charge()`/`refund()`, webhook `PaymentEvent.amount`) is in ISO 4217 minor units, as commerce stores them: whole krónur for ISK, hundredths for MGA. The Stripe driver converts to Stripe's units, which differ for ISK, UGX (two decimals) and MGA (none), with `toStripeAmount()` and `fromStripeAmount()`, and refuses a fraction of an ariary. The Stripe-shaped APIs (`manageCharge`, Connect, direct debit, product setup) and `formatAmount()` take Stripe's units: cents for USD, whole yen for JPY, hundredths for ISK and UGX. `toCents()` and `toDollars()` are for two-decimal currencies; `minorUnitExponent(currency)` says how many decimals a Stripe amount has
922
924
  - `Payment.charge()` (Stripe driver) creates AND confirms the PaymentIntent in one step
923
925
  - `subscribe()` resolves the price via `lookup_key`, not a direct Stripe price ID
924
926
  - `Payment.removePaymentMethod()` and the billable instance method `removePaymentMethod()` both take the provider's id (string); `managePaymentMethod.deletePaymentMethod()` takes the local row id (number)
@@ -42,8 +42,9 @@ Routes to the appropriate driver based on `options.driver` (default: `'expo'`).
42
42
 
43
43
  - For **Expo**: Passes the notification directly to `expo.send()` with all fields mapped.
44
44
  - For **FCM** with single token: Calls `fcm.send()` with notification mapped to `{ to, notification: { title, body }, data, priority }`.
45
- - For **FCM** with multiple tokens: Calls `fcm.sendMulticast()` and returns an aggregated result with success count.
46
- - FCM priority mapping: `'default'` is converted to `'normal'`; `'high'` and `'normal'` pass through.
45
+ - For **FCM** with multiple tokens: Calls `fcm.sendMulticast()` and returns an aggregated result with success count, and `data: { results }`, one per token in order, so a dead token (404 `UNREGISTERED`) can be told from a transient failure.
46
+ - FCM priority mapping: `'default'` is converted to `'normal'`; `'high'` and `'normal'` pass through. Unset means `high`, on Android and iOS alike.
47
+ - `data` is the same for every driver. FCM takes only strings, so a non-string value is sent as its JSON text (`{ orderId: 42 }` arrives as `"42"`).
47
48
 
48
49
  ## PushNotification Interface (index.ts)
49
50
 
@@ -128,12 +129,16 @@ async function send(message: ExpoPushMessage): Promise<PushResult>
128
129
  ```
129
130
 
130
131
  - Endpoint: `https://exp.host/--/api/v2/push/send`
131
- - Validates tokens with `isExpoPushToken()` before sending
132
- - Filters out invalid tokens and warns via `log.warn()`
132
+ - Validates tokens with `isExpoPushToken()` before sending; an invalid token
133
+ is warned about via `log.warn()`, not sent, and counted as a failure
133
134
  - Returns early with failure if no valid tokens remain
134
135
  - Creates one payload entry per token (fans out `to` array into individual messages)
136
+ - Sends them in requests of at most 100, one after another: Expo refuses a
137
+ larger request outright, so a send to 101 devices used to reach none. A
138
+ refused request fails its own messages and the rest still go
135
139
  - Default `sound: 'default'`, default `priority: 'high'`
136
- - Returns `PushResult` with `data: { tickets }` containing the full ticket array
140
+ - Returns `PushResult` with `data: { tickets }`: one ticket per token in `to`,
141
+ in the same order, so each ticket can be matched to its token
137
142
  - `success` is `true` only if zero tickets have `status: 'error'`
138
143
  - `messageId` is the first ticket's `id` (if any)
139
144
 
@@ -147,13 +152,13 @@ Validates token format: matches `Expo(nent)?PushToken[...]` pattern OR alphanume
147
152
  ```typescript
148
153
  async function getReceipts(ticketIds: string[]): Promise<Record<string, ExpoPushReceipt>>
149
154
  ```
150
- Endpoint: `https://exp.host/--/api/v2/push/getReceipts`. Checks delivery status for previously sent notifications.
155
+ Endpoint: `https://exp.host/--/api/v2/push/getReceipts`. Checks delivery status for previously sent notifications, asking 1000 IDs per request (Expo's limit) and merging the answers.
151
156
 
152
157
  #### sendBatch()
153
158
  ```typescript
154
159
  async function sendBatch(messages: ExpoPushMessage[], chunkSize?: number): Promise<PushResult[]>
155
160
  ```
156
- Sends multiple messages in chunks (default chunk size: 100, per Expo's recommendation). Processes chunks sequentially, messages within each chunk in parallel via `Promise.all`.
161
+ Sends several messages, `chunkSize` (default 100) at a time: chunks sequentially, messages within a chunk in parallel via `Promise.all`. Each message's own tokens are split into requests of 100 by `send()`.
157
162
 
158
163
  ### Exports
159
164
  - `send`, `sendBatch`, `getReceipts`, `isExpoPushToken` functions
@@ -192,7 +197,7 @@ interface FCMMessage {
192
197
  clickAction?: string
193
198
  tag?: string
194
199
  }
195
- data?: Record<string, string> // NOTE: values must be strings for FCM
200
+ data?: Record<string, unknown> // sent as strings: a non-string value as its JSON text
196
201
  priority?: 'high' | 'normal'
197
202
  ttl?: number
198
203
  collapseKey?: string
@@ -221,7 +226,7 @@ endpoint it used to fall back to is gone.
221
226
  1. Generates JWT with RS256 signing using the service account private key
222
227
  2. Exchanges JWT for OAuth2 access token at `https://oauth2.googleapis.com/token`
223
228
  3. Sends to `https://fcm.googleapis.com/v1/projects/{projectId}/messages:send`
224
- 4. Includes Android priority settings and APNS headers (`apns-priority: 10` for high, `5` for normal)
229
+ 4. Includes Android priority settings and APNS headers (`apns-priority: 10` for high, `5` for normal); an unset priority is high on both
225
230
  5. Supports `to` (token), `topic`, or `condition` targeting
226
231
 
227
232
  **JWT generation details:**
@@ -59,6 +59,13 @@ export class Job {
59
59
  ```
60
60
 
61
61
  ### Dispatch Routing
62
+
63
+ Production defaults to the database driver. An explicit `sync` driver in production
64
+ throws; reserved/unknown drivers also throw. `resolveQueueDriver()` shares this
65
+ policy across both dispatch APIs and workers. For a transactional database job
66
+ intent, `storeJob` from `@stacksjs/queue/utils` inserts immediately on the active
67
+ transaction; its consumer is the normal database queue worker, including retries.
68
+
62
69
  1. Checks `isFaked()` -- if testing, dispatches to FakeQueue
63
70
  2. `sync` driver: calls `dispatchNow()` (immediate execution)
64
71
  3. `redis` driver: creates `RedisQueue`, calls `queue.add()` with bun-queue options
@@ -76,6 +76,8 @@ interface SlugifyOptions {
76
76
 
77
77
  ## Gotchas
78
78
  - Omitting either table or column returns the base slug with no query.
79
+ - A title with no letters or digits at all (`!!!`, an emoji) gets a random
80
+ eight-character slug from `uniqueSlug()`, never `""`; `slugify()` still returns `""`.
79
81
  - The check and insert are separate: keep a UNIQUE index and handle/retry an
80
82
  insert conflict. A helper cannot reserve the returned slug against a race.
81
83
  - Collision attempts are bounded; after repeated conflicts the helper returns
@@ -84,7 +86,11 @@ interface SlugifyOptions {
84
86
  - `uniqueSlug()` is async - it queries the database
85
87
  - `slugify()` is sync - no database interaction
86
88
  - Collision detection appends `-2`, `-3`, etc. (not random suffixes)
87
- - Unicode characters are transliterated (u → u, e → e, etc.)
89
+ - Unicode characters are transliterated (u → u, e → e, etc.) where ts-slug's charmap
90
+ romanizes the script (Latin, Greek, Cyrillic, Hebrew, Arabic, Devanagari, Georgian);
91
+ any other script, such as Japanese, Chinese, Korean or Thai, keeps its letters
92
+ (`日本語のタイトル` stays as it is; percent-encoded in a URL)
93
+ - Any dash separates words: an em or en dash becomes a hyphen
88
94
  - For string-only slugification without DB, use `slug()` from `@stacksjs/strings` instead
89
95
  - Tags and Categories both use unique slugs by default
90
96
  - Slug columns should have a unique index in the database
@@ -100,9 +100,9 @@ interface TitleCaseOptions {
100
100
  locale?: string | string[]
101
101
  sentenceCase?: boolean // only capitalize first word + after terminators
102
102
  sentenceTerminators?: Set<string> // default: '.', '!', '?'
103
- titleTerminators?: Set<string> // default: '.', '!', '?', ':', '"', "'", '"'
103
+ titleTerminators?: Set<string> // default: '.', '!', '?', ':', '"', "'", and a closing curly quote (U+201D)
104
104
  smallWords?: Set<string> // words not capitalized (see SMALL_WORDS)
105
- wordSeparators?: Set<string> // default: '---', '--', '-', '---', '/'
105
+ wordSeparators?: Set<string> // default: em dash (U+2014), en dash (U+2013), '-', horizontal bar (U+2015), '/'
106
106
  }
107
107
  ```
108
108
 
@@ -196,6 +196,10 @@ Extensive built-in character map covering:
196
196
  - Vietnamese diacritics
197
197
  - Currency symbols ($, EUR, GBP, YEN, etc.)
198
198
  - Special characters (trademark, copyright, etc.)
199
+ - Any dash (hyphen, en dash, em dash, figure dash) separates words, as in ts-slug
200
+ - `slug()` keeps ASCII only, which suits identifiers such as a cloud resource name. For a
201
+ content URL use `slugify` or `uniqueSlug` from `@stacksjs/slug` (ts-slug), which keeps
202
+ the letters of a script it cannot romanize: `日本茶` stays `日本茶` there and is `""` here
199
203
 
200
204
  ### Locale Support
201
205
  Locale-specific character mappings for: `bg`, `de`, `es`, `fr`, `pt`, `uk`, `vi`, `da`, `nb`, `it`, `nl`, `sv`.
@@ -241,7 +245,7 @@ Native TypeScript implementations (no external validator dependency). All accept
241
245
  ### Core Validators
242
246
  ```typescript
243
247
  isEmail('user@example.com') // true -- regex: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
244
- isStrongPassword('Abc123!@#') // true -- min 8 chars, upper, lower, digit, symbol
248
+ isStrongPassword('Abc123!@#') // true -- min 8 chars, upper, lower, digit, symbol (any non-letter, non-number)
245
249
  isAlphanumeric('abc123') // true -- /^[a-zA-Z0-9]+$/
246
250
  isAlpha('abcdef') // true -- /^[a-zA-Z]+$/
247
251
  isNumeric('123') // true -- /^[0-9]+$/ (whole numbers only)
@@ -256,16 +260,16 @@ isBase64('SGVsbG8=') // true -- btoa(atob(x)) === x
256
260
  isBase32('MFZWI===') // true -- /^[A-Z2-7]+=*$/
257
261
  isAscii('hello') // true -- /^[\x00-\x7F]*$/
258
262
  isJSON('{"key":"value"}') // true -- JSON.parse succeeds
259
- isHSL('hsl(0, 100%, 50%)') // true
263
+ isHSL('hsl(0, 100%, 50%)') // true -- comma syntax: hsl(hue, saturation%, lightness%)
260
264
  isMimeType('text/html') // true -- /^[a-z]+\/[a-z0-9\-+.]+$/i
261
- isDataURI('data:text/plain;base64,...') // true
265
+ isDataURI('data:text/plain;base64,...') // true -- RFC 2397: the media type and ;base64 are both optional
262
266
  isJWT('eyJhbGciOiJIUzI1NiJ9.e30.c2ln') // true -- shape check only, not signature verification
263
267
  ```
264
268
 
265
269
  ### Network Validators
266
270
  ```typescript
267
271
  isIP('192.168.1.1') // true -- IPv4 and IPv6
268
- isIPRange('192.168.1.0/24') // true -- CIDR notation (IPv4: 0-32, IPv6: 0-128)
272
+ isIPRange('192.168.1.0/24') // true -- CIDR notation, a whole-number prefix (IPv4: 0-32, IPv6: 0-128)
269
273
  isMACAddress('00:11:22:33:44:55') // true -- colon or hyphen separated
270
274
  isFQDN('example.com') // true -- fully qualified domain name
271
275
  ```
@@ -282,7 +286,7 @@ isISSN('0378-5955') // true -- mod 11 check
282
286
 
283
287
  ### Geographic Validators
284
288
  ```typescript
285
- isLatLong('40.7128,-74.0060') // true -- lat: -90..90, long: -180..180
289
+ isLatLong('40.7128,-74.0060') // true -- plain decimal numbers; lat: -90..90, long: -180..180
286
290
  isLatitude('40.7128') // true
287
291
  isLongitude('-74.0060') // true
288
292
  isPostalCode('12345') // true -- US, UK, Canada, generic formats
@@ -291,8 +295,8 @@ isPostalCode('12345') // true -- US, UK, Canada, generic for
291
295
  ### Phone, Identity, Date Validators
292
296
  ```typescript
293
297
  isMobilePhone('+1234567890') // true -- international format, min 7 digits
294
- isUUID('550e8400-e29b-41d4-a716-446655440000') // true -- full value required
295
- isISO8601('2024-01-01T00:00:00Z') // true -- with Date validity check
298
+ isUUID('550e8400-e29b-41d4-a716-446655440000') // true -- versions 1-8 (RFC 9562), nil and max; full value required
299
+ isISO8601('2024-01-01T00:00:00Z') // true -- the day must exist in its month; any number of fraction digits
296
300
  isIdentityCard('AB123456') // true -- generic 5-20 alphanumeric
297
301
  isISRC('USRC17607839') // true
298
302
  isISO31661Alpha2('US') // true
@@ -404,7 +408,12 @@ interface PluralizeOptions { count?, inclusive? }
404
408
  ```
405
409
 
406
410
  ## Gotchas
407
- - Case conversion uses Unicode-aware word boundary detection (`\p{Ll}`, `\p{Lu}`) -- handles international characters
411
+ - Case conversion uses Unicode-aware word boundary detection (`\p{Ll}`, `\p{Lu}`) -- handles international characters.
412
+ Combining marks stay with their letter and digits from any script are kept, so `kebabCase('नमस्ते दुनिया')`
413
+ is `'नमस्ते-दुनिया'`
414
+ - `truncate(str, length, end)` never returns more than `length` characters, `end` included; with a `length`
415
+ shorter than `end` it returns as much of `end` as fits. It and `mask` count code points, so an emoji is one
416
+ character and is never cut in half
408
417
  - `slug()` calls `slugify()` under the hood but defaults to `{ lower: true, strict: true }`; `slugify()` defaults to `{ lower: false, strict: false }`
409
418
  - `pascalCase` and `camelCase` accept `PascalCaseOptions` with `mergeAmbiguousCharacters` to control number-prefixed word handling
410
419
  - All case functions support `prefixCharacters`/`suffixCharacters` to preserve leading/trailing non-word characters
@@ -182,7 +182,11 @@ module for the rest of that run, so a mock in one file leaks into the next. A
182
182
  framework booted against one database also stays booted. When a test needs a
183
183
  clean process, run it in a child (`src/isolated.ts`); when it needs real rows,
184
184
  give it a database nobody else uses (`src/disposable-database.ts`). Both are on
185
- the root, with no framework import.
185
+ the root, with no framework import. For direct PostgreSQL queries, use
186
+ `new SQL(database.sqlOptions!)`: Bun can let an inherited `PGDATABASE` override
187
+ a URL's database name. The helper's explicit SQL options select its owned
188
+ database. Pass `database.env` last to isolated children; it supplies matching
189
+ `DB_*`, `PG_*`, `POSTGRES_URL` and `DATABASE_URL` values.
186
190
 
187
191
  ```ts
188
192
  import { assertIsolatedOk, disposablePostgresUrl, runIsolated, test, withDisposableDatabase } from '@stacksjs/testing'
@@ -45,7 +45,7 @@ export { clamp, notNullish, retry, useOnline } from '@stacksjs/browser'
45
45
 
46
46
  ## Deep Merge (`merge.ts`)
47
47
 
48
- Replacement for `defu`. Arrays are concatenated, objects are recursively merged, special objects (Date, RegExp, Error, Promise, Map, Set, WeakMap, WeakSet, ArrayBuffer views) are NOT deeply merged.
48
+ Replacement for `defu`. Arrays are concatenated, objects are recursively merged, special objects (Date, RegExp, Error, Promise, Map, Set, WeakMap, WeakSet, ArrayBuffer views) are NOT deeply merged. A `"__proto__"` key is skipped, as defu skips it, so a parsed request body cannot set the result's prototype.
49
49
 
50
50
  ```typescript
51
51
  import { merge, mergeDefaults, createMerger, defu } from '@stacksjs/utils'
@@ -249,6 +249,17 @@ export default defineModel({
249
249
  })
250
250
  ```
251
251
 
252
+ ### Custom messages
253
+
254
+ A rule's `message` is one string for any failure of the field, or an object keyed
255
+ by rule name: `min`, `max`, `email`, `enum`, `required` and so on. `messageKeys(rule)`
256
+ lists the keys a validator takes; `timestamp()` and `date()` take `timestamp` and
257
+ `date`. A key named after the field covers failures no rule key matched, unless the
258
+ validator has a rule of that name (`email: { email: ... }` is the email rule's).
259
+ Action `validations`, `request.validate()`, `validateField()` and model writes all
260
+ apply them through `fieldErrorMessages()` from `@stacksjs/validation/messages`, after
261
+ validation; none of them sets ts-validation's process-wide message provider.
262
+
252
263
  ## Model Validation (`validator.ts`)
253
264
 
254
265
  ### `validateField(modelFile, params)`
@@ -402,6 +413,8 @@ they are. Inferred object shapes are native, not a hand-written request interfac
402
413
  schema.file().required().image().mimeTypes([...]).maxBytes(n).minBytes(n).
403
414
  extensions([...]).custom(fn) provides FileLike validation. Client MIME/extension
404
415
  checks do not prove the bytes; use stacks-storage's MIME verification where needed.
416
+ An upload is a class instance (the router's `UploadedFile`); a plain object, which
417
+ is what a JSON body gives for the same field name, is refused.
405
418
 
406
419
  registerRule/getCustomRule, unique(table,column,exceptId?), exists(table,column)
407
420
  and validateFieldAsync support application async validation. Current unique/exists
@@ -437,9 +450,11 @@ validateModelInput(MyModel, { title })
437
450
 
438
451
  Failures throw `InputValidationError` with `status: 422`. JSON must be an object;
439
452
  text never coerces objects or silently truncates; numeric input rejects booleans,
440
- non-finite values and unsafe whole numbers. Optional text/number input returns
453
+ non-finite values, unsafe whole numbers and anything but decimal notation (`0x10`
454
+ is not 16), and an identifier is decimal digits only. Optional text/number input returns
441
455
  null, and absent boolean input returns undefined. Boolean input accepts JSON
442
- booleans and the explicit form values `true`, `false`, `1`, `0` and their strings.
456
+ booleans and the explicit form values `true`, `false`, `1`, `0` and their strings,
457
+ and a checkbox's `on` and `off`.
443
458
 
444
459
  `validateModelInput` checks only supplied fields using `getDefinition()` rules.
445
460
  The action still selects writable fields, requires creation fields and validates
@@ -449,9 +464,10 @@ Root and narrow input imports share the same error constructor.
449
464
  ## Reusable boundary and stored-value rules
450
465
 
451
466
  `parseTextInput(value, field, required?, maxLength?)` rejects overlong text
452
- rather than truncating it. `parseEnumInput(value, field, choices)` requires
467
+ rather than truncating it; `maxLength` counts characters, as a database column
468
+ does, not UTF-16 units. `parseEnumInput(value, field, choices)` requires
453
469
  exact string membership; it never stringifies an object or boolean.
454
- `storedBoolean(value, fallback)` decodes SQL/form 0/1 and false/true; the
470
+ `storedBoolean(value, fallback)` decodes SQL/form 0/1, false/true and on/off; the
455
471
  caller explicitly chooses the fallback for missing or corrupt stored data.
456
472
  Partial model validation accepts attribute names and their snake-case column
457
473
  spelling, including camel-case definitions.
@@ -133,11 +133,12 @@ When `outputData` is `null`, `parseData()` uses this default template:
133
133
  }
134
134
  ```
135
135
 
136
- The parser iterates character by character through raw WHOIS text:
137
- - Keys are identified when a line matches a key in `outputData`
138
- - Values follow after `": "` separator
136
+ The parser reads raw WHOIS text a line at a time:
137
+ - A line's field is everything before its first colon; it counts when it is a key in `outputData`
138
+ - The value is everything after that colon, as written, so `2024-08-14T07:01:34Z` and `https://...` keep their colons
139
+ - A field with nothing after its colon takes the next non-empty line as its value (registries that print `Registrar:` with the name below)
139
140
  - Array-typed fields (like `Domain Status`) accumulate multiple values
140
- - Parsing stops at `"Record maintained by"` or `">>>"` markers
141
+ - Parsing stops at a line starting with `"Record maintained by"` or `">>>"`
141
142
 
142
143
  Custom parse templates: pass an object with the field names you want extracted. Use empty string `''` for single values, use `[]` for fields with multiple values.
143
144
 
@@ -59,7 +59,7 @@ function optionalId(value: unknown): number | null {
59
59
  }
60
60
 
61
61
  function booleanValue(value: unknown): boolean {
62
- return storedBoolean(value === 'on' ? true : value, false)
62
+ return storedBoolean(value, false)
63
63
  }
64
64
 
65
65
  export function invalidPostContent(payload: PostPayload): string | null {
@@ -33,7 +33,7 @@ export default new Action({
33
33
  git clone https://github.com/stacksjs/stacks.git $1
34
34
  cd $1
35
35
  # Run the pantry-install script
36
- "./storage/framework/scripts/pantry-install"
36
+ "./storage/framework/scripts/pantry-install" || exit $?
37
37
 
38
38
  echo "Project $1 has been created. Please open a new terminal, run 'bun run dev' to start the server."
39
39
 
@@ -58,10 +58,10 @@ fi
58
58
  cd $PROJECT_ROOT
59
59
  # Run the pantry-install script
60
60
  if [[ $* == *--verbose* ]]; then
61
- "$SCRIPT_PATH"
61
+ "$SCRIPT_PATH" || exit $?
62
62
  # bun ./storage/framework/core/buddy/src/cli.ts setup --verbose
63
63
  else
64
- "$SCRIPT_PATH" > /dev/null 2>&1
64
+ "$SCRIPT_PATH" > /dev/null || exit $?
65
65
  # bun ./storage/framework/core/buddy/src/cli.ts setup
66
66
  fi
67
67
 
@@ -216,7 +216,7 @@ export default defineModel({
216
216
  validation: {
217
217
  rule: schema.enum(['published', 'draft', 'archived']),
218
218
  message: {
219
- oneOf: 'Status must be either published, draft, or archived',
219
+ enum: 'Status must be either published, draft, or archived',
220
220
  },
221
221
  },
222
222
  factory: faker => faker.helpers.arrayElement(['published', 'draft', 'archived']),
@@ -71,7 +71,7 @@ export default defineModel({
71
71
  validation: {
72
72
  rule: schema.number(),
73
73
  message: {
74
- string: 'status must be a number',
74
+ number: 'status must be a number',
75
75
  },
76
76
  },
77
77
  factory: faker => faker.number.int({ min: 400, max: 500 }),
package/app/Models/Log.ts CHANGED
@@ -53,7 +53,7 @@ export default defineModel({
53
53
  // (stacksjs/stacks#2996).
54
54
  rule: schema.unix().required(),
55
55
  message: {
56
- date: 'Timestamp must be a valid date',
56
+ unix: 'Timestamp must be a valid Unix timestamp',
57
57
  },
58
58
  },
59
59
  factory: faker => faker.date.recent().getTime(),
@@ -50,7 +50,7 @@ export default defineModel({
50
50
  validation: {
51
51
  rule: schema.string().required().max(50),
52
52
  message: {
53
- number: 'brand must be a number',
53
+ string: 'brand must be a string',
54
54
  required: 'brand is required',
55
55
  },
56
56
  },
@@ -63,7 +63,7 @@ export default defineModel({
63
63
  validation: {
64
64
  rule: schema.number().required(),
65
65
  message: {
66
- string: 'exp_month must be a number',
66
+ number: 'exp_month must be a number',
67
67
  required: 'exp_month is required',
68
68
  },
69
69
  },
@@ -76,7 +76,7 @@ export default defineModel({
76
76
  validation: {
77
77
  rule: schema.number().required(),
78
78
  message: {
79
- string: 'exp_year must be a number',
79
+ number: 'exp_year must be a number',
80
80
  required: 'exp_year is required',
81
81
  },
82
82
  },
@@ -55,7 +55,7 @@ export default defineModel({
55
55
  validation: {
56
56
  rule: schema.number().required(),
57
57
  message: {
58
- string: 'expires must be a string',
58
+ number: 'unit_price must be a number',
59
59
  required: 'expires is required',
60
60
  },
61
61
  },
@@ -70,7 +70,7 @@ export default defineModel({
70
70
  validation: {
71
71
  rule: schema.number().required(),
72
72
  message: {
73
- string: 'unit_price must be a number',
73
+ number: 'unit_price must be a number',
74
74
  required: 'unit_price is required',
75
75
  },
76
76
  },
@@ -100,7 +100,7 @@ export default defineModel({
100
100
  validation: {
101
101
  rule: schema.unix(),
102
102
  message: {
103
- invalid: 'Invalid timestamp format',
103
+ unix: 'Invalid timestamp format',
104
104
  },
105
105
  },
106
106
  factory: faker => faker.date.recent().getTime(),
@@ -79,7 +79,7 @@ export default defineModel({
79
79
  validation: {
80
80
  rule: schema.timestampTz().required(),
81
81
  message: {
82
- invalid: 'Invalid date format',
82
+ timestampTz: 'Invalid date format',
83
83
  },
84
84
  },
85
85
  factory: faker => faker.date.recent().toISOString(),
@@ -33,7 +33,7 @@ export default defineModel({
33
33
  validation: {
34
34
  rule: schema.enum(['disconnection', 'error', 'success']),
35
35
  message: {
36
- in: 'Type must be one of: disconnection, error, success',
36
+ enum: 'Type must be one of: disconnection, error, success',
37
37
  },
38
38
  },
39
39
  factory: faker => faker.helpers.arrayElement(['success', 'success', 'success', 'disconnection', 'error']),
@@ -77,7 +77,7 @@ export default defineModel({
77
77
  // and MySQL (stacksjs/stacks#2996).
78
78
  rule: schema.unix(),
79
79
  message: {
80
- date: 'Time must be a valid timestamp',
80
+ unix: 'Time must be a valid timestamp',
81
81
  },
82
82
  },
83
83
  factory: faker => faker.date.recent().getTime(),
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.75.422",
5
+ "version": "0.75.431",
6
6
  "description": "Stacks, built into VS Code: stx templates, pickier linting and formatting, encrypted env files, the dev server preview and every buddy command. No other extensions required.",
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.75.422",
5
+ "version": "0.75.431",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/stacksjs/stacks.git",
@@ -45,7 +45,7 @@
45
45
  "dependencies": {
46
46
  "@iconify-json/f7": "^1.2.2",
47
47
  "@iconify-json/hugeicons": "^1.2.35",
48
- "@stacksjs/mobile": "0.75.422",
48
+ "@stacksjs/mobile": "0.75.431",
49
49
  "@stacksjs/sanitizer": "0.2.784",
50
50
  "@stacksjs/stx": "0.2.784",
51
51
  "@xterm/addon-fit": "^0.11.0",