@jaypie/mcp 0.3.2 → 0.4.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/dist/createMcpServer.d.ts +7 -1
- package/dist/index.js +26 -3135
- package/dist/index.js.map +1 -1
- package/dist/suite.d.ts +1 -0
- package/dist/suite.js +2442 -0
- package/dist/suite.js.map +1 -0
- package/package.json +8 -3
- package/release-notes/constructs/1.2.17.md +11 -0
- package/release-notes/fabric/0.1.2.md +11 -0
- package/release-notes/fabric/0.1.3.md +25 -0
- package/release-notes/fabric/0.1.4.md +42 -0
- package/release-notes/mcp/0.3.3.md +12 -0
- package/release-notes/mcp/0.3.4.md +36 -0
- package/release-notes/mcp/0.4.0.md +27 -0
- package/release-notes/testkit/1.2.15.md +23 -0
- package/skills/agents.md +25 -0
- package/skills/aws.md +107 -0
- package/skills/cdk.md +141 -0
- package/skills/cicd.md +152 -0
- package/skills/datadog.md +129 -0
- package/skills/debugging.md +148 -0
- package/skills/dns.md +134 -0
- package/skills/dynamodb.md +140 -0
- package/skills/errors.md +142 -0
- package/skills/fabric.md +191 -0
- package/skills/index.md +7 -0
- package/skills/jaypie.md +100 -0
- package/skills/legacy.md +97 -0
- package/skills/logs.md +160 -0
- package/skills/mocks.md +174 -0
- package/skills/models.md +195 -0
- package/skills/releasenotes.md +94 -0
- package/skills/secrets.md +155 -0
- package/skills/services.md +175 -0
- package/skills/style.md +190 -0
- package/skills/tests.md +209 -0
- package/skills/tools.md +127 -0
- package/skills/topics.md +116 -0
- package/skills/variables.md +146 -0
- package/skills/writing.md +153 -0
- package/prompts/Branch_Management.md +0 -34
- package/prompts/Development_Process.md +0 -89
- package/prompts/Jaypie_Agent_Rules.md +0 -110
- package/prompts/Jaypie_Auth0_Express_Mongoose.md +0 -736
- package/prompts/Jaypie_Browser_and_Frontend_Web_Packages.md +0 -18
- package/prompts/Jaypie_CDK_Constructs_and_Patterns.md +0 -430
- package/prompts/Jaypie_CICD_with_GitHub_Actions.md +0 -371
- package/prompts/Jaypie_Commander_CLI_Package.md +0 -166
- package/prompts/Jaypie_Core_Errors_and_Logging.md +0 -39
- package/prompts/Jaypie_DynamoDB_Package.md +0 -774
- package/prompts/Jaypie_Eslint_NPM_Package.md +0 -78
- package/prompts/Jaypie_Express_Package.md +0 -630
- package/prompts/Jaypie_Fabric_Commander.md +0 -411
- package/prompts/Jaypie_Fabric_LLM.md +0 -312
- package/prompts/Jaypie_Fabric_Lambda.md +0 -308
- package/prompts/Jaypie_Fabric_MCP.md +0 -316
- package/prompts/Jaypie_Fabric_Package.md +0 -513
- package/prompts/Jaypie_Fabricator.md +0 -617
- package/prompts/Jaypie_Ideal_Project_Structure.md +0 -78
- package/prompts/Jaypie_Init_CICD_with_GitHub_Actions.md +0 -1186
- package/prompts/Jaypie_Init_Express_on_Lambda.md +0 -115
- package/prompts/Jaypie_Init_Jaypie_CDK_Package.md +0 -35
- package/prompts/Jaypie_Init_Lambda_Package.md +0 -505
- package/prompts/Jaypie_Init_Monorepo_Project.md +0 -44
- package/prompts/Jaypie_Init_Project_Subpackage.md +0 -65
- package/prompts/Jaypie_Legacy_Patterns.md +0 -15
- package/prompts/Jaypie_Llm_Calls.md +0 -449
- package/prompts/Jaypie_Llm_Tools.md +0 -155
- package/prompts/Jaypie_MCP_Package.md +0 -281
- package/prompts/Jaypie_Mocks_and_Testkit.md +0 -137
- package/prompts/Jaypie_Repokit.md +0 -103
- package/prompts/Jaypie_Scrub.md +0 -177
- package/prompts/Jaypie_Streaming.md +0 -467
- package/prompts/Templates_CDK_Subpackage.md +0 -115
- package/prompts/Templates_Express_Subpackage.md +0 -187
- package/prompts/Templates_Project_Monorepo.md +0 -326
- package/prompts/Templates_Project_Subpackage.md +0 -93
- package/prompts/Write_Efficient_Prompt_Guides.md +0 -48
- package/prompts/Write_and_Maintain_Engaging_Readme.md +0 -67
package/skills/models.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Data models and type definitions
|
|
3
|
+
related: fabric, dynamodb, services
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Data Models
|
|
7
|
+
|
|
8
|
+
Patterns for defining data models and types in Jaypie applications.
|
|
9
|
+
|
|
10
|
+
## TypeScript Interfaces
|
|
11
|
+
|
|
12
|
+
### Basic Model
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
export interface User {
|
|
16
|
+
id: string;
|
|
17
|
+
email: string;
|
|
18
|
+
name: string;
|
|
19
|
+
role: "user" | "admin";
|
|
20
|
+
createdAt: string;
|
|
21
|
+
updatedAt: string;
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Input/Output Types
|
|
26
|
+
|
|
27
|
+
Separate types for different operations:
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// Create input - required fields only
|
|
31
|
+
export interface UserCreateInput {
|
|
32
|
+
email: string;
|
|
33
|
+
name: string;
|
|
34
|
+
role?: "user" | "admin";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Update input - all optional
|
|
38
|
+
export interface UserUpdateInput {
|
|
39
|
+
name?: string;
|
|
40
|
+
role?: "user" | "admin";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Response type - full model
|
|
44
|
+
export interface UserResponse extends User {
|
|
45
|
+
// Additional computed fields
|
|
46
|
+
displayName: string;
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Fabric Service Models
|
|
51
|
+
|
|
52
|
+
Define input schemas inline with fabric services:
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { fabricService } from "@jaypie/fabric";
|
|
56
|
+
|
|
57
|
+
const createUser = fabricService({
|
|
58
|
+
alias: "user_create",
|
|
59
|
+
description: "Create a new user",
|
|
60
|
+
input: {
|
|
61
|
+
email: {
|
|
62
|
+
type: String,
|
|
63
|
+
required: true,
|
|
64
|
+
description: "User email address",
|
|
65
|
+
},
|
|
66
|
+
name: {
|
|
67
|
+
type: String,
|
|
68
|
+
required: true,
|
|
69
|
+
description: "User display name",
|
|
70
|
+
},
|
|
71
|
+
role: {
|
|
72
|
+
type: ["user", "admin"] as const,
|
|
73
|
+
required: false,
|
|
74
|
+
description: "User role (defaults to user)",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
service: async ({ email, name, role }) => {
|
|
78
|
+
// Input is validated and typed
|
|
79
|
+
return createUserInDatabase({ email, name, role: role || "user" });
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Validation Patterns
|
|
85
|
+
|
|
86
|
+
### Zod Schemas
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import { z } from "zod";
|
|
90
|
+
|
|
91
|
+
export const userSchema = z.object({
|
|
92
|
+
email: z.string().email(),
|
|
93
|
+
name: z.string().min(1).max(100),
|
|
94
|
+
role: z.enum(["user", "admin"]).default("user"),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
export type User = z.infer<typeof userSchema>;
|
|
98
|
+
|
|
99
|
+
// Validate input
|
|
100
|
+
const validated = userSchema.parse(input);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Custom Validators
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
const emailValidator = (email: string): boolean => {
|
|
107
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const service = fabricService({
|
|
111
|
+
alias: "user_create",
|
|
112
|
+
input: {
|
|
113
|
+
email: { type: String, required: true },
|
|
114
|
+
},
|
|
115
|
+
service: async ({ email }) => {
|
|
116
|
+
if (!emailValidator(email)) {
|
|
117
|
+
throw new BadRequestError("Invalid email format");
|
|
118
|
+
}
|
|
119
|
+
// ...
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## DynamoDB Models
|
|
125
|
+
|
|
126
|
+
See `skill("dynamodb")` for DynamoDB-specific patterns.
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
// Single-table design types
|
|
130
|
+
export interface DynamoItem {
|
|
131
|
+
pk: string; // Partition key
|
|
132
|
+
sk: string; // Sort key
|
|
133
|
+
type: string;
|
|
134
|
+
data: Record<string, unknown>;
|
|
135
|
+
createdAt: string;
|
|
136
|
+
updatedAt: string;
|
|
137
|
+
ttl?: number;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface UserItem extends DynamoItem {
|
|
141
|
+
type: "USER";
|
|
142
|
+
data: {
|
|
143
|
+
email: string;
|
|
144
|
+
name: string;
|
|
145
|
+
role: "user" | "admin";
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## API Response Models
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// Standard response wrapper
|
|
154
|
+
export interface ApiResponse<T> {
|
|
155
|
+
success: boolean;
|
|
156
|
+
data?: T;
|
|
157
|
+
error?: {
|
|
158
|
+
code: string;
|
|
159
|
+
message: string;
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Paginated response
|
|
164
|
+
export interface PaginatedResponse<T> {
|
|
165
|
+
items: T[];
|
|
166
|
+
nextToken?: string;
|
|
167
|
+
total?: number;
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Best Practices
|
|
172
|
+
|
|
173
|
+
1. **Export types** - Make types available for consumers
|
|
174
|
+
2. **Separate concerns** - Input, output, and storage types differ
|
|
175
|
+
3. **Use const assertions** - `as const` for literal types
|
|
176
|
+
4. **Document fields** - Add JSDoc comments for complex types
|
|
177
|
+
5. **Prefer interfaces** - Use `interface` over `type` for objects
|
|
178
|
+
|
|
179
|
+
## Type Documentation
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
/**
|
|
183
|
+
* Represents a user in the system.
|
|
184
|
+
* @property id - Unique identifier (UUID)
|
|
185
|
+
* @property email - Validated email address
|
|
186
|
+
* @property role - Access level for permissions
|
|
187
|
+
*/
|
|
188
|
+
export interface User {
|
|
189
|
+
id: string;
|
|
190
|
+
email: string;
|
|
191
|
+
name: string;
|
|
192
|
+
role: "user" | "admin";
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Version history and release notes
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Release Notes
|
|
6
|
+
|
|
7
|
+
How to access and write release notes for Jaypie packages.
|
|
8
|
+
|
|
9
|
+
## MCP Tools
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
mcp__jaypie__list_release_notes()
|
|
13
|
+
mcp__jaypie__list_release_notes(package: "mcp")
|
|
14
|
+
mcp__jaypie__list_release_notes(package: "jaypie", since_version: "1.0.0")
|
|
15
|
+
mcp__jaypie__read_release_note(package: "mcp", version: "0.3.4")
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Directory Structure
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
packages/mcp/release-notes/
|
|
22
|
+
├── jaypie/
|
|
23
|
+
│ └── 1.2.3.md
|
|
24
|
+
├── mcp/
|
|
25
|
+
│ └── 0.3.4.md
|
|
26
|
+
└── testkit/
|
|
27
|
+
└── 2.0.0.md
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## File Format
|
|
31
|
+
|
|
32
|
+
Create `release-notes/<package>/<version>.md`:
|
|
33
|
+
|
|
34
|
+
```yaml
|
|
35
|
+
---
|
|
36
|
+
version: 0.3.4
|
|
37
|
+
date: 2025-01-20
|
|
38
|
+
summary: Brief one-line summary for listing
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Changes
|
|
42
|
+
|
|
43
|
+
### New Features
|
|
44
|
+
|
|
45
|
+
- Feature description
|
|
46
|
+
|
|
47
|
+
### Bug Fixes
|
|
48
|
+
|
|
49
|
+
- Fix description
|
|
50
|
+
|
|
51
|
+
### Breaking Changes
|
|
52
|
+
|
|
53
|
+
- Breaking change description
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## When to Add
|
|
57
|
+
|
|
58
|
+
Add release notes when:
|
|
59
|
+
- Bumping package version (required)
|
|
60
|
+
- Merging significant features
|
|
61
|
+
- Making breaking changes
|
|
62
|
+
- Fixing notable bugs
|
|
63
|
+
|
|
64
|
+
## Writing Guidelines
|
|
65
|
+
|
|
66
|
+
1. **Summary** - One line, present tense, describes the release
|
|
67
|
+
2. **Changes** - Group by type: New Features, Bug Fixes, Breaking Changes
|
|
68
|
+
3. **Bullets** - Start with verb (Add, Fix, Update, Remove)
|
|
69
|
+
4. **Links** - Reference issues/PRs when relevant
|
|
70
|
+
|
|
71
|
+
## Example
|
|
72
|
+
|
|
73
|
+
```markdown
|
|
74
|
+
---
|
|
75
|
+
version: 1.2.0
|
|
76
|
+
date: 2025-01-15
|
|
77
|
+
summary: Add streaming support for LLM providers
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Changes
|
|
81
|
+
|
|
82
|
+
### New Features
|
|
83
|
+
|
|
84
|
+
- Add streaming response support for Anthropic and OpenAI
|
|
85
|
+
- Add `onChunk` callback for real-time token processing
|
|
86
|
+
|
|
87
|
+
### Bug Fixes
|
|
88
|
+
|
|
89
|
+
- Fix timeout handling in concurrent requests
|
|
90
|
+
|
|
91
|
+
### Breaking Changes
|
|
92
|
+
|
|
93
|
+
- Remove deprecated `legacyMode` option
|
|
94
|
+
```
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Secret management with AWS Secrets Manager
|
|
3
|
+
related: aws, cdk, variables
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Secret Management
|
|
7
|
+
|
|
8
|
+
Jaypie uses AWS Secrets Manager for secure credential storage.
|
|
9
|
+
|
|
10
|
+
## Basic Usage
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import { getSecret } from "jaypie";
|
|
14
|
+
|
|
15
|
+
const apiKey = await getSecret("my-api-key");
|
|
16
|
+
const dbUri = await getSecret("mongodb-connection-string");
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Environment Variables
|
|
20
|
+
|
|
21
|
+
Reference secrets via environment variables in CDK:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
const handler = new JaypieLambda(this, "Handler", {
|
|
25
|
+
environment: {
|
|
26
|
+
SECRET_MONGODB_URI: "mongodb-connection-string",
|
|
27
|
+
SECRET_API_KEY: "third-party-api-key",
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
In code:
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
const secretName = process.env.SECRET_MONGODB_URI;
|
|
36
|
+
const mongoUri = await getSecret(secretName);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Creating Secrets
|
|
40
|
+
|
|
41
|
+
### Via CDK
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { Secret } from "aws-cdk-lib/aws-secretsmanager";
|
|
45
|
+
|
|
46
|
+
const secret = new Secret(this, "ApiKey", {
|
|
47
|
+
secretName: `${projectKey}/api-key`,
|
|
48
|
+
description: "Third-party API key",
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Grant read access
|
|
52
|
+
secret.grantRead(lambdaFunction);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Via AWS CLI
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
aws secretsmanager create-secret \
|
|
59
|
+
--name "my-project/api-key" \
|
|
60
|
+
--secret-string "sk_live_abc123"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Secret Naming Convention
|
|
64
|
+
|
|
65
|
+
Use project-prefixed names:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
{project-key}/{secret-name}
|
|
69
|
+
|
|
70
|
+
Examples:
|
|
71
|
+
- my-api/mongodb-uri
|
|
72
|
+
- my-api/stripe-key
|
|
73
|
+
- my-api/auth0-secret
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## JSON Secrets
|
|
77
|
+
|
|
78
|
+
Store structured data:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
aws secretsmanager create-secret \
|
|
82
|
+
--name "my-project/db-credentials" \
|
|
83
|
+
--secret-string '{"username":"admin","password":"secret123"}'
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Retrieve in code:
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
const credentialsJson = await getSecret("my-project/db-credentials");
|
|
90
|
+
const credentials = JSON.parse(credentialsJson);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Caching
|
|
94
|
+
|
|
95
|
+
Secrets are cached by default to reduce API calls:
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
// First call: fetches from Secrets Manager
|
|
99
|
+
const key1 = await getSecret("api-key");
|
|
100
|
+
|
|
101
|
+
// Second call: returns cached value
|
|
102
|
+
const key2 = await getSecret("api-key");
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Cache is scoped to Lambda execution context (warm starts reuse cache).
|
|
106
|
+
|
|
107
|
+
## Rotation
|
|
108
|
+
|
|
109
|
+
Configure automatic rotation for supported secrets:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
const secret = new Secret(this, "DbPassword", {
|
|
113
|
+
secretName: "my-project/db-password",
|
|
114
|
+
generateSecretString: {
|
|
115
|
+
excludePunctuation: true,
|
|
116
|
+
passwordLength: 32,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
secret.addRotationSchedule("Rotation", {
|
|
121
|
+
automaticallyAfter: Duration.days(30),
|
|
122
|
+
rotationLambda: rotationFunction,
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Local Development
|
|
127
|
+
|
|
128
|
+
For local development, use environment variables:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# .env.local (not committed)
|
|
132
|
+
MONGODB_URI=mongodb://localhost:27017/dev
|
|
133
|
+
API_KEY=test_key_123
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
In code, check for direct value first:
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
const mongoUri = process.env.MONGODB_URI || await getSecret(process.env.SECRET_MONGODB_URI);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## IAM Permissions
|
|
143
|
+
|
|
144
|
+
Lambda needs `secretsmanager:GetSecretValue`:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
secret.grantRead(lambdaFunction);
|
|
148
|
+
|
|
149
|
+
// Or via policy
|
|
150
|
+
lambdaFunction.addToRolePolicy(new PolicyStatement({
|
|
151
|
+
actions: ["secretsmanager:GetSecretValue"],
|
|
152
|
+
resources: [secret.secretArn],
|
|
153
|
+
}));
|
|
154
|
+
```
|
|
155
|
+
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Service layer patterns and architecture
|
|
3
|
+
related: fabric, models, tests
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Service Layer Patterns
|
|
7
|
+
|
|
8
|
+
Organizing business logic in Jaypie applications.
|
|
9
|
+
|
|
10
|
+
## Service Structure
|
|
11
|
+
|
|
12
|
+
Keep business logic in service modules:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
src/
|
|
16
|
+
├── handlers/ # Lambda/Express handlers
|
|
17
|
+
├── services/ # Business logic
|
|
18
|
+
│ ├── user.ts
|
|
19
|
+
│ └── order.ts
|
|
20
|
+
├── models/ # Data models
|
|
21
|
+
└── utils/ # Utilities
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Basic Service Pattern
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// services/user.ts
|
|
28
|
+
import { log, NotFoundError, BadRequestError } from "jaypie";
|
|
29
|
+
import { User } from "../models/user.js";
|
|
30
|
+
|
|
31
|
+
export async function getUser(userId: string) {
|
|
32
|
+
log.debug("Getting user", { userId });
|
|
33
|
+
|
|
34
|
+
const user = await User.findById(userId);
|
|
35
|
+
if (!user) {
|
|
36
|
+
throw new NotFoundError(`User ${userId} not found`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return user;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function createUser(input: UserCreateInput) {
|
|
43
|
+
log.info("Creating user", { email: input.email });
|
|
44
|
+
|
|
45
|
+
const existing = await User.findOne({ email: input.email });
|
|
46
|
+
if (existing) {
|
|
47
|
+
throw new BadRequestError("Email already registered");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return User.create(input);
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Handler Integration
|
|
55
|
+
|
|
56
|
+
Handlers call services:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// handlers/user.ts
|
|
60
|
+
import { lambdaHandler } from "@jaypie/lambda";
|
|
61
|
+
import { getUser, createUser } from "../services/user.js";
|
|
62
|
+
|
|
63
|
+
export const getUserHandler = lambdaHandler(async (event) => {
|
|
64
|
+
const { userId } = event.pathParameters;
|
|
65
|
+
return getUser(userId);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export const createUserHandler = lambdaHandler(async (event) => {
|
|
69
|
+
const input = JSON.parse(event.body);
|
|
70
|
+
return createUser(input);
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Service Dependencies
|
|
75
|
+
|
|
76
|
+
Inject dependencies for testability:
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// services/notification.ts
|
|
80
|
+
import { log } from "jaypie";
|
|
81
|
+
|
|
82
|
+
export interface NotificationService {
|
|
83
|
+
sendEmail(to: string, subject: string, body: string): Promise<void>;
|
|
84
|
+
sendSms(to: string, message: string): Promise<void>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function createNotificationService(
|
|
88
|
+
emailClient: EmailClient,
|
|
89
|
+
smsClient: SmsClient
|
|
90
|
+
): NotificationService {
|
|
91
|
+
return {
|
|
92
|
+
async sendEmail(to, subject, body) {
|
|
93
|
+
log.info("Sending email", { to, subject });
|
|
94
|
+
await emailClient.send({ to, subject, body });
|
|
95
|
+
},
|
|
96
|
+
async sendSms(to, message) {
|
|
97
|
+
log.info("Sending SMS", { to });
|
|
98
|
+
await smsClient.send({ to, message });
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Transaction Patterns
|
|
105
|
+
|
|
106
|
+
For DynamoDB operations that must succeed together, use TransactWriteItems:
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { TransactWriteItemsCommand } from "@aws-sdk/client-dynamodb";
|
|
110
|
+
|
|
111
|
+
export async function transferFunds(fromId: string, toId: string, amount: number) {
|
|
112
|
+
const from = await getAccount(fromId);
|
|
113
|
+
|
|
114
|
+
if (from.balance < amount) {
|
|
115
|
+
throw new BadRequestError("Insufficient funds");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const command = new TransactWriteItemsCommand({
|
|
119
|
+
TransactItems: [
|
|
120
|
+
{
|
|
121
|
+
Update: {
|
|
122
|
+
TableName: TABLE_NAME,
|
|
123
|
+
Key: { pk: { S: `ACCOUNT#${fromId}` }, sk: { S: "BALANCE" } },
|
|
124
|
+
UpdateExpression: "SET balance = balance - :amount",
|
|
125
|
+
ExpressionAttributeValues: { ":amount": { N: String(amount) } },
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
Update: {
|
|
130
|
+
TableName: TABLE_NAME,
|
|
131
|
+
Key: { pk: { S: `ACCOUNT#${toId}` }, sk: { S: "BALANCE" } },
|
|
132
|
+
UpdateExpression: "SET balance = balance + :amount",
|
|
133
|
+
ExpressionAttributeValues: { ":amount": { N: String(amount) } },
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
await dynamoClient.send(command);
|
|
140
|
+
log.info("Transfer completed", { fromId, toId, amount });
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Service Testing
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
148
|
+
import { getUser } from "./user.js";
|
|
149
|
+
import { User } from "../models/user.js";
|
|
150
|
+
|
|
151
|
+
vi.mock("../models/user.js");
|
|
152
|
+
|
|
153
|
+
describe("getUser", () => {
|
|
154
|
+
beforeEach(() => {
|
|
155
|
+
vi.clearAllMocks();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("returns user when found", async () => {
|
|
159
|
+
const mockUser = { id: "123", name: "John" };
|
|
160
|
+
vi.mocked(User.findById).mockResolvedValue(mockUser);
|
|
161
|
+
|
|
162
|
+
const result = await getUser("123");
|
|
163
|
+
|
|
164
|
+
expect(result).toEqual(mockUser);
|
|
165
|
+
expect(User.findById).toHaveBeenCalledWith("123");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("throws NotFoundError when missing", async () => {
|
|
169
|
+
vi.mocked(User.findById).mockResolvedValue(null);
|
|
170
|
+
|
|
171
|
+
await expect(getUser("123")).rejects.toThrow(NotFoundError);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|