@jaypie/mcp 0.2.2 → 0.2.4
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/index.js +208 -3
- package/dist/index.js.map +1 -1
- package/dist/llm.d.ts +41 -0
- package/package.json +3 -2
- package/prompts/Jaypie_CDK_Constructs_and_Patterns.md +51 -0
- package/prompts/Jaypie_DynamoDB_Package.md +547 -0
- package/prompts/Jaypie_Express_Package.md +91 -0
- package/prompts/Jaypie_Init_Lambda_Package.md +134 -1
- package/prompts/Jaypie_Llm_Calls.md +339 -3
- package/prompts/Jaypie_Llm_Tools.md +43 -12
- package/prompts/Jaypie_Vocabulary_Commander.md +411 -0
- package/prompts/Jaypie_Vocabulary_LLM.md +312 -0
- package/prompts/Jaypie_Vocabulary_Lambda.md +310 -0
- package/prompts/Jaypie_Vocabulary_MCP.md +296 -0
- package/prompts/Jaypie_Vocabulary_Package.md +141 -183
package/dist/llm.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM debugging utilities for inspecting raw provider responses
|
|
3
|
+
*/
|
|
4
|
+
export type LlmProvider = "anthropic" | "gemini" | "openai" | "openrouter";
|
|
5
|
+
export interface LlmDebugCallParams {
|
|
6
|
+
provider: LlmProvider;
|
|
7
|
+
model?: string;
|
|
8
|
+
message: string;
|
|
9
|
+
}
|
|
10
|
+
export interface LlmDebugCallResult {
|
|
11
|
+
success: boolean;
|
|
12
|
+
provider: string;
|
|
13
|
+
model: string;
|
|
14
|
+
content?: string;
|
|
15
|
+
reasoning?: string[];
|
|
16
|
+
reasoningTokens?: number;
|
|
17
|
+
history?: unknown[];
|
|
18
|
+
rawResponses?: unknown[];
|
|
19
|
+
usage?: unknown[];
|
|
20
|
+
error?: string;
|
|
21
|
+
}
|
|
22
|
+
interface Logger {
|
|
23
|
+
info: (message: string, ...args: unknown[]) => void;
|
|
24
|
+
error: (message: string, ...args: unknown[]) => void;
|
|
25
|
+
}
|
|
26
|
+
export declare const REASONING_MODELS: Record<string, string>;
|
|
27
|
+
/**
|
|
28
|
+
* Make a debug LLM call and return the raw response data for inspection
|
|
29
|
+
*/
|
|
30
|
+
export declare function debugLlmCall(params: LlmDebugCallParams, log: Logger): Promise<LlmDebugCallResult>;
|
|
31
|
+
/**
|
|
32
|
+
* List available providers and their default/reasoning models
|
|
33
|
+
*/
|
|
34
|
+
export declare function listLlmProviders(): {
|
|
35
|
+
providers: Array<{
|
|
36
|
+
name: LlmProvider;
|
|
37
|
+
defaultModel: string;
|
|
38
|
+
reasoningModels: string[];
|
|
39
|
+
}>;
|
|
40
|
+
};
|
|
41
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jaypie/mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Jaypie MCP",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"prompts"
|
|
26
26
|
],
|
|
27
27
|
"scripts": {
|
|
28
|
-
"build": "rollup --config",
|
|
28
|
+
"build": "rollup --config && chmod +x dist/index.js",
|
|
29
29
|
"format": "eslint . --fix",
|
|
30
30
|
"lint": "eslint .",
|
|
31
31
|
"prepare": "npm run build",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"typecheck": "tsc --noEmit"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
+
"@jaypie/llm": "^1.2.2",
|
|
37
38
|
"@modelcontextprotocol/sdk": "^1.17.0",
|
|
38
39
|
"commander": "^14.0.0",
|
|
39
40
|
"gray-matter": "^4.0.3",
|
|
@@ -100,6 +100,57 @@ new JaypieExpressLambda(this, "ExpressApp", {
|
|
|
100
100
|
|
|
101
101
|
Preconfigured with API-optimized timeouts and role tags.
|
|
102
102
|
|
|
103
|
+
### Streaming Lambda Functions
|
|
104
|
+
|
|
105
|
+
Enable Lambda Response Streaming via Function URLs for real-time SSE responses:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { FunctionUrlAuthType, InvokeMode } from "aws-cdk-lib/aws-lambda";
|
|
109
|
+
|
|
110
|
+
const streamingLambda = new JaypieLambda(this, "StreamingFunction", {
|
|
111
|
+
code: "dist",
|
|
112
|
+
handler: "stream.handler",
|
|
113
|
+
timeout: Duration.minutes(5), // Longer timeout for streaming
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Add Function URL with streaming enabled
|
|
117
|
+
const functionUrl = streamingLambda.addFunctionUrl({
|
|
118
|
+
authType: FunctionUrlAuthType.NONE, // Public access
|
|
119
|
+
// authType: FunctionUrlAuthType.AWS_IAM, // IAM authentication
|
|
120
|
+
invokeMode: InvokeMode.RESPONSE_STREAM, // Enable streaming
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Output the URL
|
|
124
|
+
new cdk.CfnOutput(this, "StreamingUrl", {
|
|
125
|
+
value: functionUrl.url,
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Features:
|
|
130
|
+
- `RESPONSE_STREAM` invoke mode enables real-time streaming
|
|
131
|
+
- Works with `lambdaStreamHandler` from `@jaypie/lambda`
|
|
132
|
+
- Use `FunctionUrlAuthType.AWS_IAM` for authenticated endpoints
|
|
133
|
+
- Combine with API Gateway for custom domains via `JaypieApiGateway`
|
|
134
|
+
|
|
135
|
+
For Express-based streaming with custom domains:
|
|
136
|
+
```typescript
|
|
137
|
+
// Express app with streaming routes
|
|
138
|
+
const expressLambda = new JaypieExpressLambda(this, "ExpressStream", {
|
|
139
|
+
code: "dist",
|
|
140
|
+
handler: "app.handler",
|
|
141
|
+
timeout: Duration.minutes(5),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// API Gateway handles the domain routing
|
|
145
|
+
new JaypieApiGateway(this, "Api", {
|
|
146
|
+
handler: expressLambda,
|
|
147
|
+
host: "api.example.com",
|
|
148
|
+
zone: "example.com",
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Note: API Gateway has a 29-second timeout limit. For longer streaming operations, use Function URLs directly.
|
|
153
|
+
|
|
103
154
|
### Stack Types
|
|
104
155
|
|
|
105
156
|
Use specialized stacks for different purposes:
|
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Complete guide to @jaypie/dynamodb single-table design utilities including GSI patterns, key builders, entity operations, and query functions
|
|
3
|
+
globs: packages/dynamodb/**
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Jaypie DynamoDB Package
|
|
7
|
+
|
|
8
|
+
Jaypie provides DynamoDB single-table design utilities through `@jaypie/dynamodb`. The package implements a five-GSI pattern for hierarchical data with named access patterns (not gsi1, gsi2, etc.).
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @jaypie/dynamodb
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Core Concepts
|
|
17
|
+
|
|
18
|
+
### Single-Table Design
|
|
19
|
+
|
|
20
|
+
All entities share a single DynamoDB table with:
|
|
21
|
+
- **Primary Key**: `model` (partition) + `id` (sort)
|
|
22
|
+
- **Five GSIs**: All use `sequence` as sort key for chronological ordering
|
|
23
|
+
|
|
24
|
+
### Organizational Unit (OU)
|
|
25
|
+
|
|
26
|
+
The `ou` field creates hierarchical relationships:
|
|
27
|
+
- Root entities: `ou = "@"` (APEX constant)
|
|
28
|
+
- Child entities: `ou = "{parent.model}#{parent.id}"`
|
|
29
|
+
|
|
30
|
+
### GSI Pattern
|
|
31
|
+
|
|
32
|
+
| GSI Name | Partition Key | Use Case |
|
|
33
|
+
|----------|---------------|----------|
|
|
34
|
+
| `indexOu` | `{ou}#{model}` | List all entities under a parent |
|
|
35
|
+
| `indexAlias` | `{ou}#{model}#{alias}` | Human-friendly slug lookup |
|
|
36
|
+
| `indexClass` | `{ou}#{model}#{class}` | Category filtering |
|
|
37
|
+
| `indexType` | `{ou}#{model}#{type}` | Type filtering |
|
|
38
|
+
| `indexXid` | `{ou}#{model}#{xid}` | External system ID lookup |
|
|
39
|
+
|
|
40
|
+
## Client Initialization
|
|
41
|
+
|
|
42
|
+
Initialize once at application startup:
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { initClient } from "@jaypie/dynamodb";
|
|
46
|
+
|
|
47
|
+
initClient({
|
|
48
|
+
tableName: process.env.DYNAMODB_TABLE_NAME,
|
|
49
|
+
region: process.env.AWS_REGION, // Optional, defaults to us-east-1
|
|
50
|
+
endpoint: process.env.DYNAMODB_ENDPOINT, // Optional, for local dev
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
For local development with DynamoDB Local:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
initClient({
|
|
58
|
+
tableName: "local-table",
|
|
59
|
+
endpoint: "http://127.0.0.1:8100",
|
|
60
|
+
// Credentials auto-detected for localhost endpoints
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## FabricEntity Interface
|
|
65
|
+
|
|
66
|
+
All entities must implement `FabricEntity`:
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import type { FabricEntity } from "@jaypie/dynamodb";
|
|
70
|
+
|
|
71
|
+
interface MyRecord extends FabricEntity {
|
|
72
|
+
// Primary Key (required)
|
|
73
|
+
model: string; // e.g., "record"
|
|
74
|
+
id: string; // UUID
|
|
75
|
+
|
|
76
|
+
// Required fields
|
|
77
|
+
name: string;
|
|
78
|
+
ou: string; // APEX or hierarchical
|
|
79
|
+
sequence: number; // Date.now()
|
|
80
|
+
|
|
81
|
+
// Timestamps (ISO 8601)
|
|
82
|
+
createdAt: string;
|
|
83
|
+
updatedAt: string;
|
|
84
|
+
archivedAt?: string; // Set by archiveEntity
|
|
85
|
+
deletedAt?: string; // Set by deleteEntity
|
|
86
|
+
|
|
87
|
+
// Optional - trigger GSI population
|
|
88
|
+
alias?: string; // Human-friendly slug
|
|
89
|
+
class?: string; // Category
|
|
90
|
+
type?: string; // Type classification
|
|
91
|
+
xid?: string; // External ID
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Entity Operations
|
|
96
|
+
|
|
97
|
+
### Creating Entities
|
|
98
|
+
|
|
99
|
+
Use `putEntity` to create or replace entities. GSI keys are auto-populated:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
import { APEX, putEntity } from "@jaypie/dynamodb";
|
|
103
|
+
|
|
104
|
+
const now = new Date().toISOString();
|
|
105
|
+
|
|
106
|
+
const record = await putEntity({
|
|
107
|
+
entity: {
|
|
108
|
+
model: "record",
|
|
109
|
+
id: crypto.randomUUID(),
|
|
110
|
+
name: "Daily Log",
|
|
111
|
+
ou: APEX,
|
|
112
|
+
sequence: Date.now(),
|
|
113
|
+
alias: "2026-01-07", // Optional
|
|
114
|
+
class: "memory", // Optional
|
|
115
|
+
createdAt: now,
|
|
116
|
+
updatedAt: now,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Result includes auto-populated indexes:
|
|
121
|
+
// indexOu: "@#record"
|
|
122
|
+
// indexAlias: "@#record#2026-01-07"
|
|
123
|
+
// indexClass: "@#record#memory"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Getting Entities
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
import { getEntity } from "@jaypie/dynamodb";
|
|
130
|
+
|
|
131
|
+
const record = await getEntity({ id: "123", model: "record" });
|
|
132
|
+
// Returns entity or null
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Updating Entities
|
|
136
|
+
|
|
137
|
+
Use `updateEntity` to update. Automatically sets `updatedAt` and re-indexes:
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
import { updateEntity } from "@jaypie/dynamodb";
|
|
141
|
+
|
|
142
|
+
const updated = await updateEntity({
|
|
143
|
+
entity: {
|
|
144
|
+
...existingRecord,
|
|
145
|
+
name: "Updated Name",
|
|
146
|
+
alias: "new-alias",
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
// updatedAt is set automatically
|
|
150
|
+
// indexAlias is re-calculated
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Soft Delete
|
|
154
|
+
|
|
155
|
+
Use `deleteEntity` for soft delete (sets `deletedAt`):
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
import { deleteEntity } from "@jaypie/dynamodb";
|
|
159
|
+
|
|
160
|
+
await deleteEntity({ id: "123", model: "record" });
|
|
161
|
+
// Sets deletedAt and updatedAt timestamps
|
|
162
|
+
// Entity excluded from queries by default
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Archive
|
|
166
|
+
|
|
167
|
+
Use `archiveEntity` for archiving (sets `archivedAt`):
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import { archiveEntity } from "@jaypie/dynamodb";
|
|
171
|
+
|
|
172
|
+
await archiveEntity({ id: "123", model: "record" });
|
|
173
|
+
// Sets archivedAt and updatedAt timestamps
|
|
174
|
+
// Entity excluded from queries by default
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Hard Delete
|
|
178
|
+
|
|
179
|
+
Use `destroyEntity` to permanently remove:
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
import { destroyEntity } from "@jaypie/dynamodb";
|
|
183
|
+
|
|
184
|
+
await destroyEntity({ id: "123", model: "record" });
|
|
185
|
+
// Permanently removes from table
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Hierarchical Entities
|
|
189
|
+
|
|
190
|
+
Use `calculateOu` to derive OU from parent:
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
import { calculateOu, putEntity, queryByOu } from "@jaypie/dynamodb";
|
|
194
|
+
|
|
195
|
+
// Parent reference
|
|
196
|
+
const chat = { model: "chat", id: "abc-123" };
|
|
197
|
+
|
|
198
|
+
// Calculate child OU
|
|
199
|
+
const messageOu = calculateOu(chat); // "chat#abc-123"
|
|
200
|
+
|
|
201
|
+
// Create child entity
|
|
202
|
+
const message = await putEntity({
|
|
203
|
+
entity: {
|
|
204
|
+
model: "message",
|
|
205
|
+
id: crypto.randomUUID(),
|
|
206
|
+
name: "First message",
|
|
207
|
+
ou: messageOu,
|
|
208
|
+
sequence: Date.now(),
|
|
209
|
+
createdAt: now,
|
|
210
|
+
updatedAt: now,
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
// indexOu: "chat#abc-123#message"
|
|
214
|
+
|
|
215
|
+
// Query all messages in chat
|
|
216
|
+
const { items } = await queryByOu({ model: "message", ou: messageOu });
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Query Functions
|
|
220
|
+
|
|
221
|
+
All queries use object parameters and filter soft-deleted and archived records by default.
|
|
222
|
+
|
|
223
|
+
### queryByOu - List by Parent
|
|
224
|
+
|
|
225
|
+
List all entities of a model under a parent:
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
import { APEX, queryByOu } from "@jaypie/dynamodb";
|
|
229
|
+
|
|
230
|
+
// Root-level records
|
|
231
|
+
const { items, lastEvaluatedKey } = await queryByOu({
|
|
232
|
+
model: "record",
|
|
233
|
+
ou: APEX,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Messages under a chat
|
|
237
|
+
const { items: messages } = await queryByOu({
|
|
238
|
+
model: "message",
|
|
239
|
+
ou: "chat#abc-123",
|
|
240
|
+
});
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### queryByAlias - Human-Friendly Lookup
|
|
244
|
+
|
|
245
|
+
Single entity lookup by slug:
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
import { APEX, queryByAlias } from "@jaypie/dynamodb";
|
|
249
|
+
|
|
250
|
+
const record = await queryByAlias({
|
|
251
|
+
alias: "2026-01-07",
|
|
252
|
+
model: "record",
|
|
253
|
+
ou: APEX,
|
|
254
|
+
});
|
|
255
|
+
// Returns entity or null
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### queryByClass / queryByType - Category Filtering
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
import { APEX, queryByClass, queryByType } from "@jaypie/dynamodb";
|
|
262
|
+
|
|
263
|
+
// All memory records
|
|
264
|
+
const { items } = await queryByClass({
|
|
265
|
+
model: "record",
|
|
266
|
+
ou: APEX,
|
|
267
|
+
recordClass: "memory",
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// All note-type records
|
|
271
|
+
const { items: notes } = await queryByType({
|
|
272
|
+
model: "record",
|
|
273
|
+
ou: APEX,
|
|
274
|
+
type: "note",
|
|
275
|
+
});
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### queryByXid - External ID Lookup
|
|
279
|
+
|
|
280
|
+
Single entity lookup by external system ID:
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
import { APEX, queryByXid } from "@jaypie/dynamodb";
|
|
284
|
+
|
|
285
|
+
const record = await queryByXid({
|
|
286
|
+
model: "record",
|
|
287
|
+
ou: APEX,
|
|
288
|
+
xid: "ext-12345",
|
|
289
|
+
});
|
|
290
|
+
// Returns entity or null
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Query Options
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
import type { BaseQueryOptions } from "@jaypie/dynamodb";
|
|
297
|
+
|
|
298
|
+
const result = await queryByOu({
|
|
299
|
+
model: "record",
|
|
300
|
+
ou: APEX,
|
|
301
|
+
// BaseQueryOptions:
|
|
302
|
+
limit: 25, // Max items to return
|
|
303
|
+
ascending: true, // Oldest first (default: false = newest first)
|
|
304
|
+
includeDeleted: true, // Include soft-deleted records
|
|
305
|
+
includeArchived: true, // Include archived records
|
|
306
|
+
startKey: lastEvaluatedKey, // Pagination cursor
|
|
307
|
+
});
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Pagination
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
import { APEX, queryByOu } from "@jaypie/dynamodb";
|
|
314
|
+
|
|
315
|
+
let startKey: Record<string, unknown> | undefined;
|
|
316
|
+
const allItems: FabricEntity[] = [];
|
|
317
|
+
|
|
318
|
+
do {
|
|
319
|
+
const { items, lastEvaluatedKey } = await queryByOu({
|
|
320
|
+
model: "record",
|
|
321
|
+
ou: APEX,
|
|
322
|
+
limit: 100,
|
|
323
|
+
startKey,
|
|
324
|
+
});
|
|
325
|
+
allItems.push(...items);
|
|
326
|
+
startKey = lastEvaluatedKey;
|
|
327
|
+
} while (startKey);
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Key Builder Functions
|
|
331
|
+
|
|
332
|
+
Use `indexEntity` to auto-populate GSI keys on an entity:
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
import { indexEntity } from "@jaypie/dynamodb";
|
|
336
|
+
|
|
337
|
+
const indexed = indexEntity({
|
|
338
|
+
model: "record",
|
|
339
|
+
id: "123",
|
|
340
|
+
ou: "@",
|
|
341
|
+
alias: "my-alias",
|
|
342
|
+
// ...
|
|
343
|
+
});
|
|
344
|
+
// indexOu: "@#record"
|
|
345
|
+
// indexAlias: "@#record#my-alias"
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Use individual key builders for manual key construction:
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
import {
|
|
352
|
+
buildIndexOu,
|
|
353
|
+
buildIndexAlias,
|
|
354
|
+
buildIndexClass,
|
|
355
|
+
buildIndexType,
|
|
356
|
+
buildIndexXid,
|
|
357
|
+
} from "@jaypie/dynamodb";
|
|
358
|
+
|
|
359
|
+
buildIndexOu("@", "record"); // "@#record"
|
|
360
|
+
buildIndexAlias("@", "record", "my-alias"); // "@#record#my-alias"
|
|
361
|
+
buildIndexClass("@", "record", "memory"); // "@#record#memory"
|
|
362
|
+
buildIndexType("@", "record", "note"); // "@#record#note"
|
|
363
|
+
buildIndexXid("@", "record", "ext-123"); // "@#record#ext-123"
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## Constants
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
import {
|
|
370
|
+
APEX, // "@" - Root-level marker
|
|
371
|
+
SEPARATOR, // "#" - Composite key separator
|
|
372
|
+
INDEX_OU, // "indexOu"
|
|
373
|
+
INDEX_ALIAS, // "indexAlias"
|
|
374
|
+
INDEX_CLASS, // "indexClass"
|
|
375
|
+
INDEX_TYPE, // "indexType"
|
|
376
|
+
INDEX_XID, // "indexXid"
|
|
377
|
+
} from "@jaypie/dynamodb";
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
## Table Schema (CloudFormation/CDK Reference)
|
|
381
|
+
|
|
382
|
+
```yaml
|
|
383
|
+
AttributeDefinitions:
|
|
384
|
+
- AttributeName: model
|
|
385
|
+
AttributeType: S
|
|
386
|
+
- AttributeName: id
|
|
387
|
+
AttributeType: S
|
|
388
|
+
- AttributeName: indexOu
|
|
389
|
+
AttributeType: S
|
|
390
|
+
- AttributeName: indexAlias
|
|
391
|
+
AttributeType: S
|
|
392
|
+
- AttributeName: indexClass
|
|
393
|
+
AttributeType: S
|
|
394
|
+
- AttributeName: indexType
|
|
395
|
+
AttributeType: S
|
|
396
|
+
- AttributeName: indexXid
|
|
397
|
+
AttributeType: S
|
|
398
|
+
- AttributeName: sequence
|
|
399
|
+
AttributeType: N
|
|
400
|
+
|
|
401
|
+
KeySchema:
|
|
402
|
+
- AttributeName: model
|
|
403
|
+
KeyType: HASH
|
|
404
|
+
- AttributeName: id
|
|
405
|
+
KeyType: RANGE
|
|
406
|
+
|
|
407
|
+
GlobalSecondaryIndexes:
|
|
408
|
+
- IndexName: indexOu
|
|
409
|
+
KeySchema:
|
|
410
|
+
- AttributeName: indexOu
|
|
411
|
+
KeyType: HASH
|
|
412
|
+
- AttributeName: sequence
|
|
413
|
+
KeyType: RANGE
|
|
414
|
+
Projection:
|
|
415
|
+
ProjectionType: ALL
|
|
416
|
+
# Repeat for indexAlias, indexClass, indexType, indexXid
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## Error Handling
|
|
420
|
+
|
|
421
|
+
Functions throw `ConfigurationError` if client is not initialized:
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
import { getDocClient } from "@jaypie/dynamodb";
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
const client = getDocClient();
|
|
428
|
+
} catch (error) {
|
|
429
|
+
// ConfigurationError: DynamoDB client not initialized. Call initClient() first.
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
## Testing
|
|
434
|
+
|
|
435
|
+
Mock implementations in `@jaypie/testkit`:
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
import { vi } from "vitest";
|
|
439
|
+
|
|
440
|
+
vi.mock("@jaypie/dynamodb", async () => {
|
|
441
|
+
const testkit = await import("@jaypie/testkit/mock");
|
|
442
|
+
return testkit;
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// Key builders and indexEntity work correctly (delegate to real implementations)
|
|
446
|
+
// Query functions return empty results by default
|
|
447
|
+
// Entity operations return sensible defaults
|
|
448
|
+
|
|
449
|
+
// Customize mock behavior:
|
|
450
|
+
import { queryByOu, getEntity, putEntity } from "@jaypie/testkit/mock";
|
|
451
|
+
|
|
452
|
+
queryByOu.mockResolvedValue({
|
|
453
|
+
items: [{ id: "123", name: "Test" }],
|
|
454
|
+
lastEvaluatedKey: undefined,
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
getEntity.mockResolvedValue({ id: "123", model: "record", name: "Test" });
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
## Best Practices
|
|
461
|
+
|
|
462
|
+
### Use putEntity, Not Direct Writes
|
|
463
|
+
|
|
464
|
+
Always use `putEntity` or `updateEntity` to ensure GSI keys are auto-populated:
|
|
465
|
+
|
|
466
|
+
```typescript
|
|
467
|
+
// CORRECT - uses putEntity which calls indexEntity internally
|
|
468
|
+
const entity = await putEntity({
|
|
469
|
+
entity: {
|
|
470
|
+
model: "record",
|
|
471
|
+
alias: "my-alias",
|
|
472
|
+
// ...
|
|
473
|
+
},
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// WRONG - bypasses index population
|
|
477
|
+
const entity = {
|
|
478
|
+
model: "record",
|
|
479
|
+
alias: "my-alias",
|
|
480
|
+
indexAlias: "@#record#my-alias", // Don't manually set index keys
|
|
481
|
+
};
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### Use indexEntity for Raw Entities
|
|
485
|
+
|
|
486
|
+
If you need to prepare an entity before a batch write:
|
|
487
|
+
|
|
488
|
+
```typescript
|
|
489
|
+
import { indexEntity } from "@jaypie/dynamodb";
|
|
490
|
+
|
|
491
|
+
// Use indexEntity to prepare entities for batch operations
|
|
492
|
+
const indexed = indexEntity(myEntity);
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### Use Meaningful Model Names
|
|
496
|
+
|
|
497
|
+
Model names are part of every key. Use short, descriptive names:
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
// GOOD
|
|
501
|
+
{ model: "record" }
|
|
502
|
+
{ model: "message" }
|
|
503
|
+
{ model: "chat" }
|
|
504
|
+
|
|
505
|
+
// AVOID
|
|
506
|
+
{ model: "DailyLogRecord" }
|
|
507
|
+
{ model: "ChatMessage_v2" }
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### Sequence for Ordering
|
|
511
|
+
|
|
512
|
+
Always set `sequence: Date.now()` for chronological ordering in GSI queries:
|
|
513
|
+
|
|
514
|
+
```typescript
|
|
515
|
+
const entity = await putEntity({
|
|
516
|
+
entity: {
|
|
517
|
+
// ...
|
|
518
|
+
sequence: Date.now(), // Required for proper ordering
|
|
519
|
+
},
|
|
520
|
+
});
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
### Soft Delete and Archive Patterns
|
|
524
|
+
|
|
525
|
+
Use `deleteEntity` for logical deletion and `archiveEntity` for archival:
|
|
526
|
+
|
|
527
|
+
```typescript
|
|
528
|
+
// Soft delete - user action, can be recovered
|
|
529
|
+
await deleteEntity({ id: "123", model: "record" });
|
|
530
|
+
|
|
531
|
+
// Archive - system action, long-term storage
|
|
532
|
+
await archiveEntity({ id: "123", model: "record" });
|
|
533
|
+
|
|
534
|
+
// Queries exclude both by default
|
|
535
|
+
const { items } = await queryByOu({ model: "record", ou: APEX });
|
|
536
|
+
|
|
537
|
+
// Include if needed
|
|
538
|
+
const { items: all } = await queryByOu({
|
|
539
|
+
model: "record",
|
|
540
|
+
ou: APEX,
|
|
541
|
+
includeDeleted: true,
|
|
542
|
+
includeArchived: true,
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
// Permanent deletion (use sparingly)
|
|
546
|
+
await destroyEntity({ id: "123", model: "record" });
|
|
547
|
+
```
|