lambda-pipe 0.1.0
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/LICENSE +21 -0
- package/README.md +761 -0
- package/build/cjs/cli/index.js +1 -0
- package/build/cjs/index.js +1 -0
- package/build/cjs/jobs/index.js +1 -0
- package/build/cjs/logger/index.js +1 -0
- package/build/cjs/runtime/index.js +1 -0
- package/build/cjs/websocket/index.js +1 -0
- package/build/cli/dev.d.ts +10 -0
- package/build/core/cache/index.d.ts +19 -0
- package/build/core/context/createRequestContext.d.ts +2 -0
- package/build/core/context/index.d.ts +2 -0
- package/build/core/context/types.d.ts +52 -0
- package/build/core/context/utils.d.ts +6 -0
- package/build/core/errors/HttpError.d.ts +6 -0
- package/build/core/errors/handleError.d.ts +2 -0
- package/build/core/errors/http.d.ts +13 -0
- package/build/core/errors/index.d.ts +3 -0
- package/build/core/http/helpers.d.ts +6 -0
- package/build/core/http/index.d.ts +3 -0
- package/build/core/http/serialize.d.ts +2 -0
- package/build/core/http/types.d.ts +6 -0
- package/build/core/index.d.ts +8 -0
- package/build/core/lifecycle/hooks.d.ts +4 -0
- package/build/core/lifecycle/index.d.ts +1 -0
- package/build/core/middleware/index.d.ts +2 -0
- package/build/core/middleware/run.d.ts +4 -0
- package/build/core/middleware/types.d.ts +11 -0
- package/build/core/plugins/index.d.ts +1 -0
- package/build/core/plugins/types.d.ts +15 -0
- package/build/core/routing/defineHandler.d.ts +14 -0
- package/build/core/routing/defineRoute.d.ts +20 -0
- package/build/core/routing/execution.d.ts +4 -0
- package/build/core/routing/index.d.ts +3 -0
- package/build/core/routing/types.d.ts +55 -0
- package/build/core/validation/index.d.ts +2 -0
- package/build/core/validation/types.d.ts +8 -0
- package/build/core/validation/validate.d.ts +2 -0
- package/build/esm/cli/index.js +1 -0
- package/build/esm/index.js +1 -0
- package/build/esm/jobs/index.js +1 -0
- package/build/esm/logger/index.js +1 -0
- package/build/esm/runtime/index.js +1 -0
- package/build/esm/websocket/index.js +1 -0
- package/build/index.d.ts +1 -0
- package/build/jobs/defineJob.d.ts +5 -0
- package/build/jobs/index.d.ts +2 -0
- package/build/jobs/sqs.d.ts +7 -0
- package/build/logger/index.d.ts +2 -0
- package/build/logger/logger.d.ts +2 -0
- package/build/logger/types.d.ts +6 -0
- package/build/routes/health.d.ts +2 -0
- package/build/routes/user.d.ts +2 -0
- package/build/runtime/aws/createAWSHandler.d.ts +3 -0
- package/build/runtime/aws/index.d.ts +2 -0
- package/build/runtime/aws/request.d.ts +4 -0
- package/build/runtime/fastify/createFastifyHandler.d.ts +3 -0
- package/build/runtime/fastify/index.d.ts +2 -0
- package/build/runtime/fastify/request.d.ts +4 -0
- package/build/runtime/index.d.ts +2 -0
- package/build/websocket/index.d.ts +16 -0
- package/docs/dev-server.md +278 -0
- package/docs/websocket.md +118 -0
- package/package.json +135 -0
package/README.md
ADDED
|
@@ -0,0 +1,761 @@
|
|
|
1
|
+
# ⚡ lambda-pipe
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/lambda-pipe)  
|
|
4
|
+
|
|
5
|
+
[LIVE EXAMPLE](https://codesandbox.io/p/devbox/2tyg7y)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
🚀 A type-safe plugin & middleware runtime for AWS Lambda APIs.
|
|
9
|
+
|
|
10
|
+
> Build composable serverless backends with typed context, lifecycle hooks, validation, plugins, middleware, caching, runtime adapters, and background jobs.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Basic Route
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
// src/routes
|
|
18
|
+
import {
|
|
19
|
+
defineRoute,
|
|
20
|
+
ok,
|
|
21
|
+
} from 'lambda-pipe'
|
|
22
|
+
|
|
23
|
+
export default defineRoute({
|
|
24
|
+
method: 'GET',
|
|
25
|
+
|
|
26
|
+
path: '/health',
|
|
27
|
+
|
|
28
|
+
handler: async () => {
|
|
29
|
+
return ok({
|
|
30
|
+
status: 'ok',
|
|
31
|
+
})
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
# What is lambda-pipe?
|
|
40
|
+
|
|
41
|
+
lambda-pipe is a lightweight execution runtime for serverless APIs.
|
|
42
|
+
|
|
43
|
+
It provides:
|
|
44
|
+
|
|
45
|
+
* typed request context
|
|
46
|
+
* middleware lifecycle
|
|
47
|
+
* plugin system
|
|
48
|
+
* validation pipeline
|
|
49
|
+
* authentication pipeline
|
|
50
|
+
* execution metadata
|
|
51
|
+
* cache layer
|
|
52
|
+
* runtime adapters
|
|
53
|
+
* background job handlers
|
|
54
|
+
|
|
55
|
+
without forcing framework architecture.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
# Why lambda-pipe?
|
|
60
|
+
|
|
61
|
+
Most serverless projects become messy over time:
|
|
62
|
+
|
|
63
|
+
* auth duplicated everywhere
|
|
64
|
+
* validation inconsistent
|
|
65
|
+
* middleware order unclear
|
|
66
|
+
* request context untyped
|
|
67
|
+
* plugins tightly coupled
|
|
68
|
+
* runtime logic scattered
|
|
69
|
+
|
|
70
|
+
lambda-pipe provides:
|
|
71
|
+
|
|
72
|
+
* deterministic execution
|
|
73
|
+
* composable plugins
|
|
74
|
+
* centralized lifecycle
|
|
75
|
+
* typed runtime context
|
|
76
|
+
* adapter-friendly architecture
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
# Features
|
|
81
|
+
|
|
82
|
+
* ⚡ Serverless-first runtime
|
|
83
|
+
* 🔌 Plugin system
|
|
84
|
+
* 🧩 Middleware pipeline
|
|
85
|
+
* 🧠 Cold start lifecycle hooks
|
|
86
|
+
* 🛡 Typed validation
|
|
87
|
+
* 🔑 Authentication pipeline
|
|
88
|
+
* 🍪 Cookie helpers
|
|
89
|
+
* 🧱 Runtime adapters
|
|
90
|
+
* 📝 Full TypeScript inference
|
|
91
|
+
* 💾 In-memory execution cache
|
|
92
|
+
* 🌐 Framework agnostic
|
|
93
|
+
* 📦 SQS job runtime
|
|
94
|
+
* 🚀 Fastify development server
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
# Metal model
|
|
99
|
+
|
|
100
|
+
```text
|
|
101
|
+
Runtime Adapter
|
|
102
|
+
↓
|
|
103
|
+
normalizeRequest()
|
|
104
|
+
↓
|
|
105
|
+
createRequestContext()
|
|
106
|
+
↓
|
|
107
|
+
Plugins
|
|
108
|
+
↓
|
|
109
|
+
Middleware Pipeline
|
|
110
|
+
↓
|
|
111
|
+
Handler
|
|
112
|
+
↓
|
|
113
|
+
serializeResponse()
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
# Installation
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
npm install lambda-pipe
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
# Route Definition
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import {
|
|
130
|
+
defineRoute,
|
|
131
|
+
ok,
|
|
132
|
+
} from 'lambda-pipe'
|
|
133
|
+
|
|
134
|
+
export default defineRoute({
|
|
135
|
+
method: 'GET',
|
|
136
|
+
|
|
137
|
+
path: '/users/:id',
|
|
138
|
+
|
|
139
|
+
handler: async (context) => {
|
|
140
|
+
return ok({
|
|
141
|
+
id: context.params.id,
|
|
142
|
+
})
|
|
143
|
+
},
|
|
144
|
+
})
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
# Typed Context
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import {
|
|
153
|
+
defineRoute,
|
|
154
|
+
ok,
|
|
155
|
+
} from 'lambda-pipe'
|
|
156
|
+
|
|
157
|
+
export default defineRoute<
|
|
158
|
+
{
|
|
159
|
+
query: {
|
|
160
|
+
expand?: string
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
params: {
|
|
164
|
+
id: string
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
>({
|
|
168
|
+
method: 'GET',
|
|
169
|
+
|
|
170
|
+
path: '/users/:id',
|
|
171
|
+
|
|
172
|
+
handler: async (context) => {
|
|
173
|
+
context.query.expand
|
|
174
|
+
|
|
175
|
+
context.params.id
|
|
176
|
+
|
|
177
|
+
return ok()
|
|
178
|
+
},
|
|
179
|
+
})
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
# Validation
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
import {
|
|
188
|
+
defineRoute,
|
|
189
|
+
ok,
|
|
190
|
+
ValidationError,
|
|
191
|
+
} from 'lambda-pipe'
|
|
192
|
+
|
|
193
|
+
import type {
|
|
194
|
+
Validator,
|
|
195
|
+
} from 'lambda-pipe'
|
|
196
|
+
|
|
197
|
+
const paramsValidator: Validator<{
|
|
198
|
+
id: string
|
|
199
|
+
}> = async (input) => {
|
|
200
|
+
const params = input as any
|
|
201
|
+
|
|
202
|
+
if (!params.id) {
|
|
203
|
+
throw new ValidationError('missing id')
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
id: params.id,
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export default defineRoute({
|
|
212
|
+
method: 'GET',
|
|
213
|
+
|
|
214
|
+
path: '/users/:id',
|
|
215
|
+
|
|
216
|
+
validate: {
|
|
217
|
+
params: paramsValidator,
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
handler: async (context) => {
|
|
221
|
+
return ok(context.params)
|
|
222
|
+
},
|
|
223
|
+
})
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
# Middleware
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
import type {
|
|
232
|
+
Middleware,
|
|
233
|
+
} from 'lambda-pipe'
|
|
234
|
+
|
|
235
|
+
const loggerMiddleware: Middleware = {
|
|
236
|
+
onRequest: async (context, next) => {
|
|
237
|
+
console.log(context.req.requestId)
|
|
238
|
+
|
|
239
|
+
await next()
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
onResponse: async () => {
|
|
243
|
+
console.log('response sent')
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
onError: async (error) => {
|
|
247
|
+
console.error(error)
|
|
248
|
+
},
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
# Middleware Lifecycle
|
|
255
|
+
|
|
256
|
+
| Hook | Description |
|
|
257
|
+
| --------------- | ---------------------------------- |
|
|
258
|
+
| `onInit` | Runs once during cold start |
|
|
259
|
+
| `onRequest` | Runs before handler |
|
|
260
|
+
| `onResponse` | Runs before response serialization |
|
|
261
|
+
| `afterResponse` | Runs after response serialization |
|
|
262
|
+
| `onError` | Runs on request failure |
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
# Plugins
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
import type {
|
|
270
|
+
Plugin,
|
|
271
|
+
} from 'lambda-pipe'
|
|
272
|
+
|
|
273
|
+
const authPlugin: Plugin<{
|
|
274
|
+
auth: {
|
|
275
|
+
can: (scope: string) => boolean
|
|
276
|
+
}
|
|
277
|
+
}> = {
|
|
278
|
+
name: 'auth',
|
|
279
|
+
|
|
280
|
+
setup: async () => {
|
|
281
|
+
console.log('setup once')
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
onColdStart: async () => {
|
|
285
|
+
console.log('cold start')
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
extend: () => {
|
|
289
|
+
return {
|
|
290
|
+
auth: {
|
|
291
|
+
can: (scope) => {
|
|
292
|
+
return scope === 'read:user'
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
# Plugin Context Injection
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
import {
|
|
306
|
+
defineRoute,
|
|
307
|
+
ok,
|
|
308
|
+
} from 'lambda-pipe'
|
|
309
|
+
|
|
310
|
+
type User = {
|
|
311
|
+
id: string
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
type PluginContext = {
|
|
315
|
+
auth: {
|
|
316
|
+
can: (scope: string) => boolean
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export default defineRoute<
|
|
321
|
+
{},
|
|
322
|
+
User,
|
|
323
|
+
PluginContext
|
|
324
|
+
>({
|
|
325
|
+
plugins: [authPlugin],
|
|
326
|
+
|
|
327
|
+
handler: async (context) => {
|
|
328
|
+
context.auth.can('read:user')
|
|
329
|
+
|
|
330
|
+
return ok()
|
|
331
|
+
},
|
|
332
|
+
})
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
# Authentication
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
import {
|
|
341
|
+
defineRoute,
|
|
342
|
+
ok,
|
|
343
|
+
} from 'lambda-pipe'
|
|
344
|
+
|
|
345
|
+
export default defineRoute({
|
|
346
|
+
authenticate: async (token) => {
|
|
347
|
+
if (token !== 'valid-token') {
|
|
348
|
+
return null
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
user: {
|
|
353
|
+
id: 'u1',
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
scope: ['read:user'],
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
handler: async (context) => {
|
|
361
|
+
return ok({
|
|
362
|
+
user: context.req.user,
|
|
363
|
+
})
|
|
364
|
+
},
|
|
365
|
+
})
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
# Request Context
|
|
371
|
+
|
|
372
|
+
| Property | Description |
|
|
373
|
+
| ------------------------ | ------------------ |
|
|
374
|
+
| `context.req.user` | authenticated user |
|
|
375
|
+
| `context.req.scope` | auth scopes |
|
|
376
|
+
| `context.req.metadata` | auth metadata |
|
|
377
|
+
| `context.req.requestId` | request id |
|
|
378
|
+
| `context.req.ip` | client ip |
|
|
379
|
+
| `context.req.userAgent` | request user agent |
|
|
380
|
+
| `context.req.cookies` | parsed cookies |
|
|
381
|
+
| `context.req.receivedAt` | request timestamp |
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
# Execution Context
|
|
386
|
+
|
|
387
|
+
| Property | Description |
|
|
388
|
+
| -------------------------- | -------------------- |
|
|
389
|
+
| `context.exec.isColdStart` | lambda cold start |
|
|
390
|
+
| `context.exec.containerId` | runtime container id |
|
|
391
|
+
| `context.exec.cache` | in-memory cache |
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
# Cache Example
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
const cacheKey = `user:${context.params.id}`
|
|
399
|
+
|
|
400
|
+
const user = await context.exec.cache.getOrSet(
|
|
401
|
+
cacheKey,
|
|
402
|
+
async () => {
|
|
403
|
+
return db.user.findUnique({
|
|
404
|
+
where: {
|
|
405
|
+
id: context.params.id,
|
|
406
|
+
},
|
|
407
|
+
})
|
|
408
|
+
},
|
|
409
|
+
10000,
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
return ok(user)
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
# Response Helpers
|
|
418
|
+
|
|
419
|
+
```ts
|
|
420
|
+
import {
|
|
421
|
+
ok,
|
|
422
|
+
json,
|
|
423
|
+
badRequest,
|
|
424
|
+
unauthorized,
|
|
425
|
+
forbidden,
|
|
426
|
+
} from 'lambda-pipe'
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
| Helper | Status |
|
|
430
|
+
| ---------------- | ------ |
|
|
431
|
+
| `ok()` | 200 |
|
|
432
|
+
| `json()` | custom |
|
|
433
|
+
| `badRequest()` | 400 |
|
|
434
|
+
| `unauthorized()` | 401 |
|
|
435
|
+
| `forbidden()` | 403 |
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
439
|
+
# Context Response API
|
|
440
|
+
|
|
441
|
+
```ts
|
|
442
|
+
context.setHeader('x-request-id', '123')
|
|
443
|
+
|
|
444
|
+
context.status(200)
|
|
445
|
+
|
|
446
|
+
context.setCookie('session', 'abc', {
|
|
447
|
+
httpOnly: true,
|
|
448
|
+
secure: true,
|
|
449
|
+
path: '/',
|
|
450
|
+
maxAge: 3600,
|
|
451
|
+
})
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
456
|
+
# Error Handling
|
|
457
|
+
|
|
458
|
+
```ts
|
|
459
|
+
import {
|
|
460
|
+
HttpError,
|
|
461
|
+
} from 'lambda-pipe'
|
|
462
|
+
|
|
463
|
+
throw new HttpError(
|
|
464
|
+
403,
|
|
465
|
+
'Forbidden',
|
|
466
|
+
)
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
# Built-in Errors
|
|
472
|
+
|
|
473
|
+
```ts
|
|
474
|
+
import {
|
|
475
|
+
UnauthorizedError,
|
|
476
|
+
ForbiddenError,
|
|
477
|
+
ValidationError,
|
|
478
|
+
NotFoundError,
|
|
479
|
+
} from 'lambda-pipe'
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
# AWS Lambda Example
|
|
485
|
+
|
|
486
|
+
```ts
|
|
487
|
+
// src/functions/user.ts
|
|
488
|
+
|
|
489
|
+
import {
|
|
490
|
+
lambdaHandler,
|
|
491
|
+
} from 'lambda-pipe/runtime'
|
|
492
|
+
|
|
493
|
+
import route from '../routes/user'
|
|
494
|
+
|
|
495
|
+
export const handler =
|
|
496
|
+
lambdaHandler(route)
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
# SQS Job Example
|
|
502
|
+
|
|
503
|
+
```ts
|
|
504
|
+
import {
|
|
505
|
+
defineJob,
|
|
506
|
+
sqsHandler,
|
|
507
|
+
} from 'lambda-pipe/jobs'
|
|
508
|
+
|
|
509
|
+
const userCreatedJob = defineJob({
|
|
510
|
+
name: 'user-created',
|
|
511
|
+
|
|
512
|
+
handler: async (payload: {
|
|
513
|
+
id: string
|
|
514
|
+
}) => {
|
|
515
|
+
console.log(payload.id)
|
|
516
|
+
},
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
export const handler =
|
|
520
|
+
sqsHandler(userCreatedJob)
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
---
|
|
524
|
+
|
|
525
|
+
# Utilities
|
|
526
|
+
|
|
527
|
+
```ts
|
|
528
|
+
import {
|
|
529
|
+
normalizeHeaders,
|
|
530
|
+
parseCookies,
|
|
531
|
+
extractBearerToken,
|
|
532
|
+
safeJsonParse,
|
|
533
|
+
} from 'lambda-pipe'
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
---
|
|
537
|
+
|
|
538
|
+
# Route Example
|
|
539
|
+
|
|
540
|
+
```ts
|
|
541
|
+
import {
|
|
542
|
+
defineRoute,
|
|
543
|
+
ok,
|
|
544
|
+
HttpError,
|
|
545
|
+
} from 'lambda-pipe'
|
|
546
|
+
|
|
547
|
+
import type {
|
|
548
|
+
Middleware,
|
|
549
|
+
Plugin,
|
|
550
|
+
} from 'lambda-pipe'
|
|
551
|
+
|
|
552
|
+
type User = {
|
|
553
|
+
id: string
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
type PluginContext = {
|
|
557
|
+
auth: {
|
|
558
|
+
can: (
|
|
559
|
+
scope: string,
|
|
560
|
+
) => boolean
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* plugin
|
|
566
|
+
*/
|
|
567
|
+
|
|
568
|
+
const authPlugin: Plugin<
|
|
569
|
+
PluginContext
|
|
570
|
+
> = {
|
|
571
|
+
name: 'auth',
|
|
572
|
+
|
|
573
|
+
extend: () => ({
|
|
574
|
+
auth: {
|
|
575
|
+
can: (scope: string) =>
|
|
576
|
+
scope === 'read:user',
|
|
577
|
+
},
|
|
578
|
+
}),
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* middleware
|
|
583
|
+
*/
|
|
584
|
+
|
|
585
|
+
const authMiddleware: Middleware<
|
|
586
|
+
any,
|
|
587
|
+
User,
|
|
588
|
+
PluginContext
|
|
589
|
+
> = {
|
|
590
|
+
onRequest: async (
|
|
591
|
+
context,
|
|
592
|
+
next,
|
|
593
|
+
) => {
|
|
594
|
+
if (!context.req.user) {
|
|
595
|
+
throw new HttpError(
|
|
596
|
+
401,
|
|
597
|
+
'Unauthorized',
|
|
598
|
+
)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
await next()
|
|
602
|
+
},
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* route
|
|
607
|
+
*/
|
|
608
|
+
|
|
609
|
+
export default defineRoute<
|
|
610
|
+
{
|
|
611
|
+
params: {
|
|
612
|
+
id: string
|
|
613
|
+
}
|
|
614
|
+
},
|
|
615
|
+
User,
|
|
616
|
+
PluginContext
|
|
617
|
+
>({
|
|
618
|
+
method: 'GET',
|
|
619
|
+
|
|
620
|
+
path: '/users/:id',
|
|
621
|
+
|
|
622
|
+
plugins: [authPlugin],
|
|
623
|
+
|
|
624
|
+
middleware: [
|
|
625
|
+
authMiddleware,
|
|
626
|
+
],
|
|
627
|
+
|
|
628
|
+
authenticate: async (token) => {
|
|
629
|
+
if (token !== 'valid-token') {
|
|
630
|
+
return null
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return {
|
|
634
|
+
user: {
|
|
635
|
+
id: 'u1',
|
|
636
|
+
},
|
|
637
|
+
|
|
638
|
+
scope: ['read:user'],
|
|
639
|
+
}
|
|
640
|
+
},
|
|
641
|
+
|
|
642
|
+
handler: async (context) => {
|
|
643
|
+
return ok({
|
|
644
|
+
id: context.params.id,
|
|
645
|
+
|
|
646
|
+
canRead:
|
|
647
|
+
context.auth.can(
|
|
648
|
+
'read:user',
|
|
649
|
+
),
|
|
650
|
+
|
|
651
|
+
requestId:
|
|
652
|
+
context.req.requestId,
|
|
653
|
+
})
|
|
654
|
+
},
|
|
655
|
+
})
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
---
|
|
659
|
+
|
|
660
|
+
# Development Runtime
|
|
661
|
+
|
|
662
|
+
Local development server that simulates AWS Lambda execution using a Fastify-based runtime.
|
|
663
|
+
|
|
664
|
+
It converts HTTP requests into Lambda-like events and executes routes with full middleware + plugin lifecycle.
|
|
665
|
+
|
|
666
|
+
```ts
|
|
667
|
+
import { createDevApp } from 'lambda-pipe/cli'
|
|
668
|
+
|
|
669
|
+
await createDevApp({
|
|
670
|
+
port: 3000,
|
|
671
|
+
routesDir: 'src/routes',
|
|
672
|
+
})
|
|
673
|
+
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
```bash
|
|
677
|
+
npm run dev
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
See: [DOC](https://github.com/delpikye-v/lambda-pipe/blob/main/docs/dev-server.md
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
---
|
|
684
|
+
|
|
685
|
+
# WebSocket Runtime
|
|
686
|
+
|
|
687
|
+
`lambda-pipe` supports AWS API Gateway WebSocket APIs with a simple handler abstraction.
|
|
688
|
+
|
|
689
|
+
It handles:
|
|
690
|
+
|
|
691
|
+
- $connect
|
|
692
|
+
- $disconnect
|
|
693
|
+
- custom route messages
|
|
694
|
+
|
|
695
|
+
```ts
|
|
696
|
+
import { wsHandler } from 'lambda-pipe/websocket'
|
|
697
|
+
|
|
698
|
+
export const handler = wsHandler({
|
|
699
|
+
connect: async () => {
|
|
700
|
+
console.log('client connected')
|
|
701
|
+
},
|
|
702
|
+
|
|
703
|
+
message: async (ctx) => {
|
|
704
|
+
return {
|
|
705
|
+
statusCode: 200,
|
|
706
|
+
body: JSON.stringify({ ok: true }),
|
|
707
|
+
}
|
|
708
|
+
},
|
|
709
|
+
|
|
710
|
+
disconnect: async () => {
|
|
711
|
+
console.log('client disconnected')
|
|
712
|
+
},
|
|
713
|
+
})
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
See: [DOC](https://github.com/delpikye-v/lambda-pipe/blob/main/docs/websocket.md)
|
|
717
|
+
|
|
718
|
+
---
|
|
719
|
+
|
|
720
|
+
# Philosophy
|
|
721
|
+
|
|
722
|
+
```text
|
|
723
|
+
You control:
|
|
724
|
+
- plugins
|
|
725
|
+
- middleware
|
|
726
|
+
- runtime
|
|
727
|
+
- architecture
|
|
728
|
+
|
|
729
|
+
lambda-pipe controls:
|
|
730
|
+
- execution
|
|
731
|
+
- lifecycle
|
|
732
|
+
- typing
|
|
733
|
+
- orchestration
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
---
|
|
737
|
+
|
|
738
|
+
# Design Principles
|
|
739
|
+
|
|
740
|
+
* Explicit execution
|
|
741
|
+
* Typed runtime
|
|
742
|
+
* Composition over inheritance
|
|
743
|
+
* Runtime agnostic
|
|
744
|
+
* Serverless-first architecture
|
|
745
|
+
|
|
746
|
+
---
|
|
747
|
+
|
|
748
|
+
# When to Use
|
|
749
|
+
|
|
750
|
+
* Serverless APIs
|
|
751
|
+
* Lambda backends
|
|
752
|
+
* Internal platforms
|
|
753
|
+
* Runtime adapters
|
|
754
|
+
* Typed middleware systems
|
|
755
|
+
* Plugin-based architecture
|
|
756
|
+
|
|
757
|
+
---
|
|
758
|
+
|
|
759
|
+
# License
|
|
760
|
+
|
|
761
|
+
MIT
|