@stacksjs/defaults 0.75.460 → 0.75.463
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-actions/SKILL.md +2 -2
- package/ai/skills/stacks-api/SKILL.md +11 -8
- package/ai/skills/stacks-cms/SKILL.md +5 -3
- package/ai/skills/stacks-development/SKILL.md +1 -1
- package/ai/skills/stacks-jobs/SKILL.md +4 -1
- package/ai/skills/stacks-native/SKILL.md +496 -76
- package/ai/skills/stacks-pagination/SKILL.md +10 -0
- package/ai/skills/stacks-query-builder/SKILL.md +1 -1
- package/ai/skills/stacks-queue/SKILL.md +1 -1
- package/ai/skills/stacks-router/SKILL.md +13 -10
- package/ai/skills/stacks-search-engine/SKILL.md +4 -1
- package/ai/skills/stacks-slug/SKILL.md +9 -7
- package/ai/skills/stacks-testing/SKILL.md +14 -4
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
|
@@ -20,7 +20,7 @@ Server actions are reusable business logic units invoked from routes, events, or
|
|
|
20
20
|
|
|
21
21
|
```typescript
|
|
22
22
|
// app/Actions/CreateWidget.ts
|
|
23
|
-
import { Action } from '@stacksjs/actions'
|
|
23
|
+
import { Action } from '@stacksjs/actions/runtime'
|
|
24
24
|
import { toSnakeCaseKeys } from '@stacksjs/orm'
|
|
25
25
|
import { response } from '@stacksjs/router'
|
|
26
26
|
|
|
@@ -57,7 +57,7 @@ the rules used by an explicit `request.validate()` call; do not assume attaching
|
|
|
57
57
|
a model means every custom handler has already validated all its input.
|
|
58
58
|
|
|
59
59
|
```ts
|
|
60
|
-
import { Action } from '@stacksjs/actions'
|
|
60
|
+
import { Action } from '@stacksjs/actions/runtime'
|
|
61
61
|
import { schema } from '@stacksjs/validation'
|
|
62
62
|
|
|
63
63
|
export default new Action({
|
|
@@ -222,17 +222,20 @@ response.json({ message: 'OK' })
|
|
|
222
222
|
response.json(data, { status: 200, headers: { 'X-Custom': 'value' }, pretty: true })
|
|
223
223
|
|
|
224
224
|
// Status shortcuts
|
|
225
|
-
response.
|
|
225
|
+
response.success(data, 'Saved') // 200, { success: true, data, message }
|
|
226
|
+
response.created({ id: 1 }) // 201, { success: true, data }
|
|
226
227
|
response.noContent() // 204
|
|
227
|
-
response.badRequest(
|
|
228
|
-
response.unauthorized() // 401
|
|
229
|
-
response.forbidden() // 403
|
|
230
|
-
response.notFound() // 404
|
|
231
|
-
response.
|
|
228
|
+
response.badRequest('Bad', errors?) // 400
|
|
229
|
+
response.unauthorized() // 401
|
|
230
|
+
response.forbidden() // 403
|
|
231
|
+
response.notFound() // 404
|
|
232
|
+
response.validationError(errors) // 422
|
|
233
|
+
response.error('Conflict', 409) // (message, status = 400, errors?)
|
|
234
|
+
response.serverError() // 500
|
|
232
235
|
|
|
233
236
|
// Other content types
|
|
234
|
-
response.text('Hello',
|
|
235
|
-
response.
|
|
237
|
+
response.text('Hello', 200)
|
|
238
|
+
response.view('<h1>Hello</h1>') // HTML; there is no response.html()
|
|
236
239
|
response.redirect('/new-url', 302) // 301 | 302 | 303 | 307 | 308
|
|
237
240
|
```
|
|
238
241
|
|
|
@@ -102,7 +102,7 @@ cms.posts.fetchById(id: number): Promise<PostJsonResponse | undefined>
|
|
|
102
102
|
cms.posts.fetchAll(): Promise<PostJsonResponse[]>
|
|
103
103
|
cms.posts.fetchByStatus(status: 'published' | 'draft' | 'archived'): Promise<PostJsonResponse[]>
|
|
104
104
|
cms.posts.fetchByCategory(category: string): Promise<PostJsonResponse[]>
|
|
105
|
-
cms.posts.fetchByAuthor(
|
|
105
|
+
cms.posts.fetchByAuthor(authorId: number): Promise<PostJsonResponse[]>
|
|
106
106
|
cms.posts.fetchByMinViews(minViews: number): Promise<PostJsonResponse[]>
|
|
107
107
|
cms.posts.fetchPublishedAfter(timestamp: number): Promise<PostJsonResponse[]>
|
|
108
108
|
|
|
@@ -128,8 +128,10 @@ cms.authors.findByName(name: string): Promise<AuthorJsonResponse | undefined>
|
|
|
128
128
|
cms.authors.findByUserId(userId: number): Promise<AuthorJsonResponse | undefined>
|
|
129
129
|
cms.authors.fetchAll(): Promise<AuthorJsonResponse[]>
|
|
130
130
|
cms.authors.fetchById(id: number): Promise<AuthorJsonResponse | undefined>
|
|
131
|
-
cms.authors.
|
|
132
|
-
cms.authors.
|
|
131
|
+
cms.authors.findByUuid(uuid: string): Promise<AuthorJsonResponse | undefined>
|
|
132
|
+
cms.authors.update(id, data): Promise<AuthorJsonResponse | undefined>
|
|
133
|
+
cms.authors.destroy(id): Promise<AuthorJsonResponse>
|
|
134
|
+
cms.authors.destroyMany(ids: number[]): Promise<number>
|
|
133
135
|
```
|
|
134
136
|
|
|
135
137
|
## Tags API
|
|
@@ -92,7 +92,7 @@ buddy/src/commands/
|
|
|
92
92
|
- `buddy dev:system-tray` (alias: `dev:tray`) -- System tray dev server
|
|
93
93
|
|
|
94
94
|
### Build Commands
|
|
95
|
-
- `buddy build` --
|
|
95
|
+
- `buddy build` -- Interactive target picker in a terminal; with no TTY (CI) or no pick it builds the frontend views. The framework builds only with `--stacks` / `build:stacks`
|
|
96
96
|
- `buddy build components` -- Build component libraries
|
|
97
97
|
- `buddy build docs` -- Build documentation site
|
|
98
98
|
- `buddy build server` (alias: `build:docker`) -- Build Docker server image
|
|
@@ -132,7 +132,7 @@ buddy queue # queue management
|
|
|
132
132
|
- Prefer `default new Job({...})` for dispatch methods. Discovery also supports default objects with handle and class exports with a handle method
|
|
133
133
|
- The `handle()` method receives the payload passed to `dispatch()`
|
|
134
134
|
- `dispatchNow()` runs immediately in the current process - no queue involved
|
|
135
|
-
- Default queue driver is `sync`
|
|
135
|
+
- Default queue driver is `sync` locally and `database` when `APP_ENV=production` (`QUEUE_DRIVER` overrides both). On `sync`, `dispatchAfter` blocks the caller for the delay
|
|
136
136
|
- Jobs with `rate` are auto-discovered by the scheduler
|
|
137
137
|
- Backoff array `[10, 30, 60]` means: retry after 10s, then 30s, then 60s
|
|
138
138
|
- Jobs should be idempotent - safe to retry on failure
|
|
@@ -148,6 +148,9 @@ driver. `.withIdempotencyKey(key)` claims a persistent dispatch key.
|
|
|
148
148
|
Inside `db.transaction()` dispatch defers until commit by default;
|
|
149
149
|
`.afterCommit()` states that intent, and `.withoutCommit()` explicitly opts
|
|
150
150
|
out. Outside a transaction, afterCommit warns and dispatches immediately.
|
|
151
|
+
Only the fluent builder is transaction-aware: `SomeJob.dispatch()` on a
|
|
152
|
+
`new Job(...)` instance sends at once, even inside a transaction, so use
|
|
153
|
+
`job(name, payload)` there.
|
|
151
154
|
See `stacks-queue` for the complete envelope and batch lifecycle.
|
|
152
155
|
|
|
153
156
|
Scheduling a named job through `schedule.job()` runs it in the scheduler by
|
|
@@ -1,83 +1,503 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: stacks-native
|
|
3
|
-
description: Use when planning or implementing a Stacks feature, choosing a native API, or checking which framework capability already solves a task.
|
|
3
|
+
description: Use when planning or implementing a Stacks feature, choosing a native API, or checking which framework capability already solves a task. A verified cheat sheet of the idiomatic call for every subsystem (models, queries, actions, routes, auth, jobs, events, mail, cache, storage, search, CMS, commerce, AI, stx, CLI, tests, deploy) plus the native package catalog, driver evidence and cross-package gotchas.
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility: Bun >= 1.3.0, TypeScript
|
|
6
6
|
allowed-tools: Read Edit Write Bash Grep Glob
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
#
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
`
|
|
68
|
-
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
9
|
+
# Stacks, natively
|
|
10
|
+
|
|
11
|
+
Stacks already ships most of what an app needs. Find the row below, use that
|
|
12
|
+
idiom, and read the named skill before going past it. Each snippet was checked
|
|
13
|
+
against source. The **Gotcha** is the mistake that section most often causes.
|
|
14
|
+
|
|
15
|
+
Companion files, loaded only when needed:
|
|
16
|
+
[RECIPES.md](RECIPES.md) has end-to-end tracer bullets.
|
|
17
|
+
[CATALOG.md](CATALOG.md) maps every core package to its skill, entrypoint and
|
|
18
|
+
tests (browse one section, don't load all of it).
|
|
19
|
+
[CAPABILITIES.md](CAPABILITIES.md) records which drivers are supported, partial
|
|
20
|
+
or experimental.
|
|
21
|
+
|
|
22
|
+
## Ten rules
|
|
23
|
+
|
|
24
|
+
1. **Model first.** The schema, validation, relations, traits and factory all
|
|
25
|
+
live in one `defineModel()`. Migrations are generated from models, never
|
|
26
|
+
hand-written first.
|
|
27
|
+
2. **`useApi` before a custom action.** It registers authenticated CRUD at
|
|
28
|
+
runtime. Write an Action only when the behavior differs from CRUD.
|
|
29
|
+
3. **Override, don't fork.** A file at the same path under `app/` replaces
|
|
30
|
+
the default. Use `extendModel()` to add fields to a built-in model.
|
|
31
|
+
4. **Don't import what Stacks auto-imports.** Actions, routes, jobs,
|
|
32
|
+
listeners, middleware, controllers, app commands and stx scripts use the
|
|
33
|
+
injected names bare (see [Auto-imports](#auto-imports)). Import only names
|
|
34
|
+
that aren't injected, and in the few files that load without a booted app.
|
|
35
|
+
5. **When you do import, prefer runtime subpaths** such as
|
|
36
|
+
`@stacksjs/database/runtime` over full barrels. The barrels load tooling
|
|
37
|
+
that a request does not need.
|
|
38
|
+
6. **Money is integer minor units** (cents), everywhere.
|
|
39
|
+
7. **Writes in a transaction go through `tx`.** Model calls inside
|
|
40
|
+
`transaction()` do not join it.
|
|
41
|
+
8. **Failures can be silent.** `mail.send`, `notify` and `dispatch` resolve
|
|
42
|
+
on failure, and every `log.*` call is async. Check results, use the
|
|
43
|
+
`OrFail`/`AndCollect` variants, and `await log.flush()` before exiting.
|
|
44
|
+
9. **A config type is not driver support.** Check CAPABILITIES.md before
|
|
45
|
+
promising a provider.
|
|
46
|
+
10. **Prove it.** Run `./buddy lint`, the relevant `./buddy test`, and
|
|
47
|
+
`./buddy typecheck`. UI work also has to pass the `stacks-design-taste`
|
|
48
|
+
preflight.
|
|
49
|
+
|
|
50
|
+
## Auto-imports
|
|
51
|
+
|
|
52
|
+
The full list for this app is `storage/framework/auto-imports/RUNTIME.md`
|
|
53
|
+
(regenerate it with `./buddy generate:auto-imports`). The snippets below
|
|
54
|
+
follow it, so **a missing import is deliberate.**
|
|
55
|
+
|
|
56
|
+
- **Server, after boot:** every model (`User`, `Post`, ...) and every job
|
|
57
|
+
(`SendWelcomeEmail`, ...); `Action`, `route`, `response`, `request`,
|
|
58
|
+
`Middleware`, `url`, `schema`, `db`, `sql`, `log`, `config`, `path`, `HttpError`,
|
|
59
|
+
`Auth`, `dispatch`, `listen`, `mail`, `template`, `notify`, `emit`,
|
|
60
|
+
`emitToUser`, `createChannel`, `schedule`, `Feature`, `collect`, `t`,
|
|
61
|
+
`setLocale`, `makeHash`, `verifyHash`, `defineModel`; plus the exports of
|
|
62
|
+
`resources/functions/`.
|
|
63
|
+
- **`<script client>`:** signals (`state`, `derived`, `effect`, `watch`),
|
|
64
|
+
lifecycle (`onMount`, `onDestroy`), `navigate`, `toast`, `useFetch`,
|
|
65
|
+
`useQuery`, `useMutation`, `useColorMode`, `useDark`, `keptState`, and the
|
|
66
|
+
macros (`defineProps`, ...). On-demand helpers such as `useForm`,
|
|
67
|
+
`useScroll` and `copyToClipboard` are bundled when you call them.
|
|
68
|
+
- **Still import:**
|
|
69
|
+
- Names not in the list (`cache`, `env`, `Storage`, `transaction`, `job`,
|
|
70
|
+
`validate`, `defineEvents`, `defineGates`, `defineCommand`, ...).
|
|
71
|
+
- Type-only names, with `import type`.
|
|
72
|
+
- `Job` from `@stacksjs/queue`, because the global `Job` is the jobs-table
|
|
73
|
+
model.
|
|
74
|
+
- The `Error` and `Request` models, which are never made global.
|
|
75
|
+
- **Files that load without globals, so they import everything:**
|
|
76
|
+
- Model files, because `generate:migrations`, `migrate` and `seed` read
|
|
77
|
+
them without booting.
|
|
78
|
+
- Standalone scripts and framework packages.
|
|
79
|
+
- `.ts` modules imported by a client script.
|
|
80
|
+
- **Tests get the globals too:** `tests/setup.ts` runs
|
|
81
|
+
`await bootTestEnvironment()`, so bare-style actions and routes load under
|
|
82
|
+
`bun test`. An older app whose setup still calls `setupTestEnvironment()`
|
|
83
|
+
gets "Action is not defined" until it switches.
|
|
84
|
+
|
|
85
|
+
## Data
|
|
86
|
+
|
|
87
|
+
### Models (`stacks-models`, `stacks-orm`)
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
// app/Models/Post.ts: imports stay, because migrate/generate/seed load models without globals
|
|
91
|
+
import { defineModel } from '@stacksjs/orm'
|
|
92
|
+
import { schema } from '@stacksjs/validation'
|
|
93
|
+
|
|
94
|
+
export default defineModel({
|
|
95
|
+
name: 'Post',
|
|
96
|
+
table: 'posts',
|
|
97
|
+
belongsTo: ['User'], // adds user_id; generated CRUD derives the owner from the session
|
|
98
|
+
hasMany: { comments: 'Comment' }, // the object form names the relation explicitly
|
|
99
|
+
traits: {
|
|
100
|
+
useUuid: true,
|
|
101
|
+
useSoftDeletes: true, // useTimestamps is on unless set to false
|
|
102
|
+
useApi: { uri: 'posts', routes: ['index', 'store', 'show', 'update', 'destroy'] },
|
|
103
|
+
useSearch: { searchable: ['title'], filterable: ['status'] },
|
|
104
|
+
useSeeder: { count: 20 },
|
|
105
|
+
observe: true, // emits post:created / post:updated / post:deleted
|
|
106
|
+
taggable: true,
|
|
107
|
+
commentable: true,
|
|
108
|
+
},
|
|
109
|
+
attributes: {
|
|
110
|
+
title: { fillable: true, required: true, validation: { rule: schema.string().max(120) }, factory: f => f.lorem.words(3) },
|
|
111
|
+
status: { fillable: true, default: 'draft', validation: { rule: schema.enum(['draft', 'published']) } },
|
|
112
|
+
secret: { hidden: true, guarded: true, encrypted: true },
|
|
113
|
+
},
|
|
114
|
+
casts: { meta: 'json' }, // casts sit at model level; attributes have no `cast` key
|
|
115
|
+
} as const)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
- **Other attribute keys:** `unique`, `nullable`, `type`, `order`, `foreignKey`, `personal`.
|
|
119
|
+
- **Other traits:** `useAuth`, `billable`, `likeable`, `categorizable`,
|
|
120
|
+
`useAudit`, `useActivityLog`, `prunable`, `gdpr`, `broadcastOn`/`broadcastWith`.
|
|
121
|
+
- **Add fields to a built-in model:**
|
|
122
|
+
`export default extendModel(Order, { attributes: { note: {...} } })`.
|
|
123
|
+
- **Gotcha:** a misspelled trait only logs a warning and is otherwise ignored.
|
|
124
|
+
Event names come from the lowercased model name with no separator, so
|
|
125
|
+
`StatusReport` emits `statusreport:created`.
|
|
126
|
+
|
|
127
|
+
### Queries, transactions, pagination (`stacks-query-builder`, `stacks-database`, `stacks-pagination`)
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
const post = await Post.find(id) // undefined if missing; findOrFail throws
|
|
131
|
+
const rows = await Post.where('status', 'published').with('user').orderBy('id', 'desc').get()
|
|
132
|
+
const created = await Post.create({ title })
|
|
133
|
+
await created.update({ title: 'New' }); await created.delete()
|
|
134
|
+
await Post.update(id, data); await Post.delete(id) // static forms: row | undefined, boolean
|
|
135
|
+
const page = await Post.where('status', 'published').paginate(20, 2) // (perPage, page)
|
|
136
|
+
|
|
137
|
+
import { transaction } from '@stacksjs/orm'
|
|
138
|
+
await transaction(async tx => tx.insertInto('posts').values({ title }).execute(), { retries: 2 })
|
|
139
|
+
|
|
140
|
+
await db.updateTable('posts').set({ status: 'archived' }).where('id', '=', id).execute() // raw builder (db is a global)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
- **More helpers:** `first`, `count`, `exists`, `pluck`, `whereIn`, `latest`,
|
|
144
|
+
`firstOrCreate`, `updateOrCreate`, `createMany`, `simplePaginate`,
|
|
145
|
+
`cursorPaginate(perPage, cursor, ['created_at', 'id'])`, `withoutEvents`.
|
|
146
|
+
- **Pagination defaults:** with no arguments, `paginate()` reads
|
|
147
|
+
`?page&per_page`, uses 15 per page, and caps `per_page` at 100.
|
|
148
|
+
- **Gotcha:** `paginate` takes `(perPage, page)`, the reverse of upstream.
|
|
149
|
+
`.where(...).update()` and `.delete()` on a builder return a count, not rows.
|
|
150
|
+
|
|
151
|
+
### Migrations and seeding (`stacks-migrations`)
|
|
152
|
+
|
|
153
|
+
```sh
|
|
154
|
+
./buddy generate:migrations # diff models against the snapshot and write SQL; review it, then run again to confirm no diff
|
|
155
|
+
./buddy migrate # apply (--diff previews; non-interactive runs need --generate to also generate)
|
|
156
|
+
./buddy migrate:fresh --seed # dev only: drops everything
|
|
157
|
+
./buddy seed # skips tables that already have rows unless --fresh
|
|
158
|
+
./buddy migrate:status
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Gotcha:** commit the model, its generated migration and the
|
|
162
|
+
`model-snapshot.<dialect>.json` together. A stale snapshot fails the next deploy.
|
|
163
|
+
|
|
164
|
+
## HTTP
|
|
165
|
+
|
|
166
|
+
### Actions (`stacks-actions`)
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
// app/Actions/PublishPostAction.ts: Action, schema, response and Post are all globals
|
|
170
|
+
export default new Action({
|
|
171
|
+
name: 'Publish Post',
|
|
172
|
+
method: 'POST',
|
|
173
|
+
validations: { note: { rule: schema.string().max(200) } }, // validated before authorize/handle
|
|
174
|
+
async authorize(request) { return request.can('update', await Post.findOrFail(request.getParam('id'))) },
|
|
175
|
+
async handle(request) {
|
|
176
|
+
const post = await Post.findOrFail(request.getParam('id'))
|
|
177
|
+
await post.update({ status: 'published' })
|
|
178
|
+
return response.success(post)
|
|
179
|
+
},
|
|
180
|
+
})
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Gotcha:** `model: Post` only feeds rules to an explicit
|
|
184
|
+
`await request.validate()`. It validates nothing by itself. An `authorize`
|
|
185
|
+
that returns `false` produces a 403.
|
|
186
|
+
|
|
187
|
+
### Routes, middleware, responses (`stacks-router`, `stacks-middleware`, `stacks-api`)
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
// routes/api.ts (mounted at /api automatically)
|
|
191
|
+
route.post('/posts/{id}/publish', 'Actions/PublishPostAction').middleware('auth')
|
|
192
|
+
route.group({ prefix: '/v1', middleware: ['auth'] }, () => {
|
|
193
|
+
route.get('/me', 'Actions/Auth/GetMeAction').name('me').rateLimit(60, 'minute')
|
|
194
|
+
})
|
|
195
|
+
route.resource('tags', 'Tag', { only: ['index', 'show'] })
|
|
196
|
+
|
|
197
|
+
// Typed end to end: register imported actions, infer the client
|
|
198
|
+
import { createTypedClient, createTypedRouter } from '@stacksjs/router'
|
|
199
|
+
export const api = createTypedRouter().get('/v1/posts', PostIndexAction).post('/v1/posts', PostStoreAction, { middleware: 'auth' })
|
|
200
|
+
const client = createTypedClient<typeof api>({ baseUrl })
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
// app/Middleware/EnsureBeta.ts (Middleware is a global): return nothing to continue, throw to stop
|
|
205
|
+
export default new Middleware({ name: 'EnsureBeta', priority: 5, async handle(request) { /* ... */ } })
|
|
206
|
+
// app/Middleware.ts
|
|
207
|
+
import { defineMiddleware } from '@stacksjs/router'
|
|
208
|
+
export default defineMiddleware({ beta: 'EnsureBeta' }) // merged over the built-in aliases
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
- **Built-in middleware aliases:** `auth`, `guest`, `can:ability,model`,
|
|
212
|
+
`throttle`, `verified`, `role`, `permission`, `abilities`, `signed`, `csrf`,
|
|
213
|
+
`cors`, `site`, `env:production`, `maintenance`.
|
|
214
|
+
- **Responses:** `response.json(data, { status })`, `success(data, msg?)`,
|
|
215
|
+
`created(data)` (both wrap the payload as `{ success, data }`), `noContent()`,
|
|
216
|
+
`notFound(msg?)`, `forbidden()`, `unauthorized()`,
|
|
217
|
+
`validationError(errors)`, `error(msg, status = 400)`, `serverError()`,
|
|
218
|
+
`redirect(url)`, `text(s, status?)`, `view(html)`, `download(path)`.
|
|
219
|
+
Returning a plain object gives raw JSON.
|
|
220
|
+
- **Gotcha:** there is no `route.prefix()` (use `group({ prefix })`) and no
|
|
221
|
+
`response.html()` (use `view`). String action paths load lazily and don't
|
|
222
|
+
infer response types; pass the imported action when a typed client needs them.
|
|
223
|
+
|
|
224
|
+
### Validation and errors (`stacks-validation`, `stacks-error-handling`)
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
import { validate } from '@stacksjs/validation' // schema is a global, validate is not
|
|
228
|
+
const data = await validate(request, { email: schema.string().email().required() }) // 422 HttpError on failure
|
|
229
|
+
|
|
230
|
+
import { fromPromise } from '@stacksjs/error-handling'
|
|
231
|
+
const r = await fromPromise(fetchThing()); if (r.isErr) log.warn(r.error) // isOk / isErr are properties
|
|
232
|
+
throw new HttpError(409, 'Already exists') // HttpError is a global
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**Gotcha:** `validate()` returns only the declared fields that were sent.
|
|
236
|
+
Unknown fields are dropped silently.
|
|
237
|
+
|
|
238
|
+
## Auth and access (`stacks-auth`)
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
const session = await Auth.login({ email, password }) // { user, token, ... } | null
|
|
242
|
+
const user = await request.user() // set by the 'auth' middleware; undefined on public routes
|
|
243
|
+
const viewer = await Auth.user() // resolves the bearer token or cookie itself
|
|
244
|
+
const token = await Auth.createToken(user, 'cli', ['posts:read'])
|
|
245
|
+
|
|
246
|
+
// app/Gates.ts
|
|
247
|
+
import { defineGates } from '@stacksjs/auth'
|
|
248
|
+
export default defineGates({ gates: { 'edit-settings': user => user.isAdmin }, policies: { Post: 'PostPolicy' } })
|
|
249
|
+
// in an action
|
|
250
|
+
await request.authorize('update', post) // throws a 403
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
- **2FA:** `generateTwoFactorSetup`, `enableTwoFactor`, `verifyTwoFactorLoginCode`.
|
|
254
|
+
- **Passkeys:** `beginPasskeyRegistration` / `finishPasskeyAuthentication`. In the
|
|
255
|
+
browser, import from `@stacksjs/auth/passkeys-browser`.
|
|
256
|
+
- **Gotcha:** an ability with no gate and no policy method is denied. Don't
|
|
257
|
+
loosen the default row-scoping policy to make an example work. Declare an
|
|
258
|
+
owner instead (see RECIPES.md).
|
|
259
|
+
|
|
260
|
+
## Background work
|
|
261
|
+
|
|
262
|
+
### Jobs and queues (`stacks-jobs`, `stacks-queue`)
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
// app/Jobs/SendWelcomeEmail.ts: import Job, because the global Job is the jobs-table model
|
|
266
|
+
import { Job } from '@stacksjs/queue'
|
|
267
|
+
export default new Job({ name: 'SendWelcomeEmail', queue: 'emails', tries: 3, backoff: [10, 30, 60], async handle({ email }: { email: string }) { /* ... */ } })
|
|
268
|
+
|
|
269
|
+
await SendWelcomeEmail.dispatch({ email }) // global; also dispatchAfter(60, p), dispatchNow(p)
|
|
270
|
+
import { job } from '@stacksjs/queue'
|
|
271
|
+
await job('SendWelcomeEmail', { email }).delay(60).afterCommit().dispatch() // the transaction-aware builder
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**Gotcha:** only `job(...)` waits for a transaction to commit. Inside
|
|
275
|
+
`transaction()`, `Job.dispatch()` sends immediately. The driver is `sync`
|
|
276
|
+
locally and `database` in production, and durable drivers need a worker.
|
|
277
|
+
|
|
278
|
+
### Scheduler (`stacks-scheduler`)
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
// app/Scheduler.ts
|
|
282
|
+
export default function () {
|
|
283
|
+
schedule.job('Inspire').hourly().onQueue('default') // without onQueue it runs in the scheduler process
|
|
284
|
+
schedule.action('CleanupTempFiles').everyFiveMinutes().withoutOverlapping()
|
|
285
|
+
schedule.command('echo hi').dailyAt('02:00').onOneServer()
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Gotcha:** keep each chain synchronous, because configuration applies in a
|
|
290
|
+
microtask.
|
|
291
|
+
|
|
292
|
+
### Events and listeners (`stacks-events`, `stacks-listeners`)
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
// app/Events.ts: values resolve against app/Listeners, then app/Actions
|
|
296
|
+
import { defineEvents } from '@stacksjs/events'
|
|
297
|
+
export default defineEvents({ 'user:registered': ['SendWelcomeEmail'] })
|
|
298
|
+
// app/Listeners/NotifyOps.ts
|
|
299
|
+
import { defineListener } from '@stacksjs/events'
|
|
300
|
+
export default defineListener({ listensTo: 'invoice:settled', handle: async (payload) => { /* ... */ } })
|
|
301
|
+
|
|
302
|
+
dispatch('invoice:settled', { id }) // fire and forget (global)
|
|
303
|
+
import { dispatchAndCollect } from '@stacksjs/events'
|
|
304
|
+
const results = await dispatchAndCollect('invoice:settled', { id }) // when you need listener failures
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
**Gotcha:** the event function is `dispatch`. The `emit` global is realtime
|
|
308
|
+
broadcasting, not events.
|
|
309
|
+
|
|
310
|
+
## Messaging
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
// Mail (stacks-mail, stacks-email): mail, template, notify and emit are globals
|
|
314
|
+
const { html, text } = await template('welcome', { variables: { name }, subject: 'Hi' })
|
|
315
|
+
await mail.sendOrFail({ to: [email], subject: 'Hi', html, text }) // send() resolves on failure
|
|
316
|
+
const r = await new WelcomeMail(user).send(); if (!r.success) await log.error(r.message)
|
|
317
|
+
|
|
318
|
+
// Every channel at once (stacks-notifications)
|
|
319
|
+
const results = await notify({ userId, email, phone }, { subject: 'Shipped', body }, ['email', 'sms', 'database'])
|
|
320
|
+
|
|
321
|
+
// Single channels (stacks-sms, stacks-push, stacks-chat)
|
|
322
|
+
import { sms } from '@stacksjs/sms'; await sms().to('+15551234567').body('Code 1234').send()
|
|
323
|
+
import { send as push } from '@stacksjs/push'; await push(token, { title, body }, { driver: 'fcm' })
|
|
324
|
+
import { send as chat } from '@stacksjs/chat'; await chat({ content: 'Deployed' }, { driver: 'slack' })
|
|
325
|
+
|
|
326
|
+
// Realtime (stacks-realtime)
|
|
327
|
+
emit('orders', 'created', { id }) // emitToUser(42, 'notice', data) targets private-user.42
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
**Gotcha:** `notify` resolves even when a channel failed, so check each
|
|
331
|
+
`result.success`. Only `notify` honors SMS STOP opt-outs. `emit` reaches only a
|
|
332
|
+
broadcast server running in the same process; otherwise it warns and does
|
|
333
|
+
nothing.
|
|
334
|
+
|
|
335
|
+
## Platform services
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
// Cache (stacks-cache): not a global. TTL in seconds, 0 = forever, omitted = config.cache.ttl
|
|
339
|
+
import { cache } from '@stacksjs/cache'
|
|
340
|
+
await cache.remember(`post:${id}`, 300, () => Post.find(id)); await cache.del(`post:${id}`)
|
|
341
|
+
|
|
342
|
+
// Storage (stacks-storage)
|
|
343
|
+
import { Storage } from '@stacksjs/storage' // the global `storage` is a lower-level namespace, not this facade
|
|
344
|
+
const saved = await Storage.put(file, { disk: 'public', dir: 'avatars' }) // { path, url, ... }
|
|
345
|
+
await Storage.disk('s3').write('x.txt', bytes) // disk() returns an adapter: write/read/deleteFile/publicUrl
|
|
346
|
+
|
|
347
|
+
// Search (stacks-search-engine), from the useSearch trait
|
|
348
|
+
const hits = await Post.search('widget').where({ status: 'published' }).paginate(20, 1)
|
|
349
|
+
|
|
350
|
+
// Logging, env, config (stacks-logging, stacks-env, stacks-config)
|
|
351
|
+
await log.info('Order created', { orderId }); await log.error('Charge failed', err, { orderId })
|
|
352
|
+
import { env } from '@stacksjs/env'
|
|
353
|
+
const debug = env.DEBUG; const name = config.app.name // config and log are globals; declare env keys in config/env.ts
|
|
354
|
+
|
|
355
|
+
// i18n (stacks-i18n): t is a global; strings live in locales/*.yml with {name} interpolation
|
|
356
|
+
t('greeting', { name })
|
|
357
|
+
|
|
358
|
+
// Feature flags vs bundles (stacks-feature-flags, stacks-features)
|
|
359
|
+
if (await Feature.for(user.id).active('new-checkout')) { /* user-scoped rollout, async */ }
|
|
360
|
+
import { feature } from '@stacksjs/config'; if (feature('commerce')) { /* framework bundle, sync */ }
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
**Gotcha:** `cache` has no `forget()`; use `del`. `Storage.disk()` has no
|
|
364
|
+
`put`/`get`. The search index holds only `id` plus the listed fields. Read
|
|
365
|
+
`config` after `await overridesReady` in boot-time code. `setLocale` is
|
|
366
|
+
process-global; use `applyRequestLocale(req)` per request.
|
|
367
|
+
|
|
368
|
+
## Domains
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
// CMS (stacks-cms): built-in Post, Author, Page, Menu, Redirect, Comment, Tag models
|
|
372
|
+
import { authors, posts } from '@stacksjs/cms'
|
|
373
|
+
const published = await posts.fetchByStatus('published'); await posts.fetchByAuthor(authorId)
|
|
374
|
+
// override a CMS endpoint: create app/Actions/Cms/PostIndexAction.ts
|
|
375
|
+
|
|
376
|
+
// Payments and commerce (stacks-payments, stacks-commerce): integer cents
|
|
377
|
+
import { Payment } from '@stacksjs/payments'
|
|
378
|
+
const session = await Payment.checkout(user, { mode: 'payment', lines: [{ price: 'price_x', quantity: 1 }], successUrl, cancelUrl })
|
|
379
|
+
await Payment.subscribe(user, 'premium-monthly')
|
|
380
|
+
import { commerce } from '@stacksjs/commerce'; await commerce.orders.placeOrder({ order, items, payment, idempotencyKey })
|
|
381
|
+
|
|
382
|
+
// AI (stacks-ai): provider comes from config ai.default, a provider name or model id
|
|
383
|
+
import { createAIClient, createEmbedding } from '@stacksjs/ai'
|
|
384
|
+
const { content } = await createAIClient(config.ai).generate([{ role: 'user', content: 'Hi' }], { system: 'Be brief.' })
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
- **Sites:** `@stacksjs/sites` provides `siteResolver`, `currentSite()` and
|
|
388
|
+
`forSite(query, column, siteId)`. Queries are not scoped to a site automatically.
|
|
389
|
+
- **Newsletter:** `newsletter.subscribe(email, { list })` from `@stacksjs/newsletter`.
|
|
390
|
+
- **Analytics:** render `getAnalyticsHead(analytics)` in your layout. Nothing
|
|
391
|
+
is injected for you.
|
|
392
|
+
- **Images:** `image(src).widths([320, 640]).formats(['avif', 'webp']).generate()`.
|
|
393
|
+
- **Charts:** `new Chart(canvas, { type, data })` from `@stacksjs/charts`.
|
|
394
|
+
- **Gotcha:** the default `User` is not `billable`. Publish it and enable the
|
|
395
|
+
trait before calling `user.checkout()`. `ask()`/`summarize()` always use
|
|
396
|
+
Bedrock, and `createEmbedding` defaults to OpenAI.
|
|
397
|
+
|
|
398
|
+
## Utilities (`stacks-strings`, `stacks-arrays`, `stacks-collections`, `stacks-datetime`, `stacks-slug`)
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
import { camelCase, plural, slug, truncate } from '@stacksjs/strings' // slug('Hello World') -> 'hello-world'
|
|
402
|
+
import { chunk, groupBy, sortBy, unique } from '@stacksjs/arrays'
|
|
403
|
+
collect(users).pluck('name').toArray() // collect is a server global
|
|
404
|
+
import { now } from '@stacksjs/datetime'; now().addDays(3).format('YYYY-MM-DD') // immutable, months 1-based
|
|
405
|
+
import { uniqueSlug } from '@stacksjs/slug'; await uniqueSlug(title, { table: 'posts', column: 'slug' })
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
**Gotcha:** `uniqueSlug` checks first and writes later, so keep a UNIQUE
|
|
409
|
+
index. `arrays.range()` is a statistic (max minus min), not a sequence.
|
|
410
|
+
|
|
411
|
+
## Frontend (`stacks-stx`, `stacks-composables`, `stacks-ts-css`, `stacks-ui`)
|
|
412
|
+
|
|
413
|
+
```stx
|
|
414
|
+
@extends('layouts/default')
|
|
415
|
+
<script server>
|
|
416
|
+
const post = await Post.where('slug', String(params?.slug || '')).first() // resources/views/posts/[slug].stx
|
|
417
|
+
if (!post) notFound()
|
|
418
|
+
</script>
|
|
419
|
+
<script client>
|
|
420
|
+
const likes = state(0)
|
|
421
|
+
const label = derived(() => `${likes()} likes`)
|
|
422
|
+
const { data: comments, loading } = useFetch('/api/comments')
|
|
423
|
+
</script>
|
|
424
|
+
@section('content')
|
|
425
|
+
<h1 class="text-2xl font-semibold dark:text-white">{{ post.title }}</h1>
|
|
426
|
+
@foreach (post.tags as tag) <span>{{ tag }}</span> @endforeach
|
|
427
|
+
<button @click="likes.update(n => n + 1)"><i class="i-hugeicons-favourite"></i> {{ label() }}</button>
|
|
428
|
+
<Card :title="post.title" />
|
|
429
|
+
@endsection
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
- **Signals:** read with `count()`, write with `.set()` or `.update()`.
|
|
433
|
+
There is no `.value`.
|
|
434
|
+
- **Composables in client scripts:** `useFetch`, `useQuery`, `useMutation`,
|
|
435
|
+
`navigate`, `toast.success()`, `useColorMode`, `useDark`, `keptState`.
|
|
436
|
+
`useForm` is delivered on demand.
|
|
437
|
+
- **Gotchas:**
|
|
438
|
+
- No `var`, `document` or `window` in stx scripts.
|
|
439
|
+
- `@foreach` takes no `$` sigil.
|
|
440
|
+
- Class names must be complete static strings, or added to `safelist`.
|
|
441
|
+
- The bare stx `useForm(schema, initial)` and the imported
|
|
442
|
+
`@stacksjs/composables` `useForm({ ... })` are different APIs.
|
|
443
|
+
- Imported `.ts` modules get no auto-imports.
|
|
444
|
+
|
|
445
|
+
## CLI, tests, dev, deploy
|
|
446
|
+
|
|
447
|
+
```ts
|
|
448
|
+
// app/Commands/Greet.ts, auto-discovered (scaffold with: buddy make:command)
|
|
449
|
+
import { defineCommand } from '@stacksjs/cli'
|
|
450
|
+
export default defineCommand({
|
|
451
|
+
name: 'greet <who>', description: 'Greet a user',
|
|
452
|
+
options: { '--loud, -l': { description: 'Shout', default: false } },
|
|
453
|
+
handle(options, who) { console.log(options.loud ? who.toUpperCase() : who) }, // options come first
|
|
454
|
+
})
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
```ts
|
|
458
|
+
// tests/feature/posts.test.ts (stacks-testing)
|
|
459
|
+
import { actingAs, describe, expect, test } from '@stacksjs/testing'
|
|
460
|
+
import { assertDatabaseHas, factory, refreshDatabase } from '@stacksjs/testing/database' // DB helpers come from /database
|
|
461
|
+
test('creates a post', async () => {
|
|
462
|
+
await refreshDatabase()
|
|
463
|
+
const user = await factory('User').create() // actingAs needs a persisted user
|
|
464
|
+
await actingAs(user).post('/api/posts', { title: 'Hi' })
|
|
465
|
+
await assertDatabaseHas('posts', { title: 'Hi' })
|
|
466
|
+
})
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
```sh
|
|
470
|
+
./buddy dev # frontend :3000, api :3008, dashboard :3002, docs :3006 (also dev:api, dev:dashboard)
|
|
471
|
+
./buddy test # --feature / --unit / a path
|
|
472
|
+
./buddy build # interactive picker; without a TTY it builds the frontend views
|
|
473
|
+
./buddy deploy --dry-run # preview first; CI needs --yes. Undo with buddy deploy:rollback
|
|
474
|
+
./buddy tinker # REPL with the app booted
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
**Dashboard, desktop, mobile:** override a dashboard page at
|
|
478
|
+
`resources/views/dashboard/<path>.stx` (`stacks-dashboard`).
|
|
479
|
+
`./buddy dev:desktop` runs the app in a native window (`stacks-desktop`). The
|
|
480
|
+
mobile build reuses the stx views (`stacks-mobile`).
|
|
481
|
+
|
|
482
|
+
## Cross-cutting boundaries
|
|
483
|
+
|
|
484
|
+
- **Bundles and routes are separate.** Optional bundles control which models,
|
|
485
|
+
migrations and views are selected. Route mounting has its own registry, so
|
|
486
|
+
an installed package does not mean its routes are mounted or public.
|
|
487
|
+
- **Four kinds of browser names.** Browser declarations, stx eager bindings,
|
|
488
|
+
demand-inline bindings and imported composables are different surfaces.
|
|
489
|
+
Read `stacks-auto-imports` before using an unfamiliar bare name.
|
|
490
|
+
- **Ownership.** Generated CRUD derives the owner from the authenticated user.
|
|
491
|
+
Test it with real requests and another user's rows, not a source grep.
|
|
492
|
+
- **Event dispatch.** Ordered dispatch is not the same as awaiting listeners.
|
|
493
|
+
Use the result-bearing API when the caller needs outcomes.
|
|
494
|
+
- **Skill vs source.** If a skill and the source disagree, trust the source and
|
|
495
|
+
fix the skill in the same change.
|
|
496
|
+
|
|
497
|
+
## Keeping this current
|
|
498
|
+
|
|
499
|
+
Inside the framework repository, `./buddy docs:native-skills` regenerates
|
|
500
|
+
CATALOG.md and CAPABILITIES.md. `./buddy docs:native-skills:check` reports a
|
|
501
|
+
package without guidance or a stale reference. That check proves coverage, not
|
|
502
|
+
that every sentence is right, so verify an idiom against its source entrypoint
|
|
503
|
+
before relying on an edge case.
|
|
@@ -34,6 +34,16 @@ BigInts as decimal string entries inside a JSON array. Keep those tokens intact
|
|
|
34
34
|
through query parameters; the ORM parser restores tuples and SQL bindings
|
|
35
35
|
accept decimal strings without a lossy Number conversion.
|
|
36
36
|
|
|
37
|
+
## Call order
|
|
38
|
+
|
|
39
|
+
Stacks model pagination takes `(perPage, page)`, the reverse of upstream
|
|
40
|
+
bun-query-builder: `Post.where('status', 'published').paginate(20, 2)`. With
|
|
41
|
+
no arguments it reads `?page` and `?per_page` from the request, defaults to 15
|
|
42
|
+
per page, and caps a query-string `per_page` at 100 (an explicit perPage is not
|
|
43
|
+
capped). `simplePaginate(perPage, page)` and
|
|
44
|
+
`cursorPaginate(perPage, cursor, columns, direction)` follow the same order.
|
|
45
|
+
The ORM paginator does not read the query builder's `pagination.defaultPerPage`.
|
|
46
|
+
|
|
37
47
|
## HTTP and UI
|
|
38
48
|
|
|
39
49
|
ORM request-aware pagination can derive page/limit and attach URLs from the
|
|
@@ -555,7 +555,7 @@ await SendWelcomeEmail.dispatchNow({ email, name }) // bypass queue
|
|
|
555
555
|
## config/queue.ts
|
|
556
556
|
```typescript
|
|
557
557
|
{
|
|
558
|
-
default: '
|
|
558
|
+
default: env.QUEUE_DRIVER || (env.APP_ENV === 'production' ? 'database' : 'sync'), // sync | database | redis
|
|
559
559
|
connections: {
|
|
560
560
|
sync: { driver: 'sync' },
|
|
561
561
|
database: { driver: 'database', table: 'jobs', queue: 'default', retryAfter: 90 },
|
|
@@ -312,16 +312,19 @@ await req.tokenCant('delete-users')
|
|
|
312
312
|
import { response } from '@stacksjs/router'
|
|
313
313
|
|
|
314
314
|
response.json(data, { status: 200 })
|
|
315
|
-
response.
|
|
316
|
-
response.
|
|
317
|
-
response.
|
|
318
|
-
response.
|
|
319
|
-
response.
|
|
320
|
-
response.
|
|
321
|
-
response.
|
|
315
|
+
response.success(data, 'Saved') // 200, { success: true, data, message }
|
|
316
|
+
response.created(data, location?) // 201, { success: true, data }
|
|
317
|
+
response.noContent() // 204
|
|
318
|
+
response.badRequest(message?, errors?) // 400
|
|
319
|
+
response.unauthorized(message?) // 401
|
|
320
|
+
response.forbidden(message?) // 403
|
|
321
|
+
response.notFound(message?) // 404
|
|
322
|
+
response.validationError(errors) // 422
|
|
323
|
+
response.error(message, status?) // defaults to 400, not 500
|
|
324
|
+
response.serverError(message?) // 500
|
|
322
325
|
response.redirect(url, 302)
|
|
323
|
-
response.text('hello')
|
|
324
|
-
response.
|
|
326
|
+
response.text('hello', 200)
|
|
327
|
+
response.view('<h1>Hi</h1>') // HTML; there is no response.html()
|
|
325
328
|
```
|
|
326
329
|
|
|
327
330
|
## Error Responses
|
|
@@ -367,7 +370,7 @@ route.use(logger)
|
|
|
367
370
|
```
|
|
368
371
|
|
|
369
372
|
### Available Middleware Aliases (from app/Middleware.ts)
|
|
370
|
-
maintenance, auth, guest, api, team, logger, abilities, can, throttle,
|
|
373
|
+
maintenance, cors, auth, guest, api, team, site, logger, abilities, can, throttle, signed, env, env:local, env:development, env:dev, env:staging, env:production, env:prod, role, permission, verified, csrf, compress
|
|
371
374
|
|
|
372
375
|
## Query Tracking
|
|
373
376
|
|
|
@@ -44,7 +44,10 @@ await search.addDocuments('products', [
|
|
|
44
44
|
])
|
|
45
45
|
|
|
46
46
|
// Search
|
|
47
|
-
const results = await search.search('products', 'widget')
|
|
47
|
+
const results = await search.search('products', { q: 'widget', limit: 20, filter: { status: 'active' } })
|
|
48
|
+
|
|
49
|
+
// From a model with the useSearch trait (the usual call)
|
|
50
|
+
const hits = await Product.search('widget').where({ status: 'active' }).get()
|
|
48
51
|
|
|
49
52
|
// List indexes
|
|
50
53
|
const indexes = await search.listAllIndexes()
|
|
@@ -38,14 +38,16 @@ const slug = await uniqueSlug('Product Name', {
|
|
|
38
38
|
## Basic Slugify (No Database Check)
|
|
39
39
|
|
|
40
40
|
```typescript
|
|
41
|
-
import {
|
|
41
|
+
import { slug } from '@stacksjs/strings'
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
slug('Hello World!') // 'hello-world'
|
|
44
|
+
slug('Ünïcödé Têxt') // 'unicode-text'
|
|
45
|
+
slug(' Extra Spaces ') // 'extra-spaces'
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
`@stacksjs/slug` exports only `uniqueSlug`. The sync helpers live in
|
|
49
|
+
`@stacksjs/strings`: `slug()` lowercases and strips punctuation, while the
|
|
50
|
+
lower-level `slugify()` defaults to `lower: false` and keeps case.
|
|
49
51
|
|
|
50
52
|
## Model Usage
|
|
51
53
|
|
|
@@ -77,14 +79,14 @@ interface SlugifyOptions {
|
|
|
77
79
|
## Gotchas
|
|
78
80
|
- Omitting either table or column returns the base slug with no query.
|
|
79
81
|
- A title with no letters or digits at all (`!!!`, an emoji) gets a random
|
|
80
|
-
eight-character slug from `uniqueSlug()`, never `""`; `
|
|
82
|
+
eight-character slug from `uniqueSlug()`, never `""`; `slug()` still returns `""`.
|
|
81
83
|
- The check and insert are separate: keep a UNIQUE index and handle/retry an
|
|
82
84
|
insert conflict. A helper cannot reserve the returned slug against a race.
|
|
83
85
|
- Collision attempts are bounded; after repeated conflicts the helper returns
|
|
84
86
|
a cryptographically randomized suffix instead of looping forever.
|
|
85
87
|
- Declaring a slug attribute does not itself populate it from the title.
|
|
86
88
|
- `uniqueSlug()` is async - it queries the database
|
|
87
|
-
- `
|
|
89
|
+
- `slug()` is sync - no database interaction
|
|
88
90
|
- Collision detection appends `-2`, `-3`, etc. (not random suffixes)
|
|
89
91
|
- Unicode characters are transliterated (u → u, e → e, etc.) where ts-slug's charmap
|
|
90
92
|
romanizes the script (Latin, Greek, Cyrillic, Hebrew, Arabic, Devanagari, Georgian);
|
|
@@ -24,11 +24,21 @@ before deciding what to test, and this one for how.
|
|
|
24
24
|
## Test Setup
|
|
25
25
|
|
|
26
26
|
```typescript
|
|
27
|
-
|
|
28
|
-
import {
|
|
27
|
+
// tests/setup.ts, the bunfig [test] preload
|
|
28
|
+
import { bootTestEnvironment } from '@stacksjs/testing'
|
|
29
|
+
|
|
30
|
+
// NODE_ENV/APP_ENV = 'test', then the server auto-imports (models, jobs,
|
|
31
|
+
// Action, route, response, ...), app listeners and gates
|
|
32
|
+
await bootTestEnvironment()
|
|
33
|
+
```
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
`bun test` never runs the framework preloader, so this is what makes the
|
|
36
|
+
auto-imported names exist in a test. Without it, loading an action written
|
|
37
|
+
without imports throws "Action is not defined". `setupTestEnvironment()` still
|
|
38
|
+
exists and only sets the env.
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { refreshDatabase, setupDatabase, truncateMysql, truncateSqlite } from '@stacksjs/testing/database'
|
|
32
42
|
|
|
33
43
|
// MySQL database setup; this helper does not initialize SQLite
|
|
34
44
|
await setupDatabase()
|
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.463",
|
|
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.463",
|
|
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.463",
|
|
49
49
|
"@stacksjs/sanitizer": "0.2.798",
|
|
50
50
|
"@stacksjs/stx": "0.2.798",
|
|
51
51
|
"@xterm/addon-fit": "^0.11.0",
|