@stacksjs/types 0.70.44 → 0.70.53

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/src/request.ts CHANGED
@@ -2,6 +2,10 @@ import type { ModelRow } from '@stacksjs/orm'
2
2
  import { User } from '@stacksjs/orm'
3
3
  import type { UploadedFile } from '@stacksjs/storage'
4
4
  import type { AuthToken, RouteParam } from '@stacksjs/types'
5
+ // `Infer<T extends Validator<U>>` resolves to the validator's output
6
+ // type — `Infer<typeof schema.string()> → string`. Type-only import
7
+ // keeps this package free of a runtime ts-validation dependency.
8
+ import type { Infer } from '@stacksjs/ts-validation'
5
9
 
6
10
  type UserJsonResponse = ModelRow<typeof User>
7
11
 
@@ -9,10 +13,96 @@ interface RequestData {
9
13
  [key: string]: any
10
14
  }
11
15
 
16
+ /**
17
+ * Loose route-param shape kept around for back-compat with code that
18
+ * predates {@link RequestInstance}'s `TParams` generic. New code should
19
+ * rely on the path-extracted `TParams` instead — see {@link ExtractParams}.
20
+ *
21
+ * @deprecated Use {@link ExtractParams}-driven typing on the action /
22
+ * route signature instead. URL route params are always strings at
23
+ * runtime; the `string | number` here misled callers into thinking
24
+ * the framework coerced numbers automatically (it doesn't —
25
+ * `Number(request.params.id)` is the correct pattern).
26
+ */
12
27
  type RouteParams = { [key: string]: string | number } | null
13
28
 
29
+ /**
30
+ * Legacy hard-coded list of param names treated as `number` by
31
+ * {@link RequestInstance.getParam}. The name-match heuristic is
32
+ * brittle (`'judgeId'`, `'user_id'`, etc. all silently fall through
33
+ * to `string`) and will be retired in a future release.
34
+ *
35
+ * @deprecated The name-match returns `number` for these specific keys
36
+ * only — pass the value through {@link Number} or
37
+ * {@link RequestInstance.getParamAsInt} for explicit, predictable
38
+ * coercion. See stacksjs/stacks#1851 Phase 3.
39
+ */
14
40
  type NumericField = 'id' | 'age' | 'count' | 'quantity' | 'amount' | 'price' | 'total' | 'score' | 'rating' | 'duration' | 'size' | 'weight' | 'height' | 'width' | 'length' | 'distance' | 'speed' | 'temperature' | 'volume' | 'capacity' | 'density' | 'pressure' | 'force' | 'energy' | 'power' | 'frequency' | 'voltage' | 'current' | 'resistance' | 'time' | 'date' | 'year' | 'month' | 'day' | 'hour' | 'minute' | 'second' | 'millisecond' | 'microsecond' | 'nanosecond'
15
41
 
42
+ /**
43
+ * Cookie-access helper exposed via `request.cookies` on
44
+ * {@link RequestInstance}. The methods mirror bun-router's
45
+ * `CookieAccessor` — duplicated locally so this package doesn't
46
+ * have to depend on bun-router for a single type. Keep the surface
47
+ * in sync if bun-router extends its accessor.
48
+ */
49
+ export interface RequestCookies {
50
+ get: (name: string) => string | undefined
51
+ set: (name: string, value: string, options?: {
52
+ path?: string
53
+ domain?: string
54
+ secure?: boolean
55
+ httpOnly?: boolean
56
+ sameSite?: 'strict' | 'lax' | 'none'
57
+ maxAge?: number
58
+ expires?: Date
59
+ }) => void
60
+ delete: (name: string, options?: { path?: string, domain?: string }) => void
61
+ getAll: () => Record<string, string>
62
+ }
63
+
64
+ /**
65
+ * Template-literal helper that extracts named route params from a
66
+ * path string (stacksjs/stacks#1851 Phase 2a). Supports both Stacks's
67
+ * brace-style (`/users/{id}`) and Express-style (`/users/:id`) so a
68
+ * project using either gets typed `params` out of the box.
69
+ *
70
+ * @example
71
+ * ExtractParams<'/api/judges/{id}/follow'> // { id: string }
72
+ * ExtractParams<'/api/orders/:orderId/items/:itemId'> // { orderId: string, itemId: string }
73
+ * ExtractParams<'/api/health'> // Record<string, never>
74
+ */
75
+ // Step 1: pull the next param name out of either `{name}` or `:name`,
76
+ // recurse on the rest, and union the keys.
77
+ type ExtractParamKeys<S extends string> =
78
+ // brace form: `…/{name}/…`
79
+ S extends `${string}{${infer Key}}${infer Rest}`
80
+ ? Key | ExtractParamKeys<Rest>
81
+ // colon form: `…/:name/…` (colon must be at a segment boundary
82
+ // so we don't match `:` inside e.g. a port number; the `/` before
83
+ // it enforces that)
84
+ : S extends `${string}/:${infer Key}/${infer Rest}`
85
+ ? KeyHead<Key> | ExtractParamKeys<`/${Rest}`>
86
+ // tail colon-form: `…/:name` (no trailing slash)
87
+ : S extends `${string}/:${infer Key}`
88
+ ? KeyHead<Key>
89
+ : never
90
+
91
+ // `KeyHead<'name>'>` → `'name>'` because TS template literal infers
92
+ // the longest possible match. We need to handle params that are at
93
+ // the end of the path AND followed by a query string. This util
94
+ // truncates a captured key at the first non-name character.
95
+ type KeyHead<S extends string> =
96
+ S extends `${infer H}/${string}` ? H :
97
+ S extends `${infer H}?${string}` ? H :
98
+ S extends `${infer H}.${string}` ? H :
99
+ S
100
+
101
+ export type ExtractParams<S extends string> =
102
+ [ExtractParamKeys<S>] extends [never]
103
+ ? Record<string, never>
104
+ : { [K in ExtractParamKeys<S>]: string }
105
+
16
106
  export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'CONNECT' | 'TRACE'
