@pikku/skills 0.12.4 → 0.12.8
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/dist/skills.gen.js +1 -1
- package/package.json +1 -1
- package/skills/pikku-addon/SKILL.md +74 -33
- package/skills/pikku-ai-agent/SKILL.md +197 -105
- package/skills/pikku-ai-vercel/SKILL.md +57 -18
- package/skills/pikku-ai-voice/SKILL.md +126 -52
- package/skills/pikku-audit/SKILL.md +35 -13
- package/skills/pikku-aws/SKILL.md +66 -16
- package/skills/pikku-backblaze/SKILL.md +44 -11
- package/skills/pikku-better-auth/SKILL.md +45 -10
- package/skills/pikku-cli/SKILL.md +67 -18
- package/skills/pikku-cli/references/complete-example.md +2 -0
- package/skills/pikku-concepts/SKILL.md +75 -10
- package/skills/pikku-config/SKILL.md +56 -14
- package/skills/pikku-cron/SKILL.md +13 -6
- package/skills/pikku-deploy-azure/SKILL.md +83 -28
- package/skills/pikku-deploy-cloudflare/SKILL.md +79 -37
- package/skills/pikku-deploy-express/SKILL.md +40 -4
- package/skills/pikku-deploy-fastify/SKILL.md +22 -1
- package/skills/pikku-deploy-lambda/SKILL.md +99 -19
- package/skills/pikku-deploy-nextjs/SKILL.md +49 -5
- package/skills/pikku-deploy-uws/SKILL.md +54 -1
- package/skills/pikku-deps/SKILL.md +29 -8
- package/skills/pikku-emails/SKILL.md +36 -5
- package/skills/pikku-fabric/SKILL.md +27 -2
- package/skills/pikku-fabric-debug/SKILL.md +5 -1
- package/skills/pikku-feature/SKILL.md +6 -1
- package/skills/pikku-gateway-slack/SKILL.md +72 -11
- package/skills/pikku-http/SKILL.md +18 -5
- package/skills/pikku-http/references/http-options.md +10 -5
- package/skills/pikku-i18n/SKILL.md +18 -7
- package/skills/pikku-info/SKILL.md +18 -8
- package/skills/pikku-jose/SKILL.md +35 -6
- package/skills/pikku-knowledge/SKILL.md +50 -7
- package/skills/pikku-kysely/SKILL.md +78 -15
- package/skills/pikku-machine-auth/SKILL.md +36 -1
- package/skills/pikku-mcp/SKILL.md +159 -149
- package/skills/pikku-middleware/SKILL.md +17 -5
- package/skills/pikku-mongodb/SKILL.md +10 -2
- package/skills/pikku-n8n-import/SKILL.md +14 -6
- package/skills/pikku-permissions/SKILL.md +102 -22
- package/skills/pikku-pino/SKILL.md +12 -4
- package/skills/pikku-product-second-opinion/SKILL.md +3 -3
- package/skills/pikku-queue/SKILL.md +45 -16
- package/skills/pikku-react/SKILL.md +41 -14
- package/skills/pikku-react-query/SKILL.md +14 -10
- package/skills/pikku-realtime/SKILL.md +44 -22
- package/skills/pikku-redis/SKILL.md +12 -3
- package/skills/pikku-rpc/SKILL.md +23 -12
- package/skills/pikku-rtl/SKILL.md +21 -17
- package/skills/pikku-scenario/SKILL.md +141 -76
- package/skills/pikku-schedule/SKILL.md +39 -6
- package/skills/pikku-schema-ajv/SKILL.md +24 -2
- package/skills/pikku-schema-cfworker/SKILL.md +22 -2
- package/skills/pikku-security/SKILL.md +54 -9
- package/skills/pikku-services/SKILL.md +49 -9
- package/skills/pikku-services/references/audit-wire-service.md +2 -1
- package/skills/pikku-template-clone/SKILL.md +10 -5
- package/skills/pikku-trigger/SKILL.md +50 -6
- package/skills/pikku-versioning/SKILL.md +46 -17
- package/skills/pikku-websocket/SKILL.md +72 -44
- package/skills/pikku-workflow/SKILL.md +123 -11
- package/skills/pikku-workflow/references/workflow-reference.md +13 -8
- package/skills/pikku-workflows-client/SKILL.md +13 -6
- package/skills/pikku-ws/SKILL.md +44 -8
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pikku-deploy-azure
|
|
3
3
|
description: >-
|
|
4
|
-
Use when deploying a Pikku app to Azure Functions. Covers
|
|
5
|
-
|
|
6
|
-
Azure deployment, or @pikku/azure-functions. DO NOT TRIGGER when:
|
|
7
|
-
(use pikku-deploy-lambda) or Cloudflare Workers (use
|
|
4
|
+
Use when deploying a Pikku app to Azure Functions. Covers createAzureHandler for HTTP, storage
|
|
5
|
+
queue and timer triggers, plus AzInvocationLogger and PikkuAZTimerRequest. TRIGGER when: user
|
|
6
|
+
asks about Azure Functions, Azure deployment, or @pikku/azure-functions. DO NOT TRIGGER when:
|
|
7
|
+
user asks about AWS Lambda (use pikku-deploy-lambda) or Cloudflare Workers (use
|
|
8
|
+
pikku-deploy-cloudflare).
|
|
8
9
|
---
|
|
9
10
|
|
|
10
11
|
# Pikku Azure Functions Deployment
|
|
@@ -29,43 +30,97 @@ yarn add @pikku/azure-functions @azure/functions
|
|
|
29
30
|
|
|
30
31
|
## API Reference
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
Exported from `@pikku/azure-functions`:
|
|
34
|
+
|
|
35
|
+
- `createAzureHandler(factories, handlerTypes)` — the entry point. Returns
|
|
36
|
+
`{ http?, queue?, timer? }` for the handler types you ask for.
|
|
37
|
+
- `createAzureWorkerHandler(factories)` — `createAzureHandler(factories, ['fetch'])`.
|
|
38
|
+
- `createAzureWebSocketHandler(factories)` — **a stub**: its `negotiate` always
|
|
39
|
+
answers `501 WebSocket via Azure Web PubSub not yet implemented`. Channels do
|
|
40
|
+
not work on Azure yet; do not plan a deployment around it.
|
|
41
|
+
- `AzInvocationLogger` — the logger. Note the name: there is no
|
|
42
|
+
`PikkuAzFunctionsLogger`.
|
|
43
|
+
- `PikkuAZTimerRequest` — `new PikkuAZTimerRequest(context, data)`, a
|
|
44
|
+
`PikkuRequest` carrying the data. The context argument is accepted and
|
|
45
|
+
ignored.
|
|
46
|
+
- `AzureQueueService`, `AzureDeploymentService`.
|
|
47
|
+
|
|
48
|
+
`factories` is `{ createConfig, createSingletonServices, createPlatformServices? }`.
|
|
49
|
+
Services are built from `process.env` and cached in module scope across
|
|
50
|
+
invocations of the same instance.
|
|
39
51
|
|
|
40
52
|
## Usage Patterns
|
|
41
53
|
|
|
42
|
-
###
|
|
54
|
+
### Registering handlers
|
|
43
55
|
|
|
44
56
|
```typescript
|
|
45
57
|
import { app } from '@azure/functions'
|
|
46
|
-
import {
|
|
58
|
+
import { createAzureHandler } from '@pikku/azure-functions'
|
|
59
|
+
import { createConfig, createSingletonServices } from './services.js'
|
|
60
|
+
import './.pikku/pikku-bootstrap.gen.js'
|
|
61
|
+
|
|
62
|
+
const handlers = createAzureHandler(
|
|
63
|
+
{ createConfig, createSingletonServices },
|
|
64
|
+
['fetch', 'queue', 'scheduled']
|
|
65
|
+
)
|
|
47
66
|
|
|
48
67
|
app.http('api', {
|
|
49
|
-
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
68
|
+
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
|
|
50
69
|
route: '{*path}',
|
|
51
|
-
handler:
|
|
52
|
-
const logger = new PikkuAzFunctionsLogger(context)
|
|
53
|
-
// Wire Pikku HTTP runner with Azure request/response
|
|
54
|
-
},
|
|
70
|
+
handler: handlers.http as any,
|
|
55
71
|
})
|
|
56
|
-
```
|
|
57
72
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
app.storageQueue('queue', {
|
|
74
|
+
queueName: 'my-queue',
|
|
75
|
+
connection: 'AzureWebJobsStorage',
|
|
76
|
+
handler: handlers.queue as any,
|
|
77
|
+
})
|
|
63
78
|
|
|
64
79
|
app.timer('scheduler', {
|
|
65
80
|
schedule: '0 */5 * * * *',
|
|
66
|
-
handler:
|
|
67
|
-
const request = new PikkuAzTimerRequest(timer)
|
|
68
|
-
// Process scheduled Pikku functions
|
|
69
|
-
},
|
|
81
|
+
handler: handlers.timer as any,
|
|
70
82
|
})
|
|
71
83
|
```
|
|
84
|
+
|
|
85
|
+
Note the key names: `handlerTypes` uses **`scheduled`**, but the handler it
|
|
86
|
+
returns is **`timer`**.
|
|
87
|
+
|
|
88
|
+
### HTTP
|
|
89
|
+
|
|
90
|
+
The handler buffers the whole body, converts to a standard `Request`, and
|
|
91
|
+
returns the response body as **text** — a streaming or binary response is
|
|
92
|
+
flattened. It takes no `RunHTTPWiringOptions`, so there is no `maxBodySize` or
|
|
93
|
+
`respondWith404` here; Azure's own request limits are the bound. A thrown error
|
|
94
|
+
is logged to `console.error` and whatever the response already holds is
|
|
95
|
+
returned.
|
|
96
|
+
|
|
97
|
+
### Queue
|
|
98
|
+
|
|
99
|
+
The queue name comes from the message's own `queueName`, falling back to
|
|
100
|
+
`context.triggerMetadata.queueTrigger` and then `'unknown'` — a name that does
|
|
101
|
+
not match a wired queue means the job has no handler. `attemptsMade` is read
|
|
102
|
+
from `dequeueCount`, and `waitForCompletion` throws: Azure Storage Queues are
|
|
103
|
+
fire-and-forget. A failing job throws out of the handler, so retries and the
|
|
104
|
+
poison queue are governed by `host.json`, not by Pikku.
|
|
105
|
+
|
|
106
|
+
Producer side, `AzureQueueService(connectionString?)` falls back to
|
|
107
|
+
`AzureWebJobsStorage` and throws at construction if neither is set. Messages are
|
|
108
|
+
base64-encoded (Azure requires it), `delay` is milliseconds mapped to
|
|
109
|
+
`visibilityTimeout` in whole seconds capped at 7 days, `supportsResults` is
|
|
110
|
+
`false` and `getJob()` always throws. The queue name is remapped through
|
|
111
|
+
`AZURE_QUEUE_NAME_<SCREAMING_SNAKE>` when that variable exists, otherwise used
|
|
112
|
+
as-is.
|
|
113
|
+
|
|
114
|
+
### Timer
|
|
115
|
+
|
|
116
|
+
The timer handler runs **every** scheduled task registered in the bundle,
|
|
117
|
+
ignoring both the `Timer` argument and each task's own cron expression. Unlike
|
|
118
|
+
the Lambda equivalent it does not catch per-task failures, so the first task
|
|
119
|
+
that throws aborts the ones after it — keep one schedule per function app, or
|
|
120
|
+
guard the task bodies yourself.
|
|
121
|
+
|
|
122
|
+
### Logging
|
|
123
|
+
|
|
124
|
+
`new AzInvocationLogger(context)` forwards to the invocation context's
|
|
125
|
+
`info`/`warn`/`error`/`debug`/`trace`. `setLevel()` is a **no-op**: every level
|
|
126
|
+
is emitted and filtering has to be done in Azure's own logging configuration.
|
|
@@ -26,57 +26,99 @@ yarn add @pikku/cloudflare
|
|
|
26
26
|
|
|
27
27
|
## Worker Entry
|
|
28
28
|
|
|
29
|
+
`@pikku/cloudflare` ships the handler factories the deploy codegen emits — use
|
|
30
|
+
them rather than hand-rolling an `ExportedHandler`. Each returns a
|
|
31
|
+
`WorkerEntrypoint` class that sets services up on every invocation (cached after
|
|
32
|
+
the first) and adds an RPC-callable `runRpc(name, args)`:
|
|
33
|
+
|
|
29
34
|
```typescript
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
35
|
+
import { createCloudflareHandler } from '@pikku/cloudflare'
|
|
36
|
+
import { createConfig, createSingletonServices } from './services.js'
|
|
32
37
|
import './.pikku/pikku-bootstrap.gen.js'
|
|
33
38
|
|
|
34
|
-
export default
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
},
|
|
39
|
-
|
|
40
|
-
async fetch(request, env): Promise<Response> {
|
|
41
|
-
await setupServices(env)
|
|
42
|
-
return await runFetch(request as unknown as Request)
|
|
43
|
-
},
|
|
44
|
-
} satisfies ExportedHandler<Record<string, string>>
|
|
39
|
+
export default createCloudflareHandler(
|
|
40
|
+
{ createConfig, createSingletonServices },
|
|
41
|
+
['fetch', 'scheduled']
|
|
42
|
+
)
|
|
45
43
|
```
|
|
46
44
|
|
|
45
|
+
| Factory | For |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| `createCloudflareHandler(factories, handlerTypes)` | combined `fetch`/`queue`/`scheduled` |
|
|
48
|
+
| `createCloudflareWorkerHandler(factories)` | HTTP / agent / RPC / workflow-orchestrator units |
|
|
49
|
+
| `createCloudflareCronHandler(factories)` | cron units |
|
|
50
|
+
| `createCloudflareQueueHandler(factories)` | queue-consumer and workflow-step units |
|
|
51
|
+
| `createCloudflareMCPHandler(factories)` | MCP units (same HTTP transport) |
|
|
52
|
+
| `createCloudflareWebSocketHandler(factories)` | channel units, delegating to the DO |
|
|
53
|
+
|
|
54
|
+
`factories` is `{ createConfig, createSingletonServices, createPlatformServices? }`.
|
|
55
|
+
|
|
47
56
|
## Service Setup
|
|
48
57
|
|
|
49
|
-
Cloudflare passes env
|
|
58
|
+
Cloudflare passes env bindings per-request, so services are built from `env`
|
|
59
|
+
rather than at module load. `setupServices(env, factories)` is exported from
|
|
60
|
+
`@pikku/cloudflare` and is what the factories call:
|
|
50
61
|
|
|
51
62
|
```typescript
|
|
52
|
-
|
|
53
|
-
import { LocalVariablesService, LocalSecretService } from '@pikku/core/services'
|
|
54
|
-
import { createConfig, createSingletonServices } from './services.js'
|
|
63
|
+
import { setupServices } from '@pikku/cloudflare'
|
|
55
64
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const config = await createConfig(localVariables)
|
|
61
|
-
const localSecrets = new LocalSecretService(localVariables)
|
|
62
|
-
return await createSingletonServices(config, {
|
|
63
|
-
variables: localVariables,
|
|
64
|
-
secrets: localSecrets,
|
|
65
|
-
})
|
|
66
|
-
}
|
|
65
|
+
const services = await setupServices(env, {
|
|
66
|
+
createConfig,
|
|
67
|
+
createSingletonServices,
|
|
68
|
+
})
|
|
67
69
|
```
|
|
68
70
|
|
|
71
|
+
**Do not hand-roll this.** Beyond building `LocalVariablesService` /
|
|
72
|
+
`LocalSecretService` and caching the result, it calls `setSingletonServices()` —
|
|
73
|
+
and the core runners (`fetchData`, `runQueueJob`, `runScheduled`) resolve
|
|
74
|
+
services through that global slot, *not* through the value you were returned. A
|
|
75
|
+
setup function that only returns the services leaves every request throwing
|
|
76
|
+
"Singleton services not initialized" as a CF `1101`. It also stashes the env via
|
|
77
|
+
`setCloudflareEnv`, which `getCloudflareEnv()` reads for bindings.
|
|
78
|
+
|
|
79
|
+
## HTTP
|
|
80
|
+
|
|
81
|
+
`runFetch(request, websocketHibernationServer?, options?)`:
|
|
82
|
+
|
|
83
|
+
- A `GET` with `Upgrade: websocket` is routed to the hibernation server. Without
|
|
84
|
+
one passed in it answers **426**, so a channel worker that forgets the second
|
|
85
|
+
argument fails every upgrade while plain HTTP keeps working.
|
|
86
|
+
- `CF-Ray` becomes the traceId when present, so a Cloudflare trace and a Pikku
|
|
87
|
+
trace line up without extra wiring.
|
|
88
|
+
- `options.exposeErrors` defaults to **`false`** — error detail is withheld from
|
|
89
|
+
responses unless you opt in.
|
|
90
|
+
|
|
91
|
+
## Scheduled Tasks
|
|
92
|
+
|
|
93
|
+
`runScheduled(controller)` matches registered tasks against
|
|
94
|
+
`controller.cron` and **returns after the first match**. Two tasks sharing one
|
|
95
|
+
cron expression means only one of them ever runs — give each its own expression,
|
|
96
|
+
or invoke `runScheduledTask({ name })` per task yourself.
|
|
97
|
+
|
|
69
98
|
## WebSocket (Durable Objects)
|
|
70
99
|
|
|
100
|
+
The ready-made DO class is exported; re-export it under the binding name and
|
|
101
|
+
point the worker at it:
|
|
102
|
+
|
|
71
103
|
```typescript
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return { singletonServices }
|
|
78
|
-
}
|
|
79
|
-
}
|
|
104
|
+
export { PikkuWebSocketHibernationServer as WebSocketHibernationServer } from '@pikku/cloudflare'
|
|
105
|
+
export default createCloudflareWebSocketHandler({
|
|
106
|
+
createConfig,
|
|
107
|
+
createSingletonServices,
|
|
108
|
+
})
|
|
80
109
|
```
|
|
81
110
|
|
|
82
|
-
|
|
111
|
+
Subclass `CloudflareWebSocketHibernationServer` only when you need something
|
|
112
|
+
`getParams()` cannot express — it is abstract with one method returning
|
|
113
|
+
`{ singletonServices, createWireServices? }`. The channel store
|
|
114
|
+
(`CloudflareWebsocketStore` over the DO's own storage), the event hub and the
|
|
115
|
+
channel handler factory are all built by the base class; do not supply them.
|
|
116
|
+
|
|
117
|
+
The router looks up the DO through the **`WEBSOCKET_HIBERNATION_SERVER`**
|
|
118
|
+
binding and answers `503` naming it if the binding is missing, so declare it in
|
|
119
|
+
`wrangler.toml` under exactly that name.
|
|
120
|
+
|
|
121
|
+
A throw during `onConnect` closes the socket with `1008` and answers `403
|
|
122
|
+
Forbidden` with a deliberately generic body — an auth denial and a genuine fault
|
|
123
|
+
look identical to the client. The real reason is on the logger, so read the
|
|
124
|
+
worker logs rather than the status code.
|
|
@@ -55,15 +55,37 @@ await appServer.start()
|
|
|
55
55
|
|
|
56
56
|
**Methods:**
|
|
57
57
|
|
|
58
|
-
- `init(httpOptions
|
|
58
|
+
- `init(httpOptions?: RunHTTPWiringOptions): Promise<void>` — installs the body parsers, the cookie parser and the Pikku middleware
|
|
59
59
|
- `start(): Promise<void>` — Start listening
|
|
60
|
-
- `stop(): Promise<void>` — Graceful shutdown
|
|
61
|
-
- `enableExitOnSigInt(): Promise<void>` — SIGINT handler
|
|
60
|
+
- `stop(): Promise<void>` — Graceful shutdown; throws if the server was never started
|
|
61
|
+
- `enableExitOnSigInt(): Promise<void>` — SIGINT handler: stops the singleton services, then the server, then exits 0
|
|
62
62
|
- `enableCors(options): void` — Enable CORS
|
|
63
|
-
- `enableStaticAssets(): void` —
|
|
63
|
+
- `enableStaticAssets(): void` — serve `content.localFileUploadPath` under `content.assetUrlPrefix`
|
|
64
|
+
- `enableReaper(): void` — a `PUT /reaper/*path` upload sink for local development, path-traversal checked and bounded by `content.sizeLimit` (default `1mb`)
|
|
65
|
+
- `getHttpServer(): Server` — the underlying `http.Server`, e.g. to attach a WebSocket server; throws before `start()`
|
|
66
|
+
|
|
67
|
+
`enableStaticAssets` and `enableReaper` both throw when `content` is unset.
|
|
64
68
|
|
|
65
69
|
**Property:** `app: Express` — Direct access to Express instance for custom middleware.
|
|
66
70
|
|
|
71
|
+
### Ordering, and what `init` installs for you
|
|
72
|
+
|
|
73
|
+
The health check is registered in the **constructor**, so it answers before any
|
|
74
|
+
middleware you add and cannot be wrapped in auth. It defaults to
|
|
75
|
+
`/health-check`; override with `healthCheckPath`.
|
|
76
|
+
|
|
77
|
+
Everything else is installed by `init()`: `express.json`, `express.text` (for
|
|
78
|
+
`text/xml`), `express.urlencoded`, `cookie-parser`, then the Pikku middleware.
|
|
79
|
+
Call `enableCors` **before** `init` if you want CORS applied to Pikku's routes.
|
|
80
|
+
|
|
81
|
+
Express buffers the body before Pikku sees it, so the parser limit is the only
|
|
82
|
+
place an oversized request can actually be stopped. `httpOptions.maxBodySize`
|
|
83
|
+
therefore feeds those parser limits, with an explicit `config.limits` entry
|
|
84
|
+
(`json` / `xml` / `urlencoded`) still winning. Everything defaults to `1mb`.
|
|
85
|
+
|
|
86
|
+
`init` passes `logRoutes: true` and `loadSchemas: true` by default; your
|
|
87
|
+
`httpOptions` spread over them, so you can turn either off.
|
|
88
|
+
|
|
67
89
|
## Middleware (existing Express app)
|
|
68
90
|
|
|
69
91
|
```bash
|
|
@@ -76,11 +98,25 @@ import { pikkuExpressMiddleware } from '@pikku/express-middleware'
|
|
|
76
98
|
import './.pikku/pikku-bootstrap.gen.js'
|
|
77
99
|
|
|
78
100
|
const app = express()
|
|
101
|
+
app.use(express.json())
|
|
102
|
+
app.use(cookieParser())
|
|
79
103
|
app.use(
|
|
80
104
|
pikkuExpressMiddleware({
|
|
81
105
|
logger: singletonServices.logger,
|
|
82
106
|
logRoutes: true,
|
|
83
107
|
loadSchemas: true,
|
|
108
|
+
// plus any RunHTTPWiringOptions: maxBodySize, respondWith404, coerceDataFromSchema
|
|
84
109
|
})
|
|
85
110
|
)
|
|
86
111
|
```
|
|
112
|
+
|
|
113
|
+
Options beyond `logger` are all optional: `logRoutes` logs the wiring table once
|
|
114
|
+
at startup, `loadSchemas` compiles every schema up front, and the rest are
|
|
115
|
+
`RunHTTPWiringOptions` passed through per request.
|
|
116
|
+
|
|
117
|
+
On your own app **you** own the parser stack — the middleware reads
|
|
118
|
+
`req.body`, so a body parser and `cookie-parser` must be registered before it,
|
|
119
|
+
and `maxBodySize` alone will not stop an oversized request that your parser
|
|
120
|
+
already accepted. Unmatched requests fall through to `next()` (unless
|
|
121
|
+
`respondWith404` is set), so Pikku's routes coexist with your existing ones;
|
|
122
|
+
a streaming response is the exception and does not call `next()`.
|
|
@@ -47,10 +47,19 @@ await appServer.start()
|
|
|
47
47
|
|
|
48
48
|
**Config extends CoreConfig with:** `port`, `hostname`, `healthCheckPath?`
|
|
49
49
|
|
|
50
|
-
**Methods:** `init(httpOptions
|
|
50
|
+
**Methods:** `init(httpOptions?: RunHTTPWiringOptions)`, `start()`, `stop()`, `enableExitOnSigInt()`
|
|
51
51
|
|
|
52
52
|
**Property:** `app: FastifyInstance` — Direct access to Fastify instance.
|
|
53
53
|
|
|
54
|
+
`enableCors` exists on the class but **throws `Method not implemented.`** — unlike
|
|
55
|
+
the Express server. Register `@fastify/cors` on `app` yourself before `init()`.
|
|
56
|
+
|
|
57
|
+
Unlike the Express server, the health check is registered by `init()`, not the
|
|
58
|
+
constructor, so nothing answers before `init` runs. `init` also passes
|
|
59
|
+
`logRoutes: true` and `loadSchemas: true`, which your `httpOptions` can override.
|
|
60
|
+
The Fastify instance is constructed with no options; reach for the plugin package
|
|
61
|
+
if you need `Fastify({ … })` of your own.
|
|
62
|
+
|
|
54
63
|
## Plugin (existing Fastify app)
|
|
55
64
|
|
|
56
65
|
```bash
|
|
@@ -68,6 +77,18 @@ app.register(pikkuFastifyPlugin, {
|
|
|
68
77
|
logger: singletonServices.logger,
|
|
69
78
|
logRoutes: true,
|
|
70
79
|
loadSchemas: true,
|
|
80
|
+
// plus any RunHTTPWiringOptions: maxBodySize, respondWith404, …
|
|
71
81
|
},
|
|
72
82
|
})
|
|
73
83
|
```
|
|
84
|
+
|
|
85
|
+
Every option other than `logger` is optional, and the rest of the `pikku` object
|
|
86
|
+
is `RunHTTPWiringOptions` passed straight through.
|
|
87
|
+
|
|
88
|
+
The plugin registers a catch-all `fastify.all('/*')`, so mount it on a
|
|
89
|
+
[Fastify prefix](https://fastify.dev/docs/latest/Reference/Plugins/) if the app
|
|
90
|
+
has routes of its own to keep.
|
|
91
|
+
|
|
92
|
+
Fastify buffers the body itself, so its `bodyLimit` is where an oversized request
|
|
93
|
+
is stopped. `maxBodySize` sets it — and left unset, Fastify's stricter 1MB default
|
|
94
|
+
stands rather than being loosened to Pikku's 10MB fallback.
|
|
@@ -44,30 +44,75 @@ export const coldStart = async () => {
|
|
|
44
44
|
}
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
If the deploy codegen generated your handlers, this caching is already done for
|
|
48
|
+
you by the factories in `@pikku/lambda` — `createLambdaHandler(factories,
|
|
49
|
+
handlerTypes)`, `createLambdaWorkerHandler(factories)` and
|
|
50
|
+
`createLambdaWebSocketHandler(factories)`. They build `variables`/`secrets` from
|
|
51
|
+
`process.env`, cache the singleton services in module scope, and return the
|
|
52
|
+
named exports (`handler`, `queue`, `scheduled`, or `connect`/`disconnect`/
|
|
53
|
+
`default`) that `serverless.yml` references. Hand-written handlers are for cases
|
|
54
|
+
the codegen does not cover.
|
|
55
|
+
|
|
47
56
|
## HTTP Handler
|
|
48
57
|
|
|
58
|
+
Pick the entry point that matches the API Gateway payload version — they take
|
|
59
|
+
different event types and are not interchangeable:
|
|
60
|
+
|
|
49
61
|
```typescript
|
|
50
|
-
import type {
|
|
51
|
-
import { runFetch } from '@pikku/lambda/http'
|
|
62
|
+
import type { APIGatewayEvent } from 'aws-lambda'
|
|
63
|
+
import { runFetch } from '@pikku/lambda/http' // REST API / payload v1
|
|
52
64
|
|
|
53
|
-
export const httpRoute = async (event:
|
|
65
|
+
export const httpRoute = async (event: APIGatewayEvent) => {
|
|
54
66
|
await coldStart()
|
|
55
67
|
return await runFetch(event)
|
|
56
68
|
}
|
|
57
69
|
```
|
|
58
70
|
|
|
71
|
+
```typescript
|
|
72
|
+
import type { APIGatewayProxyEventV2 } from 'aws-lambda'
|
|
73
|
+
import { runFetchV2 } from '@pikku/lambda/http' // HTTP API / payload v2
|
|
74
|
+
|
|
75
|
+
export const httpRoute = async (event: APIGatewayProxyEventV2) => {
|
|
76
|
+
await coldStart()
|
|
77
|
+
return await runFetchV2(event)
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Both answer `OPTIONS` themselves before Pikku's wirings run, so a preflight
|
|
82
|
+
never reaches your middleware. Only `runFetchV2` echoes the request `Origin`
|
|
83
|
+
into `Access-Control-Allow-Origin`; `runFetch` sets allowed headers and methods
|
|
84
|
+
but **no origin header at all**, so v1 preflights fail in the browser unless
|
|
85
|
+
API Gateway or a CloudFront layer adds one.
|
|
86
|
+
|
|
87
|
+
They also differ on failure: `runFetchV2` logs and returns a JSON `500`, while
|
|
88
|
+
`runFetch` swallows the error and returns whatever status the response already
|
|
89
|
+
carried.
|
|
90
|
+
|
|
91
|
+
Neither takes `RunHTTPWiringOptions` — there is no `maxBodySize` or
|
|
92
|
+
`respondWith404` knob here; API Gateway's own payload limit is the bound.
|
|
93
|
+
|
|
59
94
|
## Scheduled Tasks
|
|
60
95
|
|
|
61
96
|
```typescript
|
|
62
97
|
import type { ScheduledHandler } from 'aws-lambda'
|
|
63
|
-
import {
|
|
98
|
+
import { runLambdaScheduled } from '@pikku/lambda/scheduled'
|
|
64
99
|
|
|
65
|
-
export const
|
|
100
|
+
export const scheduled: ScheduledHandler = async (event) => {
|
|
66
101
|
await coldStart()
|
|
67
|
-
await
|
|
102
|
+
await runLambdaScheduled(event)
|
|
68
103
|
}
|
|
69
104
|
```
|
|
70
105
|
|
|
106
|
+
`runLambdaScheduled` runs **every** scheduled task registered in the bundle,
|
|
107
|
+
each with its own `cron-<uuid>` traceId, and logs rather than rethrows a task
|
|
108
|
+
failure — so one bad task cannot fail the invocation or stop the others. The
|
|
109
|
+
event itself is ignored; which tasks run is decided by what the unit bundled,
|
|
110
|
+
not by which EventBridge rule fired.
|
|
111
|
+
|
|
112
|
+
Reach for `runScheduledTask({ name })` from `@pikku/core/scheduler` directly
|
|
113
|
+
only when one Lambda genuinely bundles several tasks that must fire on separate
|
|
114
|
+
schedules.
|
|
115
|
+
|
|
71
116
|
## SQS Queue Worker
|
|
72
117
|
|
|
73
118
|
```typescript
|
|
@@ -80,6 +125,25 @@ export const mySQSWorker: SQSHandler = async (event) => {
|
|
|
80
125
|
}
|
|
81
126
|
```
|
|
82
127
|
|
|
128
|
+
The worker returns an `SQSBatchResponse` listing the failed messages in
|
|
129
|
+
`batchItemFailures`, which SQS only honours when the event source mapping has
|
|
130
|
+
**`ReportBatchItemFailures`** enabled. Without it the whole batch is retried
|
|
131
|
+
when any one message fails, so successfully processed jobs run twice.
|
|
132
|
+
|
|
133
|
+
Records are processed in parallel, and the queue name is taken from the last
|
|
134
|
+
segment of `eventSourceARN` — it must match the name the worker was wired under.
|
|
135
|
+
A `QueueJobDiscardedError` counts as success (no retry); anything else is
|
|
136
|
+
reported as a failed item.
|
|
137
|
+
|
|
138
|
+
`waitForCompletion` throws on an SQS job: the transport is fire-and-forget.
|
|
139
|
+
|
|
140
|
+
On the producer side, `SQSQueueService` resolves each queue URL from the
|
|
141
|
+
constructor's `queueUrlMap` first, then from
|
|
142
|
+
`SQS_QUEUE_URL_<SCREAMING_SNAKE_NAME>`, and throws naming the missing variable
|
|
143
|
+
if neither has it. `supportsResults` is `false` and `getJob()` always throws —
|
|
144
|
+
use BullMQ or PgBoss if you need results. `delay` is milliseconds, rounded up to
|
|
145
|
+
whole seconds and capped at SQS's 900s ceiling.
|
|
146
|
+
|
|
83
147
|
## WebSocket (API Gateway v2)
|
|
84
148
|
|
|
85
149
|
```typescript
|
|
@@ -90,21 +154,37 @@ import {
|
|
|
90
154
|
LambdaEventHubService,
|
|
91
155
|
} from '@pikku/lambda/websocket'
|
|
92
156
|
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
return { statusCode: 200, body: '' }
|
|
157
|
+
const params = async (event) => {
|
|
158
|
+
const { channelStore } = await coldStart()
|
|
159
|
+
return { channelStore }
|
|
97
160
|
}
|
|
98
161
|
|
|
99
|
-
export const
|
|
100
|
-
|
|
101
|
-
return await disconnectWebsocket(event, params)
|
|
102
|
-
}
|
|
162
|
+
export const connectHandler = async (event) =>
|
|
163
|
+
await connectWebsocket(event, await params(event))
|
|
103
164
|
|
|
104
|
-
export const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
165
|
+
export const disconnectHandler = async (event) =>
|
|
166
|
+
await disconnectWebsocket(event, await params(event))
|
|
167
|
+
|
|
168
|
+
export const defaultHandler = async (event) =>
|
|
169
|
+
await processWebsocketMessage(event, await params(event))
|
|
108
170
|
```
|
|
109
171
|
|
|
110
|
-
|
|
172
|
+
All three take the same `{ channelStore }` and **return a complete
|
|
173
|
+
`APIGatewayProxyResult`** — return it. Discarding `connectWebsocket`'s result
|
|
174
|
+
and answering a hardcoded `200` accepts every connection, including the ones
|
|
175
|
+
your channel's auth rejected.
|
|
176
|
+
|
|
177
|
+
`channelStore` (e.g. `PgChannelStore`) must be a real shared store: each route
|
|
178
|
+
is a separate invocation, so nothing survives in memory between `$connect` and
|
|
179
|
+
`$default`.
|
|
180
|
+
|
|
181
|
+
`LambdaEventHubService` handles cross-connection messaging and takes
|
|
182
|
+
`(logger, event, channelStore, eventHubStore)` — the `event` is needed to derive
|
|
183
|
+
the API Gateway Management endpoint, so it is constructed per invocation, not
|
|
184
|
+
once at cold start. It also needs an `EventHubStore` alongside the channel
|
|
185
|
+
store.
|
|
186
|
+
|
|
187
|
+
Two behaviours to design around: **binary payloads throw** (`Binary data is not
|
|
188
|
+
supported on serverless lambdas`), and any `PostToConnection` failure removes
|
|
189
|
+
the connection from the channel store — a transient error drops a live client,
|
|
190
|
+
not just a stale one.
|
|
@@ -38,6 +38,17 @@ export const PATCH = pikkuAPIRequest
|
|
|
38
38
|
export const DELETE = pikkuAPIRequest
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
`pikkuAPIRequest` strips a leading `/api` from the pathname before routing, so
|
|
42
|
+
wirings are declared as `/todos`, not `/api/todos`, even though the route file
|
|
43
|
+
lives under `app/api`. Turn that off with `removeAPIPrefix(false)` from the same
|
|
44
|
+
generated file if your wirings really do carry the prefix.
|
|
45
|
+
|
|
46
|
+
It takes `(req, context)` to match Next's handler signature but ignores the
|
|
47
|
+
context — Pikku routes from the URL, so the catch-all segment name is yours to
|
|
48
|
+
choose. It also passes no `RunHTTPWiringOptions`: to set `maxBodySize` or
|
|
49
|
+
`respondWith404` you need your own handler over `new PikkuNextJS(...)` calling
|
|
50
|
+
`apiRequest(req, options)`.
|
|
51
|
+
|
|
41
52
|
## Server-Side Data Fetching
|
|
42
53
|
|
|
43
54
|
Use the generated `pikku()` helper in Server Components or Server Actions:
|
|
@@ -45,7 +56,7 @@ Use the generated `pikku()` helper in Server Components or Server Actions:
|
|
|
45
56
|
```typescript
|
|
46
57
|
import { pikku } from '@/pikku-nextjs.gen.js'
|
|
47
58
|
|
|
48
|
-
const { get, post, del, rpc, staticGet, staticPost, staticRPC } = pikku()
|
|
59
|
+
const { get, post, patch, del, rpc, staticGet, staticPost, staticRPC } = pikku()
|
|
49
60
|
|
|
50
61
|
// Dynamic (reads headers/cookies — requires request context)
|
|
51
62
|
const todos = await get('/todos')
|
|
@@ -60,8 +71,19 @@ const result = await rpc('calculateTax', { amount: 100, region: 'US' })
|
|
|
60
71
|
|
|
61
72
|
**Dynamic vs Static:**
|
|
62
73
|
|
|
63
|
-
- `get`, `post`, `del`, `rpc` —
|
|
64
|
-
|
|
74
|
+
- `get`, `post`, `patch`, `del`, `rpc` — read `next/headers` cookies and headers,
|
|
75
|
+
so they force the component dynamic
|
|
76
|
+
- `staticGet`, `staticPost`, `staticRPC` — no request context, safe for
|
|
77
|
+
precompile/ISR
|
|
78
|
+
|
|
79
|
+
The static variants pass `skipUserSession: true`, so a wiring that expects a
|
|
80
|
+
session sees none. That is the real difference — not just where they can run.
|
|
81
|
+
There is no `staticPatch` or `staticDel`; a mutation at build time is not a
|
|
82
|
+
thing the generated client offers.
|
|
83
|
+
|
|
84
|
+
Both paths run with `bubbleErrors: true`, so a failing wiring **throws** in your
|
|
85
|
+
Server Component rather than resolving to an error status. Wrap the call, or let
|
|
86
|
+
the Next.js error boundary take it.
|
|
65
87
|
|
|
66
88
|
## How It Works
|
|
67
89
|
|
|
@@ -73,6 +95,28 @@ import { PikkuNextJS } from '@pikku/next'
|
|
|
73
95
|
const pikku = new PikkuNextJS(createConfig, createSingletonServices)
|
|
74
96
|
```
|
|
75
97
|
|
|
76
|
-
**Constructor:** `new PikkuNextJS(createConfig
|
|
98
|
+
**Constructor:** `new PikkuNextJS(createConfig | undefined, createSingletonServices)`
|
|
99
|
+
|
|
100
|
+
Both arguments are positional and `createConfig` is only optional in the sense
|
|
101
|
+
that passing `undefined` substitutes an empty config —
|
|
102
|
+
`createSingletonServices` is required.
|
|
103
|
+
|
|
104
|
+
Initialization is memoized on a promise, so concurrent first requests share one
|
|
105
|
+
setup; a failed setup clears the promise, so the next request retries rather
|
|
106
|
+
than caching the failure forever.
|
|
107
|
+
|
|
108
|
+
The generated `pikku-nextjs.gen.ts` wraps this with full type safety from your
|
|
109
|
+
route definitions.
|
|
110
|
+
|
|
111
|
+
## Related exports
|
|
77
112
|
|
|
78
|
-
|
|
113
|
+
- **`PikkuNextJSWorkerRPC({ fetcher })`** — same surface as `PikkuNextJS`, but
|
|
114
|
+
every call is dispatched through a `Fetcher` (a Cloudflare service binding, a
|
|
115
|
+
local HTTP client, a fabric dispatcher) instead of loading function code
|
|
116
|
+
in-process. Use it to keep functions out of the SSR bundle. A non-2xx
|
|
117
|
+
response throws with the status and body text.
|
|
118
|
+
- **`toNextJsAuthHandler(auth)`** — wraps a better-auth instance or handler
|
|
119
|
+
function for an auth route. The three-argument form
|
|
120
|
+
`(pikkuAuthFactory, createConfig, createSingletonServices)` resolves a Pikku
|
|
121
|
+
better-auth factory lazily; it throws if you pass `createConfig` without
|
|
122
|
+
`createSingletonServices`. `nextCookies` is re-exported alongside it.
|