@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.
- package/ai/skills/stacks-alias/SKILL.md +8 -0
- package/ai/skills/stacks-arrays/SKILL.md +3 -1
- package/ai/skills/stacks-build/SKILL.md +3 -0
- package/ai/skills/stacks-cache/SKILL.md +13 -9
- package/ai/skills/stacks-calendar/SKILL.md +20 -12
- package/ai/skills/stacks-chat/SKILL.md +22 -9
- package/ai/skills/stacks-database/SKILL.md +10 -0
- package/ai/skills/stacks-desktop/SKILL.md +7 -0
- package/ai/skills/stacks-events/SKILL.md +6 -2
- package/ai/skills/stacks-i18n/SKILL.md +9 -10
- package/ai/skills/stacks-native/CATALOG.md +70 -62
- package/ai/skills/stacks-notifications/SKILL.md +11 -2
- package/ai/skills/stacks-objects/SKILL.md +6 -1
- package/ai/skills/stacks-payments/SKILL.md +5 -3
- package/ai/skills/stacks-push/SKILL.md +14 -9
- package/ai/skills/stacks-queue/SKILL.md +7 -0
- package/ai/skills/stacks-slug/SKILL.md +7 -1
- package/ai/skills/stacks-strings/SKILL.md +19 -10
- package/ai/skills/stacks-testing/SKILL.md +5 -1
- package/ai/skills/stacks-utils/SKILL.md +1 -1
- package/ai/skills/stacks-validation/SKILL.md +20 -4
- package/ai/skills/stacks-whois/SKILL.md +5 -4
- package/app/Actions/Dashboard/Content/post-input.ts +1 -1
- package/app/Actions/InstallAction.ts +3 -3
- package/app/Models/Content/Post.ts +1 -1
- package/app/Models/Error.ts +1 -1
- package/app/Models/Log.ts +1 -1
- package/app/Models/PaymentMethod.ts +3 -3
- package/app/Models/PaymentProduct.ts +1 -1
- package/app/Models/Subscription.ts +1 -1
- package/app/Models/commerce/PrintDevice.ts +1 -1
- package/app/Models/commerce/Receipt.ts +1 -1
- package/app/Models/realtime/Websocket.ts +2 -2
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
- package/resources/views/cms/blocks/form.stx +31 -36
- 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
|
-
-
|
|
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.
|
|
721
|
-
Payment.
|
|
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
|
|
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
|
-
|
|
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 }
|
|
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
|
|
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,
|
|
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 --
|
|
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
|
|
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
|
|
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
|
|
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
|
|
137
|
-
-
|
|
138
|
-
-
|
|
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 `">>>"`
|
|
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
|
|
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
|
|
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
|
-
|
|
219
|
+
enum: 'Status must be either published, draft, or archived',
|
|
220
220
|
},
|
|
221
221
|
},
|
|
222
222
|
factory: faker => faker.helpers.arrayElement(['published', 'draft', 'archived']),
|
package/app/Models/Error.ts
CHANGED
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
+
number: 'exp_year must be a number',
|
|
80
80
|
required: 'exp_year is required',
|
|
81
81
|
},
|
|
82
82
|
},
|
|
@@ -33,7 +33,7 @@ export default defineModel({
|
|
|
33
33
|
validation: {
|
|
34
34
|
rule: schema.enum(['disconnection', 'error', 'success']),
|
|
35
35
|
message: {
|
|
36
|
-
|
|
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
|
-
|
|
80
|
+
unix: 'Time must be a valid timestamp',
|
|
81
81
|
},
|
|
82
82
|
},
|
|
83
83
|
factory: faker => faker.date.recent().getTime(),
|
package/ide/vscode/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"publisher": "Stacks",
|
|
3
3
|
"name": "vscode-stacks",
|
|
4
4
|
"displayName": "Stacks",
|
|
5
|
-
"version": "0.75.
|
|
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.
|
|
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.
|
|
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",
|