17
107
 
18
108
  /**
@@ -103,9 +193,35 @@ export interface SafeData<T extends Record<string, any> = Record<string, any>> {
103
193
  * })
104
194
  * ```
105
195
  */
106
- export type ActionRequest<TFields extends Record<string, any> = Record<string, any>> = RequestInstance<TFields>
196
+ export type ActionRequest<
197
+ TFields extends Record<string, any> = Record<string, any>,
198
+ TParams extends Record<string, string> = Record<string, string>,
199
+ > = RequestInstance<TFields, TParams>
107
200
 
108
- export interface RequestInstance<TFields extends Record<string, any> = Record<string, any>> {
201
+ /**
202
+ * Read the body shape declared by an action's `validations:` field as
203
+ * a TypeScript object type (stacksjs/stacks#1851 Phase 2b). Threaded
204
+ * into {@link RequestInstance}'s `TFields` so `request.all()` returns
205
+ * the body shape with the field types {@link Infer}'d from each
206
+ * `schema.X()` rule.
207
+ *
208
+ * @example
209
+ * const validations = {
210
+ * email: { rule: schema.string().email() },
211
+ * password: { rule: schema.string().min(8) },
212
+ * remember: { rule: schema.boolean() },
213
+ * } as const
214
+ * type Body = InferValidations<typeof validations>
215
+ * // → { email: string, password: string, remember: boolean }
216
+ */
217
+ export type InferValidations<V extends Record<string, { rule: any }>> = {
218
+ [K in keyof V]: Infer<V[K]['rule']>
219
+ }
220
+
221
+ export interface RequestInstance<
222
+ TFields extends Record<string, any> = Record<string, any>,
223
+ TParams extends Record<string, string> = Record<string, string>,
224
+ > {
109
225
  // ==========================================================================
110
226
  // Native Request properties
111
227
  // ==========================================================================
@@ -116,10 +232,28 @@ export interface RequestInstance<TFields extends Record<string, any> = Record<st
116
232
 
117
233
  // Raw data access (always untyped — use get()/input() for typed access)
118
234
  query: RequestData
119
- params: RouteParams
235
+ /**
236
+ * Route parameters from the URL. When the action is bound to a path
237
+ * literal (via {@link ActionOptions.path} or a typed
238
+ * `route.get<TPath>(...)` overload), `TParams` narrows to the
239
+ * extracted keys so `request.params.id` is `string` with no `as any`.
240
+ *
241
+ * For un-narrowed callers (bare `RequestInstance`), this falls
242
+ * back to `Record<string, string>` — the runtime shape is always
243
+ * string-keyed even for "numeric" params, since URL segments are
244
+ * never coerced.
245
+ */
246
+ params: TParams
120
247
  jsonBody?: any
121
248
  formBody?: any
122
249
  files: Record<string, File | File[]>
250
+ /**
251
+ * Cookies parsed from the request. The accessor exposes
252
+ * `get`/`set`/`delete`/`getAll` helpers that mirror bun-router's
253
+ * cookie handling. Optional because not every request middleware
254
+ * stack runs the cookie parser.
255
+ */
256
+ cookies?: RequestCookies
123
257
 
124
258
  // ==========================================================================
125
259
  // Model-aware Input Methods
@@ -325,12 +459,39 @@ export interface RequestInstance<TFields extends Record<string, any> = Record<st
325
459
  bearerToken: () => string | null | AuthToken
326
460
 
327
461
  // ==========================================================================
328
- // Route & Param Methods — not narrowed to model fields
462
+ // Route & Param Methods
463
+ //
464
+ // `params` (the field above) is the typed primary surface — narrow
465
+ // it via the action's `path:` literal for full inference. These
466
+ // method-form helpers stay around for back-compat and for cases
467
+ // where the param key isn't statically known.
329
468
  // ==========================================================================
330
469
 
470
+ /**
471
+ * Look up a single route param, optionally with a default. Narrows
472
+ * to {@link TParams} when the key is part of the path-extracted
473
+ * keyset; falls back to `string | undefined` otherwise (covers
474
+ * dynamic key access without `as any`).
475
+ *
476
+ * @example
477
+ * const id = request.param('id') // typed string (from path)
478
+ * const note = request.param('note', '') // string with default
479
+ */
480
+ param: <K extends keyof TParams | string, D = string>(
481
+ key: K,
482
+ defaultValue?: D,
483
+ ) => K extends keyof TParams ? TParams[K] : (string | D)
484
+ /**
485
+ * @deprecated Use {@link param} for typed param lookups, or
486
+ * `Number(request.params.id)` for explicit numeric coercion.
487
+ * The name-match heuristic returning `number` for {@link NumericField}
488
+ * keys (`id`, `count`, `amount`, …) is brittle: it silently falls
489
+ * through to `string` for `judgeId`, `user_id`, etc.
490
+ * See stacksjs/stacks#1851 Phase 3.
491
+ */
331
492
  getParam: <K extends string>(key: K) => K extends NumericField ? number : string
332
493
  route: (key: string) => number | string | null
333
- getParams: () => RouteParams
494
+ getParams: () => TParams
334
495
  getParamAsInt: (key: string) => number | null
335
496
 
336
497
  // ==========================================================================
@@ -40,7 +40,7 @@ export interface SearchEngineOptions {
40
40
  * @default string 'meilisearch'
41
41
  * @see https://stacksjs.com/docs/search-engine
42
42
  */
43
- driver: 'meilisearch' | 'algolia' | 'opensearch'
43
+ driver: 'meilisearch' | 'algolia' | 'opensearch' | 'typesense'
44
44
 
45
45
  opensearch?: {
46
46
  host: string
@@ -63,6 +63,13 @@ export interface SearchEngineOptions {
63
63
  searchOnlyApiKey?: string
64
64
  }
65
65
 
66
+ typesense?: {
67
+ host?: string
68
+ port?: number
69
+ protocol?: string
70
+ apiKey?: string
71
+ }
72
+
66
73
  filters?: {
67
74
  [key: string]: string
68
75
  }
@@ -204,6 +211,30 @@ export interface SearchOptions {
204
211
  sortable: string[]
205
212
  filterable: string[]
206
213
  options?: SearchEngineOptions
214
+ /**
215
+ * Cross-table denormalisation for searchable fields that live on a
216
+ * related model (stacksjs/stacks#1918). Maps an indexed-document
217
+ * field name to a dot-path resolved against the model instance's
218
+ * `_relations`. Without this, `toSearchableObject` only reads from
219
+ * `_attributes` and silently emits `undefined` for any field that
220
+ * exists on a `belongsTo` / `hasOne` / `hasMany` relation.
221
+ *
222
+ * Example: a `Judge` belongsTo a `CourtHouse`. To make the court
223
+ * house's `name` searchable on the judge index:
224
+ *
225
+ * useSearch: {
226
+ * searchable: ['name', 'court_name'],
227
+ * displayable: ['id', 'name', 'court_name'],
228
+ * denormalize: { court_name: 'court_house.name' },
229
+ * }
230
+ *
231
+ * The caller is responsible for eager-loading the named relations
232
+ * (e.g. via `Judge.query().with('court_house').get()`) — the live
233
+ * observer hook and the CLI bulk-index path do this automatically
234
+ * for every distinct head segment in the `denormalize` map.
235
+ * `toSearchableObject` stays synchronous; no per-row database lookup.
236
+ */
237
+ denormalize?: Record<string, string>
207
238
  }
208
239
 
209
240
  export type {
package/src/services.ts CHANGED
@@ -25,6 +25,24 @@ export interface ServicesOptions {
25
25
  scopes?: string[]
26
26
  }
27
27
 
28
+ /**
29
+ * Sign in with Apple. Apple has no static client secret — the driver
30
+ * signs a short-lived ES256 JWT from teamId + keyId + privateKey
31
+ * (the .p8 file's contents) instead.
32
+ */
33
+ apple?: {
34
+ /** The Services ID identifier, e.g. `org.example.web` */
35
+ clientId: string
36
+ /** Apple Developer Team ID (10 chars) */
37
+ teamId: string
38
+ /** Key ID of the "Sign in with Apple" key */
39
+ keyId: string
40
+ /** Contents of the downloaded .p8 private key */
41
+ privateKey: string
42
+ redirectUrl: string
43
+ scopes?: string[]
44
+ }
45
+
28
46
  facebook?: {
29
47
  clientId: string
30
48
  clientSecret: string
@@ -168,6 +186,14 @@ export interface ServicesOptions {
168
186
  stripe?: {
169
187
  secretKey?: string
170
188
  publicKey?: string
189
+ /**
190
+ * Signing secret for verifying inbound Stripe webhook requests
191
+ * (`whsec_...`, from the Stripe dashboard's webhook endpoint
192
+ * config). Required by any app receiving Stripe webhooks —
193
+ * without it, `stripe.webhooks.constructEvent` has nothing to
194
+ * verify the `stripe-signature` header against.
195
+ */
196
+ webhookSecret?: string
171
197
  /**
172
198
  * Pinned Stripe API version. Defaults to whatever the bundled SDK
173
199
  * was compiled against; override here when rolling forward without
package/src/stacks.ts CHANGED
@@ -6,6 +6,9 @@ import type {
6
6
  BinaryConfig,
7
7
  CacheConfig,
8
8
  CloudConfig,
9
+ CmsConfig,
10
+ CommerceConfig,
11
+ CorsConfig,
9
12
  DashboardConfig,
10
13
  DatabaseConfig,
11
14
  DnsConfig,
@@ -17,6 +20,8 @@ import type {
17
20
  HashingConfig,
18
21
  LibraryConfig,
19
22
  LoggingConfig,
23
+ MarketingConfig,
24
+ MonitoringConfig,
20
25
  NotificationConfig,
21
26
  PaymentConfig,
22
27
  Ports,
@@ -67,6 +72,14 @@ export interface StacksOptions {
67
72
  */
68
73
  auth: AuthConfig
69
74
 
75
+ /**
76
+ * **CORS Options**
77
+ *
78
+ * Cross-origin resource sharing policy applied to API responses.
79
+ * Configured via `config/cors.ts`. See {@link CorsConfig}.
80
+ */
81
+ cors?: CorsConfig
82
+
70
83
  /**
71
84
  * **Realtime Options**
72
85
  *
@@ -103,12 +116,26 @@ export interface StacksOptions {
103
116
  */
104
117
  cloud: CloudConfig
105
118
 
119
+ /**
120
+ * **CMS Options**
121
+ *
122
+ * Top-level feature gate for the CMS bundle (Post / Page / Author /
123
+ * Comment / Tag / Category models + content-edit dashboards).
124
+ */
125
+ cms: CmsConfig
126
+
127
+ /**
128
+ * **Commerce Options**
129
+ *
130
+ * Top-level feature gate for the commerce bundle plus storefront defaults.
131
+ */
132
+ commerce: CommerceConfig
133
+
106
134
  /**
107
135
  * **Dashboard Options**
108
136
  *
109
- * Controls which sections render in the `buddy dev --dashboard` sidebar.
110
- * Set per-section `enabled` flags to `false` to hide a section that this
111
- * project doesn't use (e.g. `commerce.enabled: false`).
137
+ * Top-level feature gate plus per-section visibility toggles for the
138
+ * `buddy dev --dashboard` sidebar.
112
139
  */
113
140
  dashboard: DashboardConfig
114
141
 
@@ -193,6 +220,22 @@ export interface StacksOptions {
193
220
  */
194
221
  logging: LoggingConfig
195
222
 
223
+ /**
224
+ * **Marketing Options**
225
+ *
226
+ * Top-level feature gate for the marketing bundle (`/api/email/subscribe`,
227
+ * `/api/contact`, Campaign / EmailList / SocialPost).
228
+ */
229
+ marketing: MarketingConfig
230
+
231
+ /**
232
+ * **Monitoring Options**
233
+ *
234
+ * Top-level feature gate for the monitoring bundle (Error model +
235
+ * error-tracking views and actions).
236
+ */
237
+ monitoring: MonitoringConfig
238
+
196
239
  /**
197
240
  * **Notification Options**
198
241
  *