@pikku/skills 0.12.4 → 0.12.6
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 +56 -29
- 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-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 +108 -75
- 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 +35 -1
- 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
|
@@ -36,19 +36,23 @@ See `pikku-concepts` for the core mental model.
|
|
|
36
36
|
|
|
37
37
|
### `wireCLI(config)`
|
|
38
38
|
|
|
39
|
+
All three factories come from `#pikku` (the generated types re-export
|
|
40
|
+
`cli/pikku-cli-types.gen.js`). Importing them from `@pikku/core/cli` compiles but
|
|
41
|
+
loses your project's service and middleware types.
|
|
42
|
+
|
|
39
43
|
```typescript
|
|
40
|
-
import { wireCLI } from '
|
|
44
|
+
import { wireCLI } from '#pikku'
|
|
41
45
|
|
|
42
46
|
wireCLI({
|
|
43
47
|
program: string, // Program name (e.g. 'todos')
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
short?: string, // Single char alias (e.g. 'v')
|
|
48
|
-
default?: any,
|
|
49
|
-
}
|
|
50
|
-
},
|
|
48
|
+
description?: string,
|
|
49
|
+
summary?: string,
|
|
50
|
+
options?: CLIOptions, // Global options — see below
|
|
51
51
|
render?: PikkuCLIRender, // Default renderer for all commands
|
|
52
|
+
middleware?: PikkuMiddleware[],
|
|
53
|
+
tags?: string[], // Targets tag middleware
|
|
54
|
+
errors?: string[],
|
|
55
|
+
auth?: boolean, // Only affects the websocket backend, not local runs
|
|
52
56
|
commands: {
|
|
53
57
|
[name: string]: PikkuCLICommand | {
|
|
54
58
|
description: string,
|
|
@@ -65,24 +69,49 @@ import { pikkuCLICommand } from '#pikku'
|
|
|
65
69
|
|
|
66
70
|
pikkuCLICommand({
|
|
67
71
|
parameters?: string, // Positional args (e.g. '<text>', '<username> <email>')
|
|
68
|
-
func
|
|
72
|
+
func?: PikkuFunc, // Business logic function — omit on a pure command group
|
|
73
|
+
title?: string,
|
|
69
74
|
description?: string,
|
|
70
75
|
render?: PikkuCLIRender, // Custom output renderer
|
|
71
|
-
options?:
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
},
|
|
76
|
+
options?: CLIOptions,
|
|
77
|
+
subcommands?: { [name: string]: PikkuCLICommand }, // nests to any depth
|
|
78
|
+
middleware?: PikkuMiddleware[],
|
|
79
|
+
permissions?: PermissionGroup,
|
|
80
|
+
auth?: boolean,
|
|
81
|
+
isDefault?: boolean, // Runs when the group is invoked with no subcommand
|
|
79
82
|
})
|
|
80
83
|
```
|
|
81
84
|
|
|
85
|
+
`parameters` is checked against the func's input at compile time — a name that is
|
|
86
|
+
not a key of the input makes the type `never`, so a typo'd positional fails to
|
|
87
|
+
build rather than arriving as `undefined`.
|
|
88
|
+
|
|
89
|
+
### Options
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
{
|
|
93
|
+
description: string,
|
|
94
|
+
short?: string, // Single char alias (e.g. 'v')
|
|
95
|
+
default?: any,
|
|
96
|
+
choices?: any[], // Restrict to these values
|
|
97
|
+
array?: boolean, // Collect every value up to the next flag
|
|
98
|
+
required?: boolean,
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
How the parser reads them, which is worth knowing before you name one:
|
|
103
|
+
|
|
104
|
+
- **Flag names are camel-cased**, so `--api-url` and `--apiUrl` both fill `apiUrl`.
|
|
105
|
+
- **`--no-x` negation only works when `x` has a boolean `default`.** Without one,
|
|
106
|
+
`--no-x` parses as an option literally named `noX` — which is why boolean flags
|
|
107
|
+
should always declare their default.
|
|
108
|
+
- Short flags cluster (`-abc`), and only the last in a cluster may take a value.
|
|
109
|
+
- An unknown `--flag` warns rather than throwing.
|
|
110
|
+
|
|
82
111
|
### `pikkuCLIRender(fn)`
|
|
83
112
|
|
|
84
113
|
```typescript
|
|
85
|
-
import { pikkuCLIRender } from '
|
|
114
|
+
import { pikkuCLIRender } from '#pikku'
|
|
86
115
|
|
|
87
116
|
const renderer = pikkuCLIRender<OutputType>((services, data) => {
|
|
88
117
|
// Format and print output to terminal
|
|
@@ -90,6 +119,15 @@ const renderer = pikkuCLIRender<OutputType>((services, data) => {
|
|
|
90
119
|
})
|
|
91
120
|
```
|
|
92
121
|
|
|
122
|
+
### Wire object (`wire.cli`)
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
wire.cli.program // program name
|
|
126
|
+
wire.cli.command // string[] — the resolved command path
|
|
127
|
+
wire.cli.data // all positionals and options, merged
|
|
128
|
+
wire.cli.channel // the channel when served remotely (see below)
|
|
129
|
+
```
|
|
130
|
+
|
|
93
131
|
## Usage Patterns
|
|
94
132
|
|
|
95
133
|
### Basic Commands
|
|
@@ -193,6 +231,17 @@ wireCLI({
|
|
|
193
231
|
|
|
194
232
|
The func's input is the positional `parameters` plus `options`, merged (e.g. `parameters: '<username> <email>'` + an `admin` option → func input `{ username, email, admin }`).
|
|
195
233
|
|
|
234
|
+
A renderer's full signature is `(services, data, session?)`. It returns nothing —
|
|
235
|
+
printing is its job.
|
|
236
|
+
|
|
237
|
+
### Running the program over a websocket
|
|
238
|
+
|
|
239
|
+
Codegen emits a `<program>-channel.gen.ts` beside your wiring: a `wireChannel`
|
|
240
|
+
that serves the same commands remotely, so a local binary and a hosted session
|
|
241
|
+
run identical code. `auth` on `wireCLI` guards **that channel only** — a locally
|
|
242
|
+
executed CLI has no connection to authenticate, so it is not a way to require a
|
|
243
|
+
session for local runs. Don't hand-write or edit the generated channel file.
|
|
244
|
+
|
|
196
245
|
## Complete Example
|
|
197
246
|
|
|
198
247
|
For a full functions + renderers + nested-subcommand wiring walkthrough, see `references/complete-example.md`.
|
|
@@ -32,6 +32,8 @@ export const deleteUser = pikkuFunc({
|
|
|
32
32
|
})
|
|
33
33
|
|
|
34
34
|
// wirings/cli.wiring.ts
|
|
35
|
+
import { wireCLI, pikkuCLICommand, pikkuCLIRender } from '#pikku'
|
|
36
|
+
|
|
35
37
|
const userRenderer = pikkuCLIRender<{ user: User }>((_services, { user }) => {
|
|
36
38
|
console.log(`Created user: ${user.username} (${user.email}) [${user.role}]`)
|
|
37
39
|
})
|
|
@@ -28,7 +28,8 @@ Pikku is a TypeScript framework that separates business logic from transport mec
|
|
|
28
28
|
For deep-dive on each topic, see the dedicated skills:
|
|
29
29
|
|
|
30
30
|
- **Wiring**: `pikku-http`, `pikku-websocket`, `pikku-rpc`, `pikku-mcp`, `pikku-queue`, `pikku-cron`, `pikku-trigger`, `pikku-cli`, `pikku-ai-agent`, `pikku-workflow`
|
|
31
|
-
- **
|
|
31
|
+
- **Authorization**: `pikku-security` (authentication/sessions), `pikku-permissions` (permission checks, scopes), `pikku-middleware` (global/tag/route middleware)
|
|
32
|
+
- **Infrastructure**: `pikku-services`, `pikku-config`
|
|
32
33
|
- **Project introspection**: `pikku-info`
|
|
33
34
|
|
|
34
35
|
## Core Mental Model
|
|
@@ -58,7 +59,7 @@ The function never imports Express, never reads `req.body`, never touches `ws.se
|
|
|
58
59
|
|
|
59
60
|
## Concept Mapping: Generic Backend → Pikku
|
|
60
61
|
|
|
61
|
-
Controllers/routes → `pikkuFunc`;
|
|
62
|
+
Controllers/routes → `pikkuFunc`; auth/sessions → `pikku-security`; authorization checks → `pikku-permissions`; request interception → `pikku-middleware`; DI → `pikku-services`; transports (HTTP/WS/queue/cron) → their `wire*` + skill. For the full Generic Backend → Pikku mapping table (with side-by-side code examples), read `references/concept-mapping.md`.
|
|
62
63
|
|
|
63
64
|
## Functions
|
|
64
65
|
|
|
@@ -95,22 +96,53 @@ Services can be destructured inline in the `func` signature (e.g. `async ({ logg
|
|
|
95
96
|
|
|
96
97
|
```typescript
|
|
97
98
|
pikkuFunc({
|
|
99
|
+
// Identity and documentation
|
|
98
100
|
title?: string, // Human-readable name
|
|
99
101
|
description?: string, // What the function does
|
|
100
|
-
version?: number, // Contract version (see pikku-
|
|
102
|
+
version?: number, // Contract version (see pikku-versioning)
|
|
103
|
+
override?: string, // Logical name override, so several exports share a versioned base
|
|
101
104
|
tags?: string[], // For grouping and middleware targeting
|
|
105
|
+
|
|
106
|
+
// Contract
|
|
107
|
+
input?: ZodSchema, // Input validation schema
|
|
108
|
+
output?: ZodSchema, // Output validation schema
|
|
109
|
+
errors?: Array<typeof PikkuError>, // Errors this function may throw
|
|
110
|
+
|
|
111
|
+
// Reachability
|
|
102
112
|
expose?: boolean, // Allow external RPC calls (see pikku-rpc)
|
|
103
113
|
remote?: boolean, // Allow remote RPC calls
|
|
104
114
|
mcp?: boolean, // Expose as MCP tool (see pikku-mcp)
|
|
115
|
+
readonly?: boolean, // Declares the function performs no writes
|
|
116
|
+
deploy?: 'serverless' | 'server' | 'auto',
|
|
117
|
+
|
|
118
|
+
// Authorization — see pikku-permissions
|
|
105
119
|
auth?: boolean, // Override default auth requirement
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
middleware?: PikkuMiddleware[], // See pikku-
|
|
120
|
+
scopes?: ScopeId[], // AND-ed, checked before permissions; session required
|
|
121
|
+
permissions?: PermissionGroup, // OR-ed pool
|
|
122
|
+
permissionsInBody?: boolean, // Last resort; needs allow.permissionsInBody in config
|
|
123
|
+
middleware?: PikkuMiddleware[], // See pikku-middleware
|
|
124
|
+
|
|
125
|
+
// Agent tooling — see pikku-ai-agent
|
|
126
|
+
approvalRequired?: boolean,
|
|
127
|
+
approvalDescription?: (services, data) => Promise<string>,
|
|
128
|
+
|
|
129
|
+
// Workflow step behavior — see pikku-workflow
|
|
130
|
+
workflowQueued?: boolean, // Dispatch via queue instead of inline
|
|
131
|
+
workflowRetries?: number,
|
|
132
|
+
workflowTimeout?: string, // e.g. '30s', '5m'
|
|
133
|
+
|
|
134
|
+
audit?: boolean | { durability?: 'best-effort' | 'transactional' },
|
|
135
|
+
|
|
110
136
|
func: async (services, data, wire) => { ... },
|
|
111
137
|
})
|
|
112
138
|
```
|
|
113
139
|
|
|
140
|
+
`scopes` is the one option `pikkuSessionlessFunc` does not accept, and the
|
|
141
|
+
omission is deliberate: scopes are AND-ed and fail closed, so an anonymous
|
|
142
|
+
caller holds none and satisfies none — a sessionless function with scopes would
|
|
143
|
+
reject every caller it exists to serve. Gate those with `permissions`, which
|
|
144
|
+
receive the optional session and may pass anonymous.
|
|
145
|
+
|
|
114
146
|
**Generics XOR `input`/`output` — never both.** A function's data and return
|
|
115
147
|
types come from *one* source: either the `input`/`output` schemas (preferred —
|
|
116
148
|
they double as runtime validation and OpenAPI) or type generics
|
|
@@ -145,7 +177,35 @@ Schemas serve triple duty: runtime validation, TypeScript types, and OpenAPI doc
|
|
|
145
177
|
|
|
146
178
|
## Server Bootstrap
|
|
147
179
|
|
|
148
|
-
|
|
180
|
+
There are two ways to start a Pikku app. Pick based on whether you need to own the HTTP server.
|
|
181
|
+
|
|
182
|
+
**1. Let Pikku own the server (preferred when you don't need a specific runtime)**
|
|
183
|
+
|
|
184
|
+
`pikku dev` and `pikku serve` create the config and singleton services, start the server, and shut it down cleanly. You write no bootstrap code at all — startup and shutdown work goes in lifecycle hooks:
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
// src/lifecycle.ts
|
|
188
|
+
import { pikkuServerLifecycle } from '@pikku/core'
|
|
189
|
+
import type { SingletonServices } from '../types/application-types.js'
|
|
190
|
+
|
|
191
|
+
export const lifecycle = pikkuServerLifecycle<SingletonServices>({
|
|
192
|
+
beforeStart: async ({ kysely }) => {
|
|
193
|
+
await runMigrations(kysely)
|
|
194
|
+
},
|
|
195
|
+
afterStart: async ({ logger }) => {
|
|
196
|
+
logger.info('accepting traffic')
|
|
197
|
+
},
|
|
198
|
+
beforeStop: async ({ queueService }) => {
|
|
199
|
+
await queueService.drain()
|
|
200
|
+
},
|
|
201
|
+
})
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Export exactly one `pikkuServerLifecycle` from anywhere in `srcDirectories` — the inspector finds it by the wrapper call. Every hook is optional and receives the already-created singleton services. See pikku-services for the ordering and the `afterStop` caveat.
|
|
205
|
+
|
|
206
|
+
**2. Bootstrap it yourself (required for a specific runtime)**
|
|
207
|
+
|
|
208
|
+
Express, Fastify, uWS, Lambda, Cloudflare and Next.js need their own entrypoint, because Pikku is embedded in a server you own:
|
|
149
209
|
|
|
150
210
|
```typescript
|
|
151
211
|
import '../../functions/.pikku/pikku-bootstrap.gen.js' // Generated — registers all wirings
|
|
@@ -168,6 +228,10 @@ await server.init()
|
|
|
168
228
|
await server.start()
|
|
169
229
|
```
|
|
170
230
|
|
|
231
|
+
**Lifecycle hooks do not run on this path** — only `pikku dev` and `pikku serve` invoke them. Do your startup work directly in the entrypoint instead.
|
|
232
|
+
|
|
233
|
+
`pikku workspace validate` warns when a project starts a server by hand *and* depends on no runtime adapter, since that combination means path 1 was available and unused. Silence it with `"lint": { "customServerBootstrap": "off" }` in `pikku.config.json`.
|
|
234
|
+
|
|
171
235
|
## Code Generation
|
|
172
236
|
|
|
173
237
|
Run `npx pikku all` to generate:
|
|
@@ -175,7 +239,7 @@ Run `npx pikku all` to generate:
|
|
|
175
239
|
- `pikku-types.gen.ts` — Typed function factories and wiring functions
|
|
176
240
|
- `pikku-fetch.gen.ts` — Type-safe HTTP client
|
|
177
241
|
- `pikku-websocket.gen.ts` — Type-safe WebSocket client
|
|
178
|
-
- `pikku-bootstrap.gen.
|
|
242
|
+
- `pikku-bootstrap.gen.ts` — Runtime initialization (auto-imports all wirings)
|
|
179
243
|
- `pikku-services.gen.ts` — Service factory types
|
|
180
244
|
|
|
181
245
|
Config lives in `pikku.config.json`:
|
|
@@ -203,12 +267,13 @@ src/
|
|
|
203
267
|
│ └── queue.wiring.ts
|
|
204
268
|
├── schemas.ts # Zod/Valibot schemas
|
|
205
269
|
├── services.ts # Service factories (see pikku-services)
|
|
270
|
+
├── lifecycle.ts # Server lifecycle hooks (pikku dev/serve only)
|
|
206
271
|
├── middleware.ts # Middleware definitions (see pikku-security)
|
|
207
272
|
├── permissions.ts # Permission definitions (see pikku-security)
|
|
208
273
|
└── .pikku/ # Generated (gitignored)
|
|
209
274
|
├── pikku-types.gen.ts
|
|
210
275
|
├── pikku-fetch.gen.ts
|
|
211
|
-
└── pikku-bootstrap.gen.
|
|
276
|
+
└── pikku-bootstrap.gen.ts
|
|
212
277
|
```
|
|
213
278
|
|
|
214
279
|
## Environment Variables
|
|
@@ -4,9 +4,10 @@ description: >-
|
|
|
4
4
|
Use when managing secrets, environment variables, config, or OAuth2 credentials in a Pikku app.
|
|
5
5
|
Covers defineSecret, defineVariable, defineCredential, and typed config access. TRIGGER when:
|
|
6
6
|
code uses defineSecret/defineVariable/defineCredential, user asks about env vars, secrets,
|
|
7
|
-
config, OAuth2, or "how do I access environment
|
|
8
|
-
API versioning/breaking changes (use
|
|
9
|
-
|
|
7
|
+
config, OAuth2, SecretValue/.reveal(), SecretCoercionError, or "how do I access environment
|
|
8
|
+
variables". DO NOT TRIGGER when: user asks about API versioning/breaking changes (use
|
|
9
|
+
pikku-versioning), service factories (use pikku-services), middleware (use pikku-middleware), or
|
|
10
|
+
auth strategies and sessions (use pikku-security).
|
|
10
11
|
installGroups: [core]
|
|
11
12
|
---
|
|
12
13
|
|
|
@@ -64,10 +65,17 @@ or any wire** — it is removed from their services type and throws at runtime i
|
|
|
64
65
|
reached through a cast. Read it where you wire the app and hand the value to a
|
|
65
66
|
service:
|
|
66
67
|
|
|
68
|
+
`getSecret` returns a `SecretValue<T>`, not the bare value. It is nominal — not
|
|
69
|
+
assignable to `string`, so every concretely-typed sink rejects it — it serializes
|
|
70
|
+
to `[secret]` in logs and audits, and coercing it to a string (a template
|
|
71
|
+
literal, a concatenation) throws `SecretCoercionError`, because that is always a
|
|
72
|
+
leak. `.reveal()` is the one way out, which makes every disclosure deliberate and
|
|
73
|
+
greppable. Call it at the point the value reaches the thing that needs it:
|
|
74
|
+
|
|
67
75
|
```typescript
|
|
68
76
|
// services.ts — allowed
|
|
69
77
|
const createSingletonServices = pikkuServices(async (config, { secrets }) => ({
|
|
70
|
-
stripe: new StripeService(await secrets.getSecret('STRIPE_CONFIG')),
|
|
78
|
+
stripe: new StripeService((await secrets.getSecret('STRIPE_CONFIG')).reveal()),
|
|
71
79
|
}))
|
|
72
80
|
|
|
73
81
|
// functions/*.ts — ask the service, never the secret store
|
|
@@ -113,7 +121,7 @@ defineSecret({
|
|
|
113
121
|
})
|
|
114
122
|
|
|
115
123
|
// In your services factory — fully typed
|
|
116
|
-
const config = await secrets.getSecret('STRIPE_CONFIG')
|
|
124
|
+
const config = (await secrets.getSecret('STRIPE_CONFIG')).reveal()
|
|
117
125
|
// config.apiKey → string (autocompleted)
|
|
118
126
|
// config.webhookSecret → string (autocompleted)
|
|
119
127
|
|
|
@@ -178,17 +186,34 @@ defineCredential({
|
|
|
178
186
|
},
|
|
179
187
|
})
|
|
180
188
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
189
|
+
### Reading a Credential
|
|
190
|
+
|
|
191
|
+
A declared credential is resolved per invocation through `wire.getCredential(name)`,
|
|
192
|
+
so the natural place to read it is a wire service factory: build the client there
|
|
193
|
+
once and let functions ask the client, the same way they ask a service for a
|
|
194
|
+
secret-derived value. Tokens refresh automatically, so what arrives is already
|
|
195
|
+
valid.
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
export const createWireServices = pikkuWireServices(async (_services, wire) => {
|
|
199
|
+
const cred = await wire.getCredential?.<{ accessToken: string }>('slack')
|
|
200
|
+
if (!cred?.accessToken) {
|
|
201
|
+
// Tells the caller which credential to connect, and where.
|
|
202
|
+
throw new MissingCredentialError('slack', 'oauth2', '/credentials/slack/connect')
|
|
187
203
|
}
|
|
188
|
-
)
|
|
189
|
-
|
|
204
|
+
return { slack: new SlackClient(cred.accessToken) }
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
// functions/*.ts — ask the client, never the credential store
|
|
208
|
+
export const postMessage = pikkuFunc({
|
|
209
|
+
func: async ({ slack }, { channel, text }) => slack.postMessage(channel, text),
|
|
210
|
+
})
|
|
190
211
|
```
|
|
191
212
|
|
|
213
|
+
A `wire` credential resolves per user, so an unconnected user hits
|
|
214
|
+
`MissingCredentialError` rather than silently acting as someone else; a
|
|
215
|
+
`singleton` credential is platform-level and identical for every caller.
|
|
216
|
+
|
|
192
217
|
## Key Rule
|
|
193
218
|
|
|
194
219
|
**Never use `process.env` inside Pikku functions.** Use the `variables` or `secrets` service:
|
|
@@ -201,7 +226,24 @@ const apiKey = process.env.API_KEY
|
|
|
201
226
|
const apiKey = services.variables.get('API_KEY')
|
|
202
227
|
```
|
|
203
228
|
|
|
204
|
-
`process.env` belongs only in server bootstrap code (`start.ts`).
|
|
229
|
+
`process.env` belongs only in server bootstrap code (`start.ts`). Under `pikku dev` / `pikku serve` there is no `start.ts` — startup work goes in a `pikkuServerLifecycle` export, and the hooks receive the singleton services, so read configuration through `variables` there too (see pikku-services).
|
|
230
|
+
|
|
231
|
+
### Lint rules
|
|
232
|
+
|
|
233
|
+
`pikku.config.json` can set the severity of individual checks:
|
|
234
|
+
|
|
235
|
+
```json
|
|
236
|
+
{
|
|
237
|
+
"lint": {
|
|
238
|
+
"servicesNotDestructured": "error",
|
|
239
|
+
"wiresNotDestructured": "error",
|
|
240
|
+
"functionDynamicImport": "warn",
|
|
241
|
+
"customServerBootstrap": "warn"
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
`customServerBootstrap` is the one evaluated by `pikku workspace validate` rather than codegen: it warns when the root `start`/`dev` script boots a server without `pikku dev` / `pikku serve` and no runtime adapter is installed. Set it to `"off"` to keep a hand-rolled entrypoint, or `"error"` to enforce the hooks.
|
|
205
247
|
|
|
206
248
|
## Complete Example
|
|
207
249
|
|
|
@@ -6,6 +6,7 @@ description: >-
|
|
|
6
6
|
when: code uses wireScheduler, user asks about cron, scheduled tasks, recurring jobs, or "run
|
|
7
7
|
every X minutes/hours". DO NOT TRIGGER when: user asks about background jobs with retries (use
|
|
8
8
|
pikku-queue) or event-driven triggers (use pikku-trigger).
|
|
9
|
+
installGroups: [core]
|
|
9
10
|
---
|
|
10
11
|
|
|
11
12
|
# Pikku Cron/Scheduler Wiring
|
|
@@ -42,6 +43,7 @@ wireScheduler({
|
|
|
42
43
|
name: string, // Unique scheduler name
|
|
43
44
|
schedule: string, // Cron expression
|
|
44
45
|
func: PikkuVoidFunc, // Must be pikkuVoidFunc (no input/output)
|
|
46
|
+
tags?: string[], // Targets tag middleware — see pikku-middleware
|
|
45
47
|
middleware?: PikkuMiddleware[],
|
|
46
48
|
})
|
|
47
49
|
```
|
|
@@ -53,10 +55,17 @@ Inside scheduled functions:
|
|
|
53
55
|
```typescript
|
|
54
56
|
wire.scheduledTask.name // Scheduler name
|
|
55
57
|
wire.scheduledTask.schedule // Cron expression string
|
|
56
|
-
wire.scheduledTask.executionTime //
|
|
57
|
-
wire.scheduledTask.skip(reason) //
|
|
58
|
+
wire.scheduledTask.executionTime // Date this execution was triggered
|
|
59
|
+
wire.scheduledTask.skip(reason?) // Abort this execution — THROWS, never returns
|
|
58
60
|
```
|
|
59
61
|
|
|
62
|
+
**`skip()` aborts by throwing.** It reads like an early return but it is not:
|
|
63
|
+
nothing after the call runs, so there is no need to `return` afterwards. The
|
|
64
|
+
consequence that bites is in middleware — a `try/catch` around `await next()`
|
|
65
|
+
will catch a skip and report it as a failure. If your middleware distinguishes
|
|
66
|
+
success from failure, let the skip pass through rather than logging it as an
|
|
67
|
+
error.
|
|
68
|
+
|
|
60
69
|
### Cron Expression Reference
|
|
61
70
|
|
|
62
71
|
```
|
|
@@ -113,8 +122,7 @@ const weeklyCleanup = pikkuVoidFunc({
|
|
|
113
122
|
|
|
114
123
|
const staleCount = await db.countStaleTodos()
|
|
115
124
|
if (staleCount === 0) {
|
|
116
|
-
wire.scheduledTask.skip('No stale todos found')
|
|
117
|
-
return
|
|
125
|
+
wire.scheduledTask.skip('No stale todos found') // throws — nothing below runs
|
|
118
126
|
}
|
|
119
127
|
|
|
120
128
|
await db.deleteCompletedTodos({ olderThan: '30d' })
|
|
@@ -178,8 +186,7 @@ export const cleanupExpired = pikkuVoidFunc({
|
|
|
178
186
|
func: async ({ db, logger }, _input, wire) => {
|
|
179
187
|
const count = await db.countExpiredSessions()
|
|
180
188
|
if (count === 0) {
|
|
181
|
-
wire.scheduledTask.skip('No expired sessions')
|
|
182
|
-
return
|
|
189
|
+
wire.scheduledTask.skip('No expired sessions') // throws — nothing below runs
|
|
183
190
|
}
|
|
184
191
|
await db.deleteExpiredSessions()
|
|
185
192
|
logger.info(`Cleaned ${count} expired sessions`)
|
|
@@ -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.
|