@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
|
@@ -41,10 +41,30 @@ const schema = new CFWorkerSchemaService(logger: Logger)
|
|
|
41
41
|
|
|
42
42
|
**Methods:**
|
|
43
43
|
|
|
44
|
-
- `compileSchema(
|
|
44
|
+
- `compileSchema(name: string, schema: any): void` — Compile and register a JSON schema under `name`
|
|
45
45
|
- `validateSchema(schemaName: string, json: any): void` — Validate data against a compiled schema (throws on failure)
|
|
46
46
|
- `getSchemaNames(): Set<string>` — Get all registered schema names
|
|
47
|
-
- `getSchemaKeys(schemaName: string): string[]` —
|
|
47
|
+
- `getSchemaKeys(schemaName: string): string[]` — Top-level property keys, or `[]` if the schema has no `properties`
|
|
48
|
+
|
|
49
|
+
### Where it differs from AJV
|
|
50
|
+
|
|
51
|
+
These two are not drop-in equivalents, and the differences are the kind that
|
|
52
|
+
surface as behaviour changes rather than compile errors:
|
|
53
|
+
|
|
54
|
+
- **No `useDefaults`.** AJV fills schema defaults into the validated object in
|
|
55
|
+
place; this validator does not. A field you relied on being defaulted arrives
|
|
56
|
+
`undefined` on Workers.
|
|
57
|
+
- **It re-compiles when the schema value changes.** AJV caches by name forever;
|
|
58
|
+
here a `compileSchema` with a different value for the same name replaces the
|
|
59
|
+
validator, which is what lets a dev hot-reload pick up regenerated schemas.
|
|
60
|
+
- **Each validator gets a deep clone of the schema** (`@cfworker/json-schema`
|
|
61
|
+
mutates what it is given, which throws on a frozen generated object).
|
|
62
|
+
- A compile failure throws `Error('Failed to compile schema: <name>')` with the
|
|
63
|
+
underlying cause swallowed — check the schema by hand when you see it.
|
|
64
|
+
|
|
65
|
+
A failed validation throws `UnprocessableContentError` (422) with the validator
|
|
66
|
+
errors joined; a *missing* schema throws a bare string, `Missing validator for
|
|
67
|
+
<name>`, not an `Error`.
|
|
48
68
|
|
|
49
69
|
## Usage Patterns
|
|
50
70
|
|
|
@@ -23,6 +23,10 @@ For **permissions** (pikkuPermission, pikkuAuth, per-function authorization) see
|
|
|
23
23
|
|
|
24
24
|
## Session Management
|
|
25
25
|
|
|
26
|
+
`session`, `setSession` and `clearSession` live on the **wire** — the function's
|
|
27
|
+
third argument — not on services. `setSession`/`clearSession` may be async
|
|
28
|
+
(cookie and session-store backends write on the way out), so await them.
|
|
29
|
+
|
|
26
30
|
```typescript
|
|
27
31
|
// Read session in pikkuFunc (session guaranteed to exist)
|
|
28
32
|
const getProfile = pikkuFunc({
|
|
@@ -36,7 +40,7 @@ const login = pikkuFunc({
|
|
|
36
40
|
auth: false,
|
|
37
41
|
func: async ({ jwt, db }, { email, password }, { setSession }) => {
|
|
38
42
|
const user = await db.verifyCredentials(email, password)
|
|
39
|
-
setSession({ userId: user.id })
|
|
43
|
+
await setSession({ userId: user.id })
|
|
40
44
|
return { token: jwt.sign({ userId: user.id }) }
|
|
41
45
|
},
|
|
42
46
|
})
|
|
@@ -44,28 +48,31 @@ const login = pikkuFunc({
|
|
|
44
48
|
// Clear session (logout)
|
|
45
49
|
const logout = pikkuFunc({
|
|
46
50
|
func: async ({}, _data, { clearSession }) => {
|
|
47
|
-
clearSession()
|
|
51
|
+
await clearSession()
|
|
48
52
|
},
|
|
49
53
|
})
|
|
50
54
|
```
|
|
51
55
|
|
|
56
|
+
`login` is `auth: false` because the caller has no session yet — a `pikkuFunc`
|
|
57
|
+
with the default `auth` would be rejected before its body ever ran.
|
|
58
|
+
|
|
52
59
|
## Built-in Auth Strategies
|
|
53
60
|
|
|
54
61
|
Apply these via `addHTTPMiddleware` in a wirings file:
|
|
55
62
|
|
|
56
63
|
```typescript
|
|
57
64
|
import { authBearer, authCookie, authAPIKey } from '@pikku/core/middleware'
|
|
58
|
-
import { addHTTPMiddleware } from '
|
|
65
|
+
import { addHTTPMiddleware } from '#pikku'
|
|
59
66
|
|
|
60
67
|
// JWT bearer token — reads Authorization header
|
|
61
68
|
addHTTPMiddleware('*', [authBearer()])
|
|
62
69
|
|
|
63
|
-
// Cookie-based sessions —
|
|
70
|
+
// Cookie-based sessions — re-issues the cookie when the session changes
|
|
64
71
|
addHTTPMiddleware('*', [
|
|
65
72
|
authCookie({
|
|
66
73
|
name: 'session',
|
|
67
74
|
expiresIn: { value: 30, unit: 'day' },
|
|
68
|
-
options: {
|
|
75
|
+
options: { sameSite: 'strict' },
|
|
69
76
|
}),
|
|
70
77
|
])
|
|
71
78
|
|
|
@@ -73,6 +80,40 @@ addHTTPMiddleware('*', [
|
|
|
73
80
|
addHTTPMiddleware('*', [authAPIKey({ source: 'all' })])
|
|
74
81
|
```
|
|
75
82
|
|
|
83
|
+
All three share the same escape hatch: they do nothing when there is no HTTP
|
|
84
|
+
request, or when a session is already set. That is what lets you stack several —
|
|
85
|
+
whichever runs first and finds a credential wins, and the rest step aside — and
|
|
86
|
+
it is also why none of them authenticate a queue job, a scheduled task or a
|
|
87
|
+
channel message. Those need a session set another way.
|
|
88
|
+
|
|
89
|
+
Each decodes its credential with the `jwt` service; without one registered, they
|
|
90
|
+
silently authenticate nobody.
|
|
91
|
+
|
|
92
|
+
**`authBearer` in static-token mode.** Passing `token` switches it from decoding
|
|
93
|
+
a JWT to comparing (in constant time) against a fixed value — the shape to use
|
|
94
|
+
for a service-to-service caller or a demo:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
authBearer({
|
|
98
|
+
token: {
|
|
99
|
+
secretId: 'AGENT_DEMO_TOKEN', // or: value: 'literal-token'
|
|
100
|
+
userSession: { userId: 'demo-user' },
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
An unset secret leaves the middleware inert rather than erroring, so a template
|
|
106
|
+
that ships this is safe until someone provides the secret. A malformed
|
|
107
|
+
`Authorization` header (no `Bearer ` scheme) throws `InvalidSessionError` in
|
|
108
|
+
either mode.
|
|
109
|
+
|
|
110
|
+
**`authCookie` options.** `name`, `expiresIn` and `options` are all part of the
|
|
111
|
+
config; `options` merges over the defaults `{ httpOnly: true, secure: true,
|
|
112
|
+
sameSite: 'lax', path: '/' }`, so only override what you need. The cookie is
|
|
113
|
+
re-issued after the request only when the session actually changed, which is how
|
|
114
|
+
a rolling session extends itself without writing a `Set-Cookie` on every
|
|
115
|
+
response.
|
|
116
|
+
|
|
76
117
|
## Complete Example
|
|
77
118
|
|
|
78
119
|
```typescript
|
|
@@ -84,10 +125,14 @@ export const isVerified = pikkuAuth(async (_services, session) => !!session?.ema
|
|
|
84
125
|
|
|
85
126
|
// wirings/auth.wiring.ts
|
|
86
127
|
import { authCookie } from '@pikku/core/middleware'
|
|
87
|
-
import { addHTTPMiddleware } from '
|
|
128
|
+
import { addHTTPMiddleware } from '#pikku'
|
|
88
129
|
|
|
89
130
|
addHTTPMiddleware('*', [
|
|
90
|
-
authCookie({
|
|
131
|
+
authCookie({
|
|
132
|
+
name: 'session',
|
|
133
|
+
expiresIn: { value: 30, unit: 'day' },
|
|
134
|
+
options: {},
|
|
135
|
+
}),
|
|
91
136
|
])
|
|
92
137
|
|
|
93
138
|
// functions/auth.functions.ts
|
|
@@ -95,14 +140,14 @@ export const login = pikkuFunc({
|
|
|
95
140
|
auth: false,
|
|
96
141
|
func: async ({ jwt, db }, { email, password }, { setSession }) => {
|
|
97
142
|
const user = await db.verifyCredentials(email, password)
|
|
98
|
-
setSession({ userId: user.id })
|
|
143
|
+
await setSession({ userId: user.id })
|
|
99
144
|
return { token: jwt.sign({ userId: user.id }) }
|
|
100
145
|
},
|
|
101
146
|
})
|
|
102
147
|
|
|
103
148
|
export const logout = pikkuFunc({
|
|
104
149
|
func: async ({}, _data, { clearSession }) => {
|
|
105
|
-
clearSession()
|
|
150
|
+
await clearSession()
|
|
106
151
|
},
|
|
107
152
|
})
|
|
108
153
|
```
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
name: pikku-services
|
|
3
3
|
description: >-
|
|
4
4
|
Use when setting up dependency injection, creating custom services, or configuring the service
|
|
5
|
-
layer in a Pikku app. Covers pikkuServices (singleton), pikkuWireServices (per-request),
|
|
6
|
-
typing, built-in services, and
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
layer in a Pikku app. Covers pikkuServices (singleton), pikkuWireServices (per-request),
|
|
6
|
+
pikkuServerLifecycle (startup/shutdown hooks), service typing, built-in services, and
|
|
7
|
+
tree-shaking. TRIGGER when: code uses pikkuServices/pikkuWireServices/pikkuServerLifecycle, user
|
|
8
|
+
asks about services.ts, lifecycle.ts, dependency injection, service factories, startup or
|
|
9
|
+
shutdown work, or built-in services (ConsoleLogger, JoseJWTService). DO NOT TRIGGER when: user asks
|
|
10
|
+
about middleware (use pikku-middleware), auth strategies or sessions (use pikku-security),
|
|
11
|
+
permissions (use pikku-permissions), or secrets/variables (use pikku-config).
|
|
10
12
|
installGroups: [core]
|
|
11
13
|
---
|
|
12
14
|
|
|
@@ -74,6 +76,39 @@ export const createWireServices = pikkuWireServices(
|
|
|
74
76
|
)
|
|
75
77
|
```
|
|
76
78
|
|
|
79
|
+
### `pikkuServerLifecycle(hooks)` — startup and shutdown work
|
|
80
|
+
|
|
81
|
+
A service factory should **construct** services, not run startup side effects. Seeding a database, warming a cache, starting a background consumer or draining a queue belongs in lifecycle hooks, which receive the singleton services after they are built:
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
// src/lifecycle.ts
|
|
85
|
+
import { pikkuServerLifecycle } from '@pikku/core'
|
|
86
|
+
import type { SingletonServices } from '../types/application-types.js'
|
|
87
|
+
|
|
88
|
+
export const lifecycle = pikkuServerLifecycle<SingletonServices>({
|
|
89
|
+
beforeStart: async ({ kysely }) => {
|
|
90
|
+
await runMigrations(kysely) // before the port opens
|
|
91
|
+
},
|
|
92
|
+
afterStart: async (services) => {
|
|
93
|
+
await seedDevData(services) // server is accepting traffic
|
|
94
|
+
},
|
|
95
|
+
beforeStop: async ({ queueService }) => {
|
|
96
|
+
await queueService.drain() // services are still alive here
|
|
97
|
+
},
|
|
98
|
+
afterStop: async () => {
|
|
99
|
+
await releaseExternalLock() // services are ALREADY stopped
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Every hook is optional. Order is `beforeStart` → server starts → `afterStart`, then on SIGINT `beforeStop` → services stopped → server stopped → `afterStop`.
|
|
105
|
+
|
|
106
|
+
**`afterStop` runs after the singleton services have been stopped.** It still receives the services object, but the services inside it are shut down — using one there is a use-after-close bug. Anything that needs a live service goes in `beforeStop`.
|
|
107
|
+
|
|
108
|
+
Export **exactly one** `pikkuServerLifecycle` from anywhere in `srcDirectories`; the inspector finds it by the wrapper call, so the filename is free (`src/lifecycle.ts` by convention). It must be an exported `const` initialized with a direct call to `pikkuServerLifecycle` — a re-export or a conditional wrapper is invisible to the inspector.
|
|
109
|
+
|
|
110
|
+
**Only `pikku dev` and `pikku serve` run these hooks.** If you bootstrap your own server (Express, Fastify, uWS, Lambda, Cloudflare, Next.js), no runtime adapter invokes them — put the work in your entrypoint instead.
|
|
111
|
+
|
|
77
112
|
### Auto-Generated Service Manifest
|
|
78
113
|
|
|
79
114
|
After `npx pikku all`, Pikku generates `.pikku/pikku-services.gen.ts`, a manifest of which services are actually used by wired functions:
|
|
@@ -97,7 +132,7 @@ export type RequiredSingletonServices = Pick<
|
|
|
97
132
|
|
|
98
133
|
### Using Services in Functions
|
|
99
134
|
|
|
100
|
-
**Every service must be declared in `SingletonServices` (or `Services`) in `application-types.d.ts`.** Never access a service via a body-level cast (`services as typeof services & { myService: MyService }`) — that means the type is missing. Add the import and the field to `SingletonServices`, then destructure inline in the function signature. The inspector emits `SERVICES_NOT_DESTRUCTURED` and tree-shaking breaks when the first param is a plain identifier rather than an object pattern. Never `new` a service inside a function — services arrive only via injection.
|
|
135
|
+
**Every service must be declared in `SingletonServices` (or `Services`) in `application-types.d.ts`.** Never access a service via a body-level cast (`services as typeof services & { myService: MyService }`) — that means the type is missing. Add the import and the field to `SingletonServices`, then destructure inline in the function signature. The inspector emits `SERVICES_NOT_DESTRUCTURED` (`PKU410`) and tree-shaking breaks when the first param is a plain identifier rather than an object pattern. Never `new` a service inside a function — services arrive only via injection.
|
|
101
136
|
|
|
102
137
|
```typescript
|
|
103
138
|
// ✅ Correct — inline destructure, no cast
|
|
@@ -122,15 +157,20 @@ const getUser = pikkuFunc({
|
|
|
122
157
|
|
|
123
158
|
**Never write a `if (!service) throw ...` existence guard in a function body.** It is dead code, and it defeats the platform.
|
|
124
159
|
|
|
125
|
-
Optionality lives in exactly one place — `services.ts` / the `SingletonServices` declaration — and it means *"this may not be created"*, not *"this may be missing at call time"*. A service is optional precisely because **nothing destructures it**, and `
|
|
160
|
+
Optionality lives in exactly one place — `services.ts` / the `SingletonServices` declaration — and it means *"this may not be created"*, not *"this may be missing at call time"*. A service is optional precisely because **nothing destructures it**, and the generated `requiredSingletonServices` manifest therefore never marks it for creation. The moment any wired function destructures it, Pikku creates it and guarantees it is there.
|
|
126
161
|
|
|
127
162
|
The types enforce this rather than merely documenting it. The inspector records the services destructured by every wired `func`, `permissions` **and** `middleware`, and emits them as `RequiredSingletonServices`. The generated function types then default their service parameter to:
|
|
128
163
|
|
|
129
164
|
```typescript
|
|
130
165
|
export type WiredSingletonServices = RequiredSingletonServices & SingletonServices
|
|
131
|
-
export type WiredServices = RequiredSingletonServices & Services
|
|
166
|
+
export type WiredServices = SecretlessServices<RequiredSingletonServices & Services>
|
|
132
167
|
```
|
|
133
168
|
|
|
169
|
+
The `SecretlessServices<...>` wrapper is why `secrets` never appears in a
|
|
170
|
+
function's services: it is stripped at the type level, not merely omitted by
|
|
171
|
+
convention. Read secrets in a service factory or middleware and hand the value
|
|
172
|
+
to a service instead.
|
|
173
|
+
|
|
134
174
|
so a service that is `foo?: Foo` in `SingletonServices` arrives as a non-optional `Foo` in every function, permission and middleware that uses it. There is nothing to guard against.
|
|
135
175
|
|
|
136
176
|
```typescript
|
|
@@ -232,7 +272,7 @@ export const createSingletonServices = pikkuServices(async (config) => {
|
|
|
232
272
|
|
|
233
273
|
export const createWireServices = pikkuWireServices(
|
|
234
274
|
async (singletonServices, wire) => ({
|
|
235
|
-
scopedLogger: new ScopedLogger(wire.session?.
|
|
275
|
+
scopedLogger: new ScopedLogger(wire.session?.userId),
|
|
236
276
|
})
|
|
237
277
|
)
|
|
238
278
|
|
|
@@ -23,7 +23,8 @@ The `audit` wire service is typed as `AuditLog` (from `@pikku/core`). Functions
|
|
|
23
23
|
```typescript
|
|
24
24
|
const deleteUser = pikkuFunc({
|
|
25
25
|
func: async ({ audit }, { userId }) => {
|
|
26
|
-
|
|
26
|
+
// The user identity comes from the wire session — the payload is metadata.
|
|
27
|
+
await audit.write({ type: 'user.deleted', source: 'explicit', metadata: { userId } })
|
|
27
28
|
// ...
|
|
28
29
|
},
|
|
29
30
|
})
|
|
@@ -18,11 +18,16 @@ commit, separate from any feature work.
|
|
|
18
18
|
_template_, not the user's project — leaving it in place is misleading.
|
|
19
19
|
Either delete it (`git rm README.md`) or rewrite it with the new project's
|
|
20
20
|
name and purpose. Never ship a clone with the generic template README.
|
|
21
|
-
2. **Keep the lockfile committed.**
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
2. **Keep the lockfile committed.** Do NOT re-add `yarn.lock` to `.gitignore`.
|
|
22
|
+
A real project commits its lockfile for reproducible installs. The correct
|
|
23
|
+
pattern is `yarn.lock` followed by `!/yarn.lock`, which commits the root
|
|
24
|
+
lockfile while keeping generated per-unit lockfiles under `.deploy/` (and
|
|
25
|
+
`e2e/`) ignored.
|
|
26
|
+
|
|
27
|
+
`create-pikku` keeps only the chosen package manager's lockfile and deletes
|
|
28
|
+
the other, and for yarn it may have written an **empty** `yarn.lock` as a
|
|
29
|
+
marker. Commit the lockfile *after* the first install has filled it in —
|
|
30
|
+
committing the empty placeholder pins nothing.
|
|
26
31
|
3. **Rename template identifiers.** Update `name` in the root `package.json`
|
|
27
32
|
(and any `@project/*` or other placeholder names) to the real project.
|
|
28
33
|
4. **Drop template-only artifacts.** Remove any `TEMPLATE.md`, demo docs, or
|
|
@@ -35,16 +35,23 @@ See `pikku-concepts` for the core mental model.
|
|
|
35
35
|
|
|
36
36
|
## API Reference
|
|
37
37
|
|
|
38
|
+
All three come from `#pikku`. A trigger is deliberately split in two: the
|
|
39
|
+
**source** owns the connection to the outside world and the **trigger** names the
|
|
40
|
+
function to run, so one source can be swapped (Redis → PG) without touching the
|
|
41
|
+
handler, and a handler can exist before any source is wired.
|
|
42
|
+
|
|
38
43
|
### `wireTrigger(config)`
|
|
39
44
|
|
|
40
45
|
Define the target function that handles trigger events:
|
|
41
46
|
|
|
42
47
|
```typescript
|
|
43
|
-
import { wireTrigger } from '
|
|
48
|
+
import { wireTrigger } from '#pikku'
|
|
44
49
|
|
|
45
50
|
wireTrigger({
|
|
46
51
|
name: string, // Trigger name (matches source)
|
|
47
52
|
func: PikkuFunc, // Function to call when event fires
|
|
53
|
+
description?: string,
|
|
54
|
+
tags?: string[],
|
|
48
55
|
})
|
|
49
56
|
```
|
|
50
57
|
|
|
@@ -53,18 +60,24 @@ wireTrigger({
|
|
|
53
60
|
Define the event source that fires triggers:
|
|
54
61
|
|
|
55
62
|
```typescript
|
|
56
|
-
import { wireTriggerSource } from '
|
|
63
|
+
import { wireTriggerSource } from '#pikku'
|
|
57
64
|
|
|
58
65
|
wireTriggerSource({
|
|
59
|
-
name: string, // Must match wireTrigger name
|
|
60
|
-
func: PikkuTriggerFunc, // Source function (sets up listener)
|
|
61
|
-
input: object, // Configuration
|
|
66
|
+
name: string, // Must match a wireTrigger name
|
|
67
|
+
func: PikkuTriggerFunc, // Source function (sets up the listener)
|
|
68
|
+
input: object, // Configuration handed to the source
|
|
62
69
|
})
|
|
63
70
|
```
|
|
64
71
|
|
|
72
|
+
`input` is required whenever the source function declares an input type, and the
|
|
73
|
+
name must be unique — wiring the same source name twice throws
|
|
74
|
+
`Trigger source already exists`.
|
|
75
|
+
|
|
65
76
|
### `pikkuTriggerFunc<TInput, TEvent>`
|
|
66
77
|
|
|
67
|
-
|
|
78
|
+
A trigger source function runs **once at startup**, not once per event. It sets
|
|
79
|
+
up a listener, calls `trigger.invoke(...)` for each event it sees, and returns a
|
|
80
|
+
teardown function:
|
|
68
81
|
|
|
69
82
|
```typescript
|
|
70
83
|
import { pikkuTriggerFunc } from '#pikku'
|
|
@@ -83,6 +96,37 @@ const source = pikkuTriggerFunc<
|
|
|
83
96
|
})
|
|
84
97
|
```
|
|
85
98
|
|
|
99
|
+
It receives **singleton services only** — there is no session, no request and no
|
|
100
|
+
per-wire services, because a listener outlives every event it will ever emit.
|
|
101
|
+
The config-object form (`pikkuTriggerFunc({ func, title, description, tags,
|
|
102
|
+
input, output })`) is also accepted when you want schemas or metadata on the
|
|
103
|
+
source.
|
|
104
|
+
|
|
105
|
+
## Starting triggers
|
|
106
|
+
|
|
107
|
+
Nothing fires until a `TriggerService` is started. For a single process,
|
|
108
|
+
`InMemoryTriggerService` walks every wired source that has at least one matching
|
|
109
|
+
target and sets it up:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { InMemoryTriggerService } from '@pikku/core/services'
|
|
113
|
+
|
|
114
|
+
const triggerService = new InMemoryTriggerService()
|
|
115
|
+
await triggerService.start()
|
|
116
|
+
// on shutdown
|
|
117
|
+
await triggerService.stop() // runs every source's teardown
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
A source with no matching `wireTrigger` is logged and skipped rather than
|
|
121
|
+
erroring — the two halves are wired independently, so a half-wired trigger is a
|
|
122
|
+
normal intermediate state.
|
|
123
|
+
|
|
124
|
+
**If a wiring is silently skipped**, look for
|
|
125
|
+
`Skipping trigger … metadata not found` in the logs. Both wirings read metadata
|
|
126
|
+
generated by the inspector, and it warns rather than throwing; the usual fix is
|
|
127
|
+
the one the warning suggests — move the wiring into its own file so codegen
|
|
128
|
+
picks it up.
|
|
129
|
+
|
|
86
130
|
## Usage Patterns
|
|
87
131
|
|
|
88
132
|
### Redis Pub/Sub Source
|
|
@@ -34,20 +34,22 @@ See `pikku-concepts` for the core mental model.
|
|
|
34
34
|
|
|
35
35
|
## Function Versioning
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
A function with `version: N` is registered under the id `name@vN`. The bare
|
|
38
|
+
name still resolves to it, so callers that don't care about versions keep
|
|
39
|
+
working while a pinned `getBook@v1` stays addressable for the ones that do.
|
|
38
40
|
|
|
39
|
-
**The pattern:**
|
|
41
|
+
**The pattern:** when you need to introduce a breaking change, copy the current
|
|
42
|
+
function into a pinned `v1` and bump the live one to `version: 2`.
|
|
40
43
|
|
|
41
|
-
1. Create
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
1. Create `my-function-v1.function.ts` exporting `getBookV1` with `version: 1` —
|
|
45
|
+
the trailing `V1` matching the version is stripped automatically, so the id
|
|
46
|
+
becomes `getBook@v1`
|
|
47
|
+
2. Add `version: 2` to the existing `getBook`
|
|
45
48
|
|
|
46
49
|
```typescript
|
|
47
50
|
// my-function-v1.function.ts — old contract, kept for running workflows/agents
|
|
48
51
|
export const getBookV1 = pikkuFunc({
|
|
49
|
-
|
|
50
|
-
version: 1,
|
|
52
|
+
version: 1, // id becomes getBook@v1 — the V1 suffix is stripped
|
|
51
53
|
input: z.object({ bookId: z.string() }),
|
|
52
54
|
output: z.object({ title: z.string() }),
|
|
53
55
|
func: async ({ db }, { bookId }) => {
|
|
@@ -55,8 +57,9 @@ export const getBookV1 = pikkuFunc({
|
|
|
55
57
|
},
|
|
56
58
|
})
|
|
57
59
|
|
|
58
|
-
// my-function.function.ts — latest contract,
|
|
60
|
+
// my-function.function.ts — latest contract, id becomes getBook@v2
|
|
59
61
|
export const getBook = pikkuFunc({
|
|
62
|
+
version: 2,
|
|
60
63
|
input: z.object({
|
|
61
64
|
bookId: z.string(),
|
|
62
65
|
format: z.enum(['full', 'summary']),
|
|
@@ -72,7 +75,17 @@ export const getBook = pikkuFunc({
|
|
|
72
75
|
})
|
|
73
76
|
```
|
|
74
77
|
|
|
75
|
-
**
|
|
78
|
+
**Bump the live function explicitly.** Nothing promotes an unversioned function
|
|
79
|
+
to the next version for you — without `version: 2` it is treated as version 1 of
|
|
80
|
+
the `getBook` contract, colliding with the pinned `getBook@v1` and making
|
|
81
|
+
`versions check` report the published contract as modified.
|
|
82
|
+
|
|
83
|
+
**`override` is the escape hatch, not the requirement.** The contract key comes
|
|
84
|
+
from the exported name with a matching `V<n>` suffix removed, so
|
|
85
|
+
`getBookV1` + `version: 1` already lands on `getBook`. Use
|
|
86
|
+
`override: 'getBook'` only when the export can't follow that convention — for
|
|
87
|
+
instance `legacyGetBook` with `version: 1`, which would otherwise key under
|
|
88
|
+
`legacyGetBook`.
|
|
76
89
|
|
|
77
90
|
## Version Manifest (`versions.pikku.json`)
|
|
78
91
|
|
|
@@ -99,22 +112,38 @@ Pikku tracks contract hashes to detect breaking changes:
|
|
|
99
112
|
}
|
|
100
113
|
```
|
|
101
114
|
|
|
102
|
-
Each hash is derived from the function's input and output schemas plus the
|
|
115
|
+
Each hash is derived from the function's input and output schemas plus the
|
|
116
|
+
contract key. If a schema changes without a version bump, `pikku versions check`
|
|
117
|
+
will fail.
|
|
118
|
+
|
|
119
|
+
The manifest lives at `versions.pikku.json` in the project's `rootDir`, and its
|
|
120
|
+
presence is what switches versioning on — with no manifest, nothing is checked.
|
|
103
121
|
|
|
104
122
|
## CLI Commands
|
|
105
123
|
|
|
106
124
|
```bash
|
|
107
|
-
npx pikku versions init #
|
|
125
|
+
npx pikku versions init # Create an empty versioning manifest (run once)
|
|
108
126
|
npx pikku versions check # Detect contract changes (use in CI)
|
|
109
|
-
npx pikku versions update #
|
|
127
|
+
npx pikku versions update # Record current contract hashes
|
|
110
128
|
```
|
|
111
129
|
|
|
130
|
+
`init` writes `{ "manifestVersion": 1, "contracts": {} }` and nothing more — it
|
|
131
|
+
does **not** capture the hashes of the functions you already have. Run
|
|
132
|
+
`versions update` straight after it to record the current state, otherwise
|
|
133
|
+
`check` has nothing to compare against and silently passes.
|
|
134
|
+
|
|
135
|
+
`update` refuses to save when a published version's hash changed, so it can
|
|
136
|
+
never overwrite an immutable record; it reports that as a diagnostic and leaves
|
|
137
|
+
the manifest alone. Fix the contract or bump the version, then run it again.
|
|
138
|
+
|
|
112
139
|
**Workflow:**
|
|
113
140
|
|
|
114
|
-
1. `pikku versions init` —
|
|
141
|
+
1. `pikku versions init` then `pikku versions update` — once, to create and
|
|
142
|
+
populate `versions.pikku.json`
|
|
115
143
|
2. Develop normally — add/modify functions
|
|
116
144
|
3. `pikku versions check` — CI catches unversioned breaking changes
|
|
117
|
-
4. If intentional:
|
|
145
|
+
4. If intentional: pin the old contract as `…V1` with `version: 1`, bump the
|
|
146
|
+
live function to `version: 2`, then `pikku versions update`
|
|
118
147
|
|
|
119
148
|
## CI Integration
|
|
120
149
|
|
|
@@ -135,9 +164,8 @@ jobs:
|
|
|
135
164
|
## Complete Example
|
|
136
165
|
|
|
137
166
|
```typescript
|
|
138
|
-
// create-todo-v1.function.ts — v1 locked contract
|
|
167
|
+
// create-todo-v1.function.ts — v1 locked contract, id: createTodo@v1
|
|
139
168
|
export const createTodoV1 = pikkuSessionlessFunc({
|
|
140
|
-
override: 'createTodo', // groups under 'createTodo' contract family
|
|
141
169
|
version: 1,
|
|
142
170
|
input: z.object({ title: z.string() }),
|
|
143
171
|
output: z.object({ id: z.string(), title: z.string() }),
|
|
@@ -146,6 +174,7 @@ export const createTodoV1 = pikkuSessionlessFunc({
|
|
|
146
174
|
|
|
147
175
|
// create-todo.function.ts — v2 (latest), called by default
|
|
148
176
|
export const createTodo = pikkuSessionlessFunc({
|
|
177
|
+
version: 2,
|
|
149
178
|
input: z.object({
|
|
150
179
|
title: z.string(),
|
|
151
180
|
priority: z.enum(['low', 'medium', 'high']),
|