@aws/nx-plugin-mcp 1.0.0-rc.9 → 1.0.0-rc.90
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/bin/aws-nx-mcp.js +14263 -13571
- package/docs/get_started/building-with-ai.mdx +116 -0
- package/docs/get_started/concepts.mdx +67 -0
- package/docs/get_started/existing-project.mdx +180 -0
- package/docs/get_started/graph-builder.mdx +39 -0
- package/docs/get_started/quick-start.mdx +277 -0
- package/docs/get_started/tutorials/contribute-generator.mdx +408 -0
- package/docs/get_started/tutorials/dungeon-game/1.mdx +1578 -0
- package/docs/get_started/tutorials/dungeon-game/2.mdx +245 -0
- package/docs/get_started/tutorials/dungeon-game/3.mdx +66 -0
- package/docs/get_started/tutorials/dungeon-game/4.mdx +164 -0
- package/docs/get_started/tutorials/dungeon-game/overview.mdx +145 -0
- package/docs/get_started/tutorials/dungeon-game/wrap-up.mdx +41 -0
- package/docs/get_started/upgrading.mdx +157 -0
- package/docs/guides/agentcore-gateway.mdx +490 -0
- package/docs/guides/agentcore-harness.mdx +312 -0
- package/docs/guides/astro-docs.mdx +8 -0
- package/docs/guides/connection/agentcore-gateway-agent.mdx +222 -0
- package/docs/guides/connection/agentcore-gateway-gateway.mdx +154 -0
- package/docs/guides/connection/agentcore-gateway-mcp.mdx +134 -0
- package/docs/guides/connection/py-agent-a2a.mdx +48 -16
- package/docs/guides/connection/py-agent-dynamodb.mdx +115 -0
- package/docs/guides/connection/py-agent-gateway.mdx +182 -0
- package/docs/guides/connection/py-agent-mcp.mdx +43 -14
- package/docs/guides/connection/py-agent-rdb.mdx +169 -0
- package/docs/guides/connection/py-fast-api-dynamodb.mdx +56 -0
- package/docs/guides/connection/py-fast-api-rdb.mdx +175 -0
- package/docs/guides/connection/py-mcp-server-dynamodb.mdx +115 -0
- package/docs/guides/connection/py-mcp-server-rdb.mdx +178 -0
- package/docs/guides/connection/react-agentcore-gateway.mdx +112 -0
- package/docs/guides/connection/react-agui.mdx +33 -14
- package/docs/guides/connection/react-fastapi.mdx +39 -3
- package/docs/guides/connection/react-py-agent.mdx +10 -16
- package/docs/guides/connection/react-smithy.mdx +4 -4
- package/docs/guides/connection/react-trpc.mdx +1 -1
- package/docs/guides/connection/react-ts-agent.mdx +9 -9
- package/docs/guides/connection/smithy-dynamodb.mdx +5 -5
- package/docs/guides/connection/smithy-rdb.mdx +9 -9
- package/docs/guides/connection/trpc-dynamodb.mdx +4 -4
- package/docs/guides/connection/trpc-rdb.mdx +5 -5
- package/docs/guides/connection/ts-agent-a2a.mdx +14 -11
- package/docs/guides/connection/ts-agent-dynamodb.mdx +35 -36
- package/docs/guides/connection/ts-agent-gateway.mdx +147 -0
- package/docs/guides/connection/ts-agent-mcp.mdx +12 -9
- package/docs/guides/connection/ts-agent-rdb.mdx +61 -25
- package/docs/guides/connection/ts-mcp-server-dynamodb.mdx +35 -36
- package/docs/guides/connection/ts-mcp-server-rdb.mdx +59 -18
- package/docs/guides/connection.mdx +122 -5
- package/docs/guides/docker-bundling.mdx +82 -14
- package/docs/guides/fastapi.mdx +253 -12
- package/docs/guides/local-development.mdx +87 -0
- package/docs/guides/nx-generator.mdx +4 -3
- package/docs/guides/nx-migration.mdx +165 -0
- package/docs/guides/py-agent.mdx +332 -55
- package/docs/guides/py-dynamodb.mdx +476 -0
- package/docs/guides/py-mcp-server.mdx +57 -2
- package/docs/guides/py-rdb.mdx +269 -0
- package/docs/guides/python-lambda-function.mdx +1 -1
- package/docs/guides/python-project.mdx +31 -0
- package/docs/guides/react-website-auth.mdx +104 -5
- package/docs/guides/react-website.mdx +409 -95
- package/docs/guides/runtime-config.mdx +17 -4
- package/docs/guides/security.mdx +75 -0
- package/docs/guides/smithy-project.mdx +167 -0
- package/docs/guides/terraform-project.mdx +119 -8
- package/docs/guides/trpc.mdx +55 -14
- package/docs/guides/ts-agent.mdx +207 -23
- package/docs/guides/ts-dcr-proxy.mdx +569 -0
- package/docs/guides/ts-dynamodb.mdx +66 -242
- package/docs/guides/ts-lambda-function.mdx +1 -1
- package/docs/guides/ts-mcp-server.mdx +111 -29
- package/docs/guides/ts-nx-plugin.mdx +4 -4
- package/docs/guides/ts-rdb.mdx +116 -466
- package/docs/guides/ts-smithy-api.mdx +260 -16
- package/docs/guides/typescript-infrastructure.mdx +79 -25
- package/docs/guides/typescript-project.mdx +157 -24
- package/docs/guides/workspace.mdx +31 -2
- package/docs/snippets/agent/architecture.mdx +1 -1
- package/docs/snippets/agent/bedrock-deployment.mdx +9 -5
- package/docs/snippets/agent/runtime-arn.mdx +23 -2
- package/docs/snippets/agent/securing-your-agent.mdx +39 -0
- package/docs/snippets/api/access-logging.mdx +38 -0
- package/docs/snippets/api/cors-configuration-cdk-note.mdx +1 -1
- package/docs/snippets/api/cors-configuration-terraform-note.mdx +1 -1
- package/docs/snippets/api/type-safe-api-integrations.mdx +218 -373
- package/docs/snippets/api/waf-configuration.mdx +3 -3
- package/docs/snippets/connection/a2a-infrastructure.mdx +1 -1
- package/docs/snippets/connection/dynamodb-local-development.mdx +2 -2
- package/docs/snippets/connection/lambda-dynamodb-access.mdx +2 -2
- package/docs/snippets/connection/py-dynamodb-local-development.mdx +7 -0
- package/docs/snippets/connection/py-lambda-rdb-ssl-requirements.mdx +7 -0
- package/docs/snippets/connection/rdb-api-infrastructure.mdx +42 -19
- package/docs/snippets/dynamodb/deploying-table.mdx +176 -0
- package/docs/snippets/dynamodb/encryption-options.mdx +168 -0
- package/docs/snippets/dynamodb/gsi-config.mdx +38 -0
- package/docs/snippets/dynamodb/infrastructure.mdx +33 -0
- package/docs/snippets/dynamodb/local-dev-start.mdx +13 -0
- package/docs/snippets/dynamodb/local-dev-windows.mdx +15 -0
- package/docs/snippets/experimental-generator.mdx +8 -0
- package/docs/snippets/lambda-function/deploying-your-function.mdx +3 -3
- package/docs/snippets/mcp/architecture.mdx +1 -1
- package/docs/snippets/mcp/bedrock-deployment.mdx +9 -5
- package/docs/snippets/mcp/config.mdx +3 -2
- package/docs/snippets/mcp/shared-constructs.mdx +4 -5
- package/docs/snippets/pdk-migration/example/01-migrate-api.mdx +7 -5
- package/docs/snippets/pdk-migration/example/02-migrate-website.mdx +29 -10
- package/docs/snippets/pdk-migration/example/03-migrate-infra.mdx +4 -4
- package/docs/snippets/pdk-migration/faq/type-safe-api.mdx +5 -111
- package/docs/snippets/prerequisites.mdx +1 -4
- package/docs/snippets/rdb/admin-credentials.mdx +42 -0
- package/docs/snippets/rdb/architecture.mdx +39 -0
- package/docs/snippets/rdb/cluster-instances.mdx +31 -0
- package/docs/snippets/rdb/deletion-protection.mdx +64 -0
- package/docs/snippets/rdb/deploying.mdx +178 -0
- package/docs/snippets/rdb/encryption-key-rotation.mdx +30 -0
- package/docs/snippets/rdb/engine-version.mdx +63 -0
- package/docs/snippets/rdb/infrastructure.mdx +35 -0
- package/docs/snippets/rdb/logging-mysql.mdx +5 -0
- package/docs/snippets/rdb/logging-postgres.mdx +5 -0
- package/docs/snippets/rdb/performance-insights.mdx +36 -0
- package/docs/snippets/rdb/rds-proxy.mdx +50 -0
- package/docs/snippets/rdb/removal-policy.mdx +57 -0
- package/docs/snippets/rdb/serverless-capacity.mdx +32 -0
- package/docs/snippets/recommended-prerequisites.mdx +10 -0
- package/docs/snippets/required-prerequisites.mdx +1 -4
- package/docs/snippets/runtime-config-app-id-note.mdx +8 -0
- package/docs/snippets/shared-constructs.mdx +1 -1
- package/docs/snippets/trivy-image-scan.mdx +37 -0
- package/generators.json +170 -10
- package/package.json +1 -1
- package/src/agentcore-gateway/agent-connection/schema.json +31 -0
- package/src/agentcore-gateway/gateway-connection/schema.json +31 -0
- package/src/agentcore-gateway/mcp-connection/schema.json +31 -0
- package/src/agentcore-gateway/react-connection/schema.json +31 -0
- package/src/agentcore-gateway/schema.json +72 -0
- package/src/agentcore-harness/schema.json +53 -0
- package/src/connection/schema.json +5 -0
- package/src/infra/app/schema.json +5 -0
- package/src/init/schema.json +35 -0
- package/src/internal/test-matrix/schema.json +21 -0
- package/src/license/schema.json +5 -0
- package/src/open-api/json-metadata/schema.json +20 -0
- package/src/preset/schema.json +16 -5
- package/src/py/agent/a2a-connection/schema.json +5 -0
- package/src/py/agent/gateway-connection/schema.json +31 -0
- package/src/py/agent/mcp-connection/schema.json +5 -0
- package/src/py/agent/react-connection/schema.json +5 -0
- package/src/py/agent/schema.json +15 -1
- package/src/py/api/schema.json +5 -0
- package/src/py/dynamodb/agent-connection/schema.json +27 -0
- package/src/py/dynamodb/fast-api-connection/schema.json +23 -0
- package/src/py/dynamodb/mcp-server-connection/schema.json +27 -0
- package/src/py/dynamodb/schema.json +76 -0
- package/src/py/fast-api/react/schema.json +5 -0
- package/src/py/fast-api/schema.json +6 -0
- package/src/py/lambda-function/schema.json +6 -1
- package/src/py/mcp-server/schema.json +6 -0
- package/src/py/project/schema.json +6 -0
- package/src/py/rdb/agent-connection/schema.json +27 -0
- package/src/py/rdb/fast-api-connection/schema.json +23 -0
- package/src/py/rdb/mcp-server-connection/schema.json +27 -0
- package/src/py/rdb/schema.json +78 -0
- package/src/smithy/project/schema.json +28 -1
- package/src/smithy/react-connection/schema.json +5 -0
- package/src/smithy/ts/api/schema.json +6 -0
- package/src/terraform/project/schema.json +5 -0
- package/src/trpc/backend/schema.json +6 -0
- package/src/trpc/react/schema.json +5 -0
- package/src/ts/agent/a2a-connection/schema.json +5 -0
- package/src/ts/agent/gateway-connection/schema.json +31 -0
- package/src/ts/agent/mcp-connection/schema.json +5 -0
- package/src/ts/agent/react-connection/schema.json +5 -0
- package/src/ts/agent/schema.json +14 -0
- package/src/ts/api/schema.json +5 -0
- package/src/ts/astro-docs/schema.json +3 -3
- package/src/ts/dcr-proxy/schema.json +44 -0
- package/src/ts/docs/schema.json +3 -3
- package/src/ts/dynamodb/agent-connection/schema.json +5 -0
- package/src/ts/dynamodb/mcp-server-connection/schema.json +5 -0
- package/src/ts/dynamodb/schema.json +26 -2
- package/src/ts/dynamodb/smithy-connection/schema.json +5 -0
- package/src/ts/dynamodb/trpc-connection/schema.json +5 -0
- package/src/ts/lambda-function/schema.json +5 -0
- package/src/ts/lib/schema.json +5 -0
- package/src/ts/mcp-server/schema.json +6 -0
- package/src/ts/nx-generator/schema.json +5 -0
- package/src/ts/nx-migration/schema.json +63 -0
- package/src/ts/nx-plugin/schema.json +5 -0
- package/src/ts/rdb/agent-connection/schema.json +5 -0
- package/src/ts/rdb/mcp-server-connection/schema.json +5 -0
- package/src/ts/rdb/schema.json +7 -1
- package/src/ts/rdb/smithy-connection/schema.json +5 -0
- package/src/ts/rdb/trpc-connection/schema.json +5 -0
- package/src/ts/react-website/app/schema.json +12 -6
- package/src/ts/react-website/cognito-auth/schema.json +5 -0
- package/src/ts/react-website/runtime-config/schema.json +5 -0
- package/src/ts/website/app/schema.json +11 -6
- package/src/ts/website/auth/schema.json +5 -0
- /package/docs/snippets/connection/{lambda-rdb-ssl-requirements.mdx → ts-lambda-rdb-ssl-requirements.mdx} +0 -0
- /package/docs/snippets/connection/{mcp-server-rdb-ssl-requirements.mdx → ts-mcp-server-rdb-ssl-requirements.mdx} +0 -0
package/docs/guides/fastapi.mdx
CHANGED
|
@@ -6,7 +6,9 @@ when:
|
|
|
6
6
|
framework: [fastapi]
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
import { FileTree, Tabs, TabItem } from '@astrojs/starlight/components';
|
|
9
|
+
import { FileTree, Tabs, TabItem, CardGrid } from '@astrojs/starlight/components';
|
|
10
|
+
import Astro from '@astrojs/react';
|
|
11
|
+
import ConnectionCard from '@components/connection-card.astro';
|
|
10
12
|
import Link from '@components/link.astro';
|
|
11
13
|
import RunGenerator from '@components/run-generator.astro';
|
|
12
14
|
import GeneratorParameters from '@components/generator-parameters.astro';
|
|
@@ -119,9 +121,7 @@ def read_item(item_id: int):
|
|
|
119
121
|
The logger automatically includes:
|
|
120
122
|
|
|
121
123
|
- Correlation IDs for request tracing
|
|
122
|
-
- Request path and method
|
|
123
|
-
- Lambda context information
|
|
124
|
-
- Cold start indicators
|
|
124
|
+
- Request path, matched route and method
|
|
125
125
|
|
|
126
126
|
#### Tracing
|
|
127
127
|
|
|
@@ -157,8 +157,7 @@ Default metrics include:
|
|
|
157
157
|
|
|
158
158
|
- Request counts
|
|
159
159
|
- Success/failure counts
|
|
160
|
-
-
|
|
161
|
-
- Per-route metrics
|
|
160
|
+
- Per-route metrics (via a `route` dimension of `<method> <path>`)
|
|
162
161
|
|
|
163
162
|
### Error Handling
|
|
164
163
|
|
|
@@ -185,6 +184,216 @@ Unhandled exceptions are caught by the middleware and:
|
|
|
185
184
|
It's recommended to specify response models for your API operations for better code generation if using the `connection` generator. <Link path="guides/connection/react-fastapi#errors">See here for more details</Link>.
|
|
186
185
|
:::
|
|
187
186
|
|
|
187
|
+
### Accessing the Calling User
|
|
188
|
+
|
|
189
|
+
When your API is protected by authentication, your route handlers often need to know who is calling. The generated FastAPI runs inside AWS Lambda via the [Lambda Web Adapter](https://github.com/awslabs/aws-lambda-web-adapter), which forwards the API Gateway request context as JSON on the `x-amzn-request-context` header. You can read it from the FastAPI `Request` to extract the caller's identity.
|
|
190
|
+
|
|
191
|
+
As an example, let's add a `/me` endpoint that returns details about the calling user. We'll implement the extraction as a [FastAPI dependency](https://fastapi.tiangolo.com/tutorial/dependencies/) so it can be reused across routes. The shape of the request context — and therefore how you extract the identity — depends on both your selected `auth` method and whether you deployed a REST or HTTP API.
|
|
192
|
+
|
|
193
|
+
<OptionFilter when={{ auth: 'iam' }} description="Identity extraction for IAM-authenticated APIs">
|
|
194
|
+
For `IAM` authentication, we look up the caller in Cognito using the sub extracted from the API Gateway request context. Create `identity.py` alongside `main.py`:
|
|
195
|
+
|
|
196
|
+
<Tabs syncKey="http-rest">
|
|
197
|
+
<TabItem label="REST API" _filter={{ infra: 'rest-lambda' }}>
|
|
198
|
+
```python
|
|
199
|
+
import json
|
|
200
|
+
import os
|
|
201
|
+
from typing import Annotated
|
|
202
|
+
|
|
203
|
+
from boto3 import client
|
|
204
|
+
from fastapi import Depends, HTTPException, Request
|
|
205
|
+
from pydantic import BaseModel
|
|
206
|
+
|
|
207
|
+
cognito = client("cognito-idp")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class Identity(BaseModel):
|
|
211
|
+
sub: str
|
|
212
|
+
username: str
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def get_identity(request: Request) -> Identity:
|
|
216
|
+
# The Lambda Web Adapter forwards the API Gateway request context as JSON
|
|
217
|
+
request_context_header = request.headers.get("x-amzn-request-context")
|
|
218
|
+
if not request_context_header:
|
|
219
|
+
raise HTTPException(status_code=403, detail="Unable to determine calling user")
|
|
220
|
+
|
|
221
|
+
request_context = json.loads(request_context_header)
|
|
222
|
+
provider = request_context.get("identity", {}).get("cognitoAuthenticationProvider")
|
|
223
|
+
|
|
224
|
+
sub = provider.split(":")[-1] if provider else None
|
|
225
|
+
if not sub:
|
|
226
|
+
raise HTTPException(status_code=403, detail="Unable to determine calling user")
|
|
227
|
+
|
|
228
|
+
users = cognito.list_users(
|
|
229
|
+
# Assumes user pool id is configured in lambda environment
|
|
230
|
+
UserPoolId=os.environ["USER_POOL_ID"],
|
|
231
|
+
Limit=1,
|
|
232
|
+
Filter=f'sub="{sub}"',
|
|
233
|
+
).get("Users", [])
|
|
234
|
+
|
|
235
|
+
if len(users) != 1:
|
|
236
|
+
raise HTTPException(status_code=403, detail=f"No user found with subjectId {sub}")
|
|
237
|
+
|
|
238
|
+
return Identity(sub=sub, username=users[0]["Username"])
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
CurrentUser = Annotated[Identity, Depends(get_identity)]
|
|
242
|
+
```
|
|
243
|
+
</TabItem>
|
|
244
|
+
<TabItem label="HTTP API" _filter={{ infra: 'http-lambda' }}>
|
|
245
|
+
```python
|
|
246
|
+
import json
|
|
247
|
+
import os
|
|
248
|
+
from typing import Annotated
|
|
249
|
+
|
|
250
|
+
from boto3 import client
|
|
251
|
+
from fastapi import Depends, HTTPException, Request
|
|
252
|
+
from pydantic import BaseModel
|
|
253
|
+
|
|
254
|
+
cognito = client("cognito-idp")
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class Identity(BaseModel):
|
|
258
|
+
sub: str
|
|
259
|
+
username: str
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def get_identity(request: Request) -> Identity:
|
|
263
|
+
# The Lambda Web Adapter forwards the API Gateway request context as JSON
|
|
264
|
+
request_context_header = request.headers.get("x-amzn-request-context")
|
|
265
|
+
if not request_context_header:
|
|
266
|
+
raise HTTPException(status_code=403, detail="Unable to determine calling user")
|
|
267
|
+
|
|
268
|
+
request_context = json.loads(request_context_header)
|
|
269
|
+
amr = (
|
|
270
|
+
request_context.get("authorizer", {})
|
|
271
|
+
.get("iam", {})
|
|
272
|
+
.get("cognitoIdentity", {})
|
|
273
|
+
.get("amr", [])
|
|
274
|
+
)
|
|
275
|
+
sign_in = next((s for s in amr if ":CognitoSignIn:" in s), None)
|
|
276
|
+
sub = sign_in.split(":")[-1] if sign_in else None
|
|
277
|
+
|
|
278
|
+
if not sub:
|
|
279
|
+
raise HTTPException(status_code=403, detail="Unable to determine calling user")
|
|
280
|
+
|
|
281
|
+
users = cognito.list_users(
|
|
282
|
+
# Assumes user pool id is configured in lambda environment
|
|
283
|
+
UserPoolId=os.environ["USER_POOL_ID"],
|
|
284
|
+
Limit=1,
|
|
285
|
+
Filter=f'sub="{sub}"',
|
|
286
|
+
).get("Users", [])
|
|
287
|
+
|
|
288
|
+
if len(users) != 1:
|
|
289
|
+
raise HTTPException(status_code=403, detail=f"No user found with subjectId {sub}")
|
|
290
|
+
|
|
291
|
+
return Identity(sub=sub, username=users[0]["Username"])
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
CurrentUser = Annotated[Identity, Depends(get_identity)]
|
|
295
|
+
```
|
|
296
|
+
</TabItem>
|
|
297
|
+
</Tabs>
|
|
298
|
+
</OptionFilter>
|
|
299
|
+
|
|
300
|
+
<OptionFilter when={{ auth: 'cognito' }} description="Identity extraction for Cognito-authenticated APIs">
|
|
301
|
+
With `auth: 'cognito'`, the API Gateway Cognito User Pools authorizer verifies the JWT that the caller supplies in the `Authorization` header and places the verified claims on the request context.
|
|
302
|
+
|
|
303
|
+
Create `identity.py` alongside `main.py`:
|
|
304
|
+
|
|
305
|
+
<Tabs syncKey="http-rest">
|
|
306
|
+
<TabItem label="REST API" _filter={{ infra: 'rest-lambda' }}>
|
|
307
|
+
```python
|
|
308
|
+
import json
|
|
309
|
+
from typing import Annotated
|
|
310
|
+
|
|
311
|
+
from fastapi import Depends, HTTPException, Request
|
|
312
|
+
from pydantic import BaseModel
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class Identity(BaseModel):
|
|
316
|
+
sub: str
|
|
317
|
+
username: str
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def get_identity(request: Request) -> Identity:
|
|
321
|
+
# The Lambda Web Adapter forwards the API Gateway request context as JSON
|
|
322
|
+
request_context_header = request.headers.get("x-amzn-request-context")
|
|
323
|
+
if not request_context_header:
|
|
324
|
+
raise HTTPException(status_code=403, detail="Unable to determine calling user")
|
|
325
|
+
|
|
326
|
+
request_context = json.loads(request_context_header)
|
|
327
|
+
claims = request_context.get("authorizer", {}).get("claims", {})
|
|
328
|
+
|
|
329
|
+
sub = claims.get("sub")
|
|
330
|
+
username = claims.get("username")
|
|
331
|
+
|
|
332
|
+
if not sub or not username:
|
|
333
|
+
raise HTTPException(status_code=403, detail="Unable to determine calling user")
|
|
334
|
+
|
|
335
|
+
return Identity(sub=sub, username=username)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
CurrentUser = Annotated[Identity, Depends(get_identity)]
|
|
339
|
+
```
|
|
340
|
+
</TabItem>
|
|
341
|
+
<TabItem label="HTTP API" _filter={{ infra: 'http-lambda' }}>
|
|
342
|
+
HTTP APIs use a JWT authorizer which places the verified claims under `authorizer.jwt.claims`:
|
|
343
|
+
|
|
344
|
+
```python
|
|
345
|
+
import json
|
|
346
|
+
from typing import Annotated
|
|
347
|
+
|
|
348
|
+
from fastapi import Depends, HTTPException, Request
|
|
349
|
+
from pydantic import BaseModel
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class Identity(BaseModel):
|
|
353
|
+
sub: str
|
|
354
|
+
username: str
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def get_identity(request: Request) -> Identity:
|
|
358
|
+
# The Lambda Web Adapter forwards the API Gateway request context as JSON
|
|
359
|
+
request_context_header = request.headers.get("x-amzn-request-context")
|
|
360
|
+
if not request_context_header:
|
|
361
|
+
raise HTTPException(status_code=403, detail="Unable to determine calling user")
|
|
362
|
+
|
|
363
|
+
request_context = json.loads(request_context_header)
|
|
364
|
+
claims = request_context.get("authorizer", {}).get("jwt", {}).get("claims", {})
|
|
365
|
+
|
|
366
|
+
sub = claims.get("sub")
|
|
367
|
+
username = claims.get("username")
|
|
368
|
+
|
|
369
|
+
if not sub or not username:
|
|
370
|
+
raise HTTPException(status_code=403, detail="Unable to determine calling user")
|
|
371
|
+
|
|
372
|
+
return Identity(sub=sub, username=username)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
CurrentUser = Annotated[Identity, Depends(get_identity)]
|
|
376
|
+
```
|
|
377
|
+
</TabItem>
|
|
378
|
+
</Tabs>
|
|
379
|
+
|
|
380
|
+
:::tip[No token verification required]
|
|
381
|
+
You don't need any JWT-verification library here — the API Gateway Cognito User Pools authorizer has already verified the signature, issuer, scopes, and expiry by the time your Lambda runs. If any of those checks fail, API Gateway returns `401 Unauthorized` and your handler is never invoked.
|
|
382
|
+
:::
|
|
383
|
+
</OptionFilter>
|
|
384
|
+
|
|
385
|
+
You can then inject the `CurrentUser` dependency into any route that needs the caller's identity:
|
|
386
|
+
|
|
387
|
+
```python
|
|
388
|
+
from .identity import CurrentUser, Identity
|
|
389
|
+
from .init import app, tracer
|
|
390
|
+
|
|
391
|
+
@app.get("/me")
|
|
392
|
+
@tracer.capture_method
|
|
393
|
+
def me(identity: CurrentUser) -> Identity:
|
|
394
|
+
return identity
|
|
395
|
+
```
|
|
396
|
+
|
|
188
397
|
<OptionFilter when={{ infra: 'rest-lambda' }} description="Streaming — REST API only">
|
|
189
398
|
### Streaming
|
|
190
399
|
|
|
@@ -226,14 +435,14 @@ To consume a stream of responses, you can make use of the <Link path="guides/con
|
|
|
226
435
|
|
|
227
436
|
## Deploying your FastAPI
|
|
228
437
|
|
|
229
|
-
The FastAPI generator creates CDK or Terraform infrastructure as code based on your selected `
|
|
438
|
+
The FastAPI generator creates CDK or Terraform infrastructure as code based on your selected `iac`. You can use this to deploy your FastAPI.
|
|
230
439
|
|
|
231
440
|
<Infrastructure>
|
|
232
441
|
<Fragment slot="cdk">
|
|
233
442
|
The CDK construct for deploying your API in the `common/constructs` folder. You can use this in a CDK application:
|
|
234
443
|
|
|
235
444
|
```ts {6-8}
|
|
236
|
-
import { MyApi } from '
|
|
445
|
+
import { MyApi } from '@my-scope/common-constructs';
|
|
237
446
|
|
|
238
447
|
export class ExampleStack extends Stack {
|
|
239
448
|
constructor(scope: Construct, id: string) {
|
|
@@ -261,7 +470,7 @@ This sets up:
|
|
|
261
470
|
If you selected to use `Cognito` authentication, you will need to supply the `identity` property to the API construct:
|
|
262
471
|
|
|
263
472
|
```ts {9}
|
|
264
|
-
import { MyApi, UserIdentity } from '
|
|
473
|
+
import { MyApi, UserIdentity } from '@my-scope/common-constructs';
|
|
265
474
|
|
|
266
475
|
export class ExampleStack extends Stack {
|
|
267
476
|
constructor(scope: Construct, id: string) {
|
|
@@ -282,6 +491,8 @@ The `UserIdentity` construct can be generated using the <Link path="/guides/reac
|
|
|
282
491
|
<OptionFilter when={{ auth: 'custom' }} description="Custom Lambda Authorizer CDK usage">
|
|
283
492
|
:::caution[Custom Lambda Authorizer]
|
|
284
493
|
When using `Custom` auth, the construct creates a Lambda Authorizer internally from the generated `authorizer.py` file, which **denies all requests by default**. You must implement your authorization logic in that file before your API will accept any traffic.
|
|
494
|
+
|
|
495
|
+
Authorizer results are cached for 5 minutes, keyed on the `Authorization` header, so a revoked token may be accepted until its cached result expires. Reduce the cache duration in the generated infrastructure if you need faster revocation.
|
|
285
496
|
:::
|
|
286
497
|
</OptionFilter>
|
|
287
498
|
</Fragment>
|
|
@@ -330,7 +541,7 @@ This sets up:
|
|
|
330
541
|
:::note[Cognito Authentication]
|
|
331
542
|
If you selected to use `Cognito` authentication, you will need to supply the Cognito configuration:
|
|
332
543
|
|
|
333
|
-
```hcl {
|
|
544
|
+
```hcl {6-7}
|
|
334
545
|
module "my_api" {
|
|
335
546
|
source = "../../common/terraform/src/app/apis/my-api"
|
|
336
547
|
|
|
@@ -395,6 +606,8 @@ module "my_api" {
|
|
|
395
606
|
<OptionFilter when={{ auth: 'custom' }} description="Custom Lambda Authorizer usage with Terraform">
|
|
396
607
|
:::caution[Custom Lambda Authorizer]
|
|
397
608
|
When using `Custom` auth, your API is protected by a Lambda Authorizer that **denies all requests by default**. You must implement your authorization logic in the generated `authorizer.py` file before your API will accept any traffic.
|
|
609
|
+
|
|
610
|
+
Authorizer results are cached for 5 minutes, keyed on the `Authorization` header, so a revoked token may be accepted until its cached result expires. Reduce the cache duration in the generated infrastructure if you need faster revocation.
|
|
398
611
|
:::
|
|
399
612
|
</OptionFilter>
|
|
400
613
|
</Fragment>
|
|
@@ -406,6 +619,12 @@ When using `Custom` auth, your API is protected by a Lambda Authorizer that **de
|
|
|
406
619
|
<Snippet name="api/waf-configuration" parentHeading="WAF" />
|
|
407
620
|
</OptionFilter>
|
|
408
621
|
|
|
622
|
+
<OptionFilter when={{ infra: 'rest-lambda' }} description="Access logging — REST APIs log requests to CloudWatch by default">
|
|
623
|
+
### Access logging
|
|
624
|
+
|
|
625
|
+
<Snippet name="api/access-logging" parentHeading="Access logging" />
|
|
626
|
+
</OptionFilter>
|
|
627
|
+
|
|
409
628
|
### Integrations
|
|
410
629
|
|
|
411
630
|
<Snippet name="api/type-safe-api-integrations" parentHeading="Integrations" />
|
|
@@ -430,14 +649,14 @@ If you are actively working on both your CDK infrastructure and FastAPI together
|
|
|
430
649
|
<NxCommands
|
|
431
650
|
commands={[
|
|
432
651
|
'watch --projects=<FastAPIProject> -- \\ ',
|
|
433
|
-
'run
|
|
652
|
+
'run @<scope>/common-constructs:"generate:<ApiName>-metadata"',
|
|
434
653
|
]}
|
|
435
654
|
/>
|
|
436
655
|
:::
|
|
437
656
|
</Fragment>
|
|
438
657
|
<Fragment slot="terraform">
|
|
439
658
|
:::note[Terraform Limitations]
|
|
440
|
-
We do not support type-safe integrations for Terraform, and therefore no code generation targets are configured if you selected Terraform for your `
|
|
659
|
+
We do not support type-safe integrations for Terraform, and therefore no code generation targets are configured if you selected Terraform for your `iac`.
|
|
441
660
|
:::
|
|
442
661
|
</Fragment>
|
|
443
662
|
</Infrastructure>
|
|
@@ -509,3 +728,25 @@ This starts a local FastAPI development server with:
|
|
|
509
728
|
## Invoking your FastAPI
|
|
510
729
|
|
|
511
730
|
To invoke your API from a React website, you can use the <Link path="guides/connection/react-fastapi">`connection` generator</Link>.
|
|
731
|
+
|
|
732
|
+
## Connections
|
|
733
|
+
|
|
734
|
+
Use the <Link path="guides/connection">`connection`</Link> generator to integrate this project with others in your workspace. The following connections involve this project:
|
|
735
|
+
|
|
736
|
+
<CardGrid>
|
|
737
|
+
<ConnectionCard
|
|
738
|
+
title="React to FastAPI"
|
|
739
|
+
description="Call a Python FastAPI from a React website"
|
|
740
|
+
href={`/nx-plugin-for-aws/${Astro.currentLocale || 'en'}/guides/connection/react-fastapi`}
|
|
741
|
+
source="react"
|
|
742
|
+
target="fastapi"
|
|
743
|
+
/>
|
|
744
|
+
<ConnectionCard
|
|
745
|
+
title="FastAPI to Python DynamoDB"
|
|
746
|
+
description="Connect a FastAPI to a DynamoDB table"
|
|
747
|
+
href={`/nx-plugin-for-aws/${Astro.currentLocale || 'en'}/guides/connection/py-fast-api-dynamodb`}
|
|
748
|
+
source="fastapi"
|
|
749
|
+
target="dynamodb"
|
|
750
|
+
targetBadge="python"
|
|
751
|
+
/>
|
|
752
|
+
</CardGrid>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Local Development
|
|
3
|
+
description: How local development works with the serve and dev targets
|
|
4
|
+
---
|
|
5
|
+
import Link from '@components/link.astro';
|
|
6
|
+
import NxCommands from '@components/nx-commands.astro';
|
|
7
|
+
import PackageManagerShortCommand from '@components/package-manager-short-command.astro';
|
|
8
|
+
|
|
9
|
+
Connected projects expose two targets for running them on your machine: `serve` and `dev`. The difference is one of **scope** — how much of your application runs locally versus pointing at deployed AWS infrastructure.
|
|
10
|
+
|
|
11
|
+
Consider a workspace with the following connections: a website that calls a tRPC `api`, and also calls an `agent` which in turn calls an `mcp` server.
|
|
12
|
+
|
|
13
|
+
## `serve`
|
|
14
|
+
|
|
15
|
+
The `serve` target runs **only the targeted project** locally. Every other project it connects to is expected to be **deployed**, and is reached at its real AWS URL.
|
|
16
|
+
|
|
17
|
+
<NxCommands commands={['serve website']} />
|
|
18
|
+
|
|
19
|
+
Because the targeted project talks to deployed resources, it needs to know where they are. For a website this is provided by its `runtime-config.json` (see <Link path="guides/react-website#local-runtime-config">Local Runtime Config</Link>), which you load from a deployed application. For server-side projects (APIs and agents) that read <Link path="guides/runtime-config">Runtime Configuration</Link>, set the `RUNTIME_CONFIG_APP_ID` environment variable to point at your deployed AppConfig application.
|
|
20
|
+
|
|
21
|
+
```d2
|
|
22
|
+
direction: right
|
|
23
|
+
|
|
24
|
+
local: Local {
|
|
25
|
+
style.stroke-dash: 3
|
|
26
|
+
website: website
|
|
27
|
+
}
|
|
28
|
+
deployed: Deployed {
|
|
29
|
+
style.stroke-dash: 3
|
|
30
|
+
api: api
|
|
31
|
+
agent: agent
|
|
32
|
+
mcp: mcp
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
local.website -> deployed.api
|
|
36
|
+
local.website -> deployed.agent
|
|
37
|
+
deployed.agent -> deployed.mcp
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Use `serve` when you want to iterate on a single project against the "real", deployed versions of everything it depends on.
|
|
41
|
+
|
|
42
|
+
## `dev`
|
|
43
|
+
|
|
44
|
+
The `dev` target runs the **targeted project** and **every project connected to it transitively**, all on your machine. The connection generator wires this up automatically — running `dev` on the website also starts local servers for the `api`, the `agent`, and the `mcp` server it reaches through the agent.
|
|
45
|
+
|
|
46
|
+
<NxCommands commands={['dev website']} />
|
|
47
|
+
|
|
48
|
+
When run this way, the website's `runtime-config.json` is automatically overridden (via [Vite's `MODE`](https://vite.dev/guide/env-and-mode), set to `local-dev`) so that it points at your locally running servers instead of deployed URLs.
|
|
49
|
+
|
|
50
|
+
```d2
|
|
51
|
+
direction: right
|
|
52
|
+
|
|
53
|
+
local: Local {
|
|
54
|
+
style.stroke-dash: 3
|
|
55
|
+
website: website
|
|
56
|
+
api: api
|
|
57
|
+
agent: agent
|
|
58
|
+
mcp: mcp
|
|
59
|
+
|
|
60
|
+
website -> api
|
|
61
|
+
website -> agent
|
|
62
|
+
agent -> mcp
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Every project runs locally, so there are no deployed dependencies.
|
|
67
|
+
|
|
68
|
+
Use `dev` when you are working across several connected projects at once and want to iterate quickly without deploying your infrastructure.
|
|
69
|
+
|
|
70
|
+
### Projects with multiple components
|
|
71
|
+
|
|
72
|
+
Some project types can hold multiple components in a single project (for example a TypeScript or Python project containing several agents and MCP servers). For these:
|
|
73
|
+
|
|
74
|
+
- The project-level `dev` target starts **all** of the project's components together. Each component is added to `dev` as it is generated:
|
|
75
|
+
|
|
76
|
+
<NxCommands commands={['dev my-project']} />
|
|
77
|
+
- Each component also exposes a `<component-name>-dev` target, so you can run a single component on its own:
|
|
78
|
+
|
|
79
|
+
<NxCommands commands={['my-component-dev my-project']} />
|
|
80
|
+
|
|
81
|
+
:::tip[Workspace `dev` script]
|
|
82
|
+
Your workspace's root `package.json` includes a `dev` script that runs `nx run-many --target dev`, starting the `dev` target for every project that has one. Spin up your whole workspace's local development servers with:
|
|
83
|
+
|
|
84
|
+
<PackageManagerShortCommand commands={["dev"]} />
|
|
85
|
+
:::
|
|
86
|
+
|
|
87
|
+
For more detail on each project type's local development server, see the relevant guide — for example the <Link path="guides/react-website#local-development-server">React Website</Link> guide.
|
|
@@ -286,7 +286,7 @@ import { generateFiles, joinPathFragments } from '@nx/devkit';
|
|
|
286
286
|
// Generate files from templates
|
|
287
287
|
generateFiles(
|
|
288
288
|
tree,
|
|
289
|
-
joinPathFragments(
|
|
289
|
+
joinPathFragments(import.meta.dirname, 'files'), // Template directory
|
|
290
290
|
'path/to/output', // Output directory
|
|
291
291
|
{
|
|
292
292
|
// Variables to replace in templates
|
|
@@ -459,7 +459,7 @@ export const myGenerator = async (tree: Tree, schema: MyGeneratorSchema) => {
|
|
|
459
459
|
|
|
460
460
|
generateFiles(
|
|
461
461
|
tree,
|
|
462
|
-
joinPathFragments(
|
|
462
|
+
joinPathFragments(import.meta.dirname, 'files'), // Template directory
|
|
463
463
|
'path/to/output', // Output directory
|
|
464
464
|
data,
|
|
465
465
|
);
|
|
@@ -497,7 +497,7 @@ Unit tests for generators are straightforward to implement. Here's a typical pat
|
|
|
497
497
|
|
|
498
498
|
```typescript
|
|
499
499
|
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
|
500
|
-
import { yourGenerator } from './generator';
|
|
500
|
+
import { yourGenerator } from './generator.js';
|
|
501
501
|
|
|
502
502
|
describe('your generator', () => {
|
|
503
503
|
let tree;
|
|
@@ -583,6 +583,7 @@ When this generator is run in our repository, it'll generate the following files
|
|
|
583
583
|
- docs/src/content/docs/guides/
|
|
584
584
|
- \<name>.mdx Documentation page for your generator
|
|
585
585
|
- packages/nx-plugin/generators.json Updated to include your generator
|
|
586
|
+
- packages/nx-plugin/sdk/\<prefix>.ts Updated to expose your generator from the SDK (for `ts#` and `py#` generators)
|
|
586
587
|
</FileTree>
|
|
587
588
|
|
|
588
589
|
You can then start to implement your generator.
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Nx Migration Generator
|
|
3
|
+
description: Generates an Nx Migration for an Nx Plugin
|
|
4
|
+
generator: ts#nx-migration
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
import { FileTree } from '@astrojs/starlight/components';
|
|
8
|
+
import RunGenerator from '@components/run-generator.astro';
|
|
9
|
+
import NxCommands from '@components/nx-commands.astro';
|
|
10
|
+
import GeneratorParameters from '@components/generator-parameters.astro';
|
|
11
|
+
import Link from '@components/link.astro';
|
|
12
|
+
|
|
13
|
+
Adds an [Nx Migration](https://nx.dev/docs/kb/migration-generators) to an Nx Plugin. Migrations let `nx migrate` automatically update projects that were generated by a previous version of your plugin when you make a breaking change — renamed targets, changed vended config, or a dependency bump that requires accompanying code changes.
|
|
14
|
+
|
|
15
|
+
The generator scaffolds the migration, registers it in your plugin's `migrations.json`, and wires the `nx-migrations` field into your plugin's `package.json` (creating both if they don't exist yet).
|
|
16
|
+
|
|
17
|
+
:::tip[Recommended Setup]
|
|
18
|
+
We recommend using the <Link path="/guides/ts-nx-plugin">`ts#nx-plugin`</Link> generator as the base project for your migrations, since it configures a plugin project set up to publish generators and migrations.
|
|
19
|
+
:::
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
### Generate a Migration
|
|
24
|
+
|
|
25
|
+
<RunGenerator generator="ts#nx-migration" />
|
|
26
|
+
|
|
27
|
+
### Options
|
|
28
|
+
|
|
29
|
+
<GeneratorParameters generator="ts#nx-migration" />
|
|
30
|
+
|
|
31
|
+
## The three kinds of migration
|
|
32
|
+
|
|
33
|
+
Migrations come in three forms, discriminated by which fields the `migrations.json` entry carries. Pass `--kind` to choose (default `deterministic`):
|
|
34
|
+
|
|
35
|
+
- **Deterministic** (`implementation`) — a codemod with an exact before/after, and the one to reach for first. It runs unattended, including in CI and non-interactive terminals, and applies identically for every user. Use it alone when the change has a shape you can match reliably everywhere it appears, reporting anything it skipped via `nextSteps`.
|
|
36
|
+
- **Hybrid** (`implementation` + `prompt`) — both halves of one change. Use it when a change is breaking and can't be fully applied mechanically: the codemod does as much as it can safely match and returns `agentContext` describing what it changed *and what it skipped*; Nx passes that context to the paired `prompt`, which directs the agent at the rest. The prompt is what stops a workspace being left broken when the leftover edits span too many shapes to codemod.
|
|
37
|
+
- **Agentic** (`prompt`) — a markdown instruction file applied by the user's local coding agent via Nx's agentic `nx migrate` flow. Use it alone when no mechanical before/after holds at all, because the correct edit depends on what the user has built. When no agent runs (CI, no agent installed, consent declined), Nx surfaces the prompt as manual instructions — so write prompts as self-contained, human-actionable steps.
|
|
38
|
+
|
|
39
|
+
## Generator Output
|
|
40
|
+
|
|
41
|
+
The generator creates the following files under your plugin project's source folder, depending on the chosen `kind`:
|
|
42
|
+
|
|
43
|
+
<FileTree>
|
|
44
|
+
- src/migrations/latest/\<name>/
|
|
45
|
+
- migration.ts Codemod implementation (deterministic and hybrid)
|
|
46
|
+
- migration.spec.ts Tests for your migration (deterministic and hybrid)
|
|
47
|
+
- prompt.md Agent/human instructions (agentic and hybrid)
|
|
48
|
+
- migrations.json Created or updated to register your migration
|
|
49
|
+
- package.json Created or updated to add an "nx-migrations" entry
|
|
50
|
+
</FileTree>
|
|
51
|
+
|
|
52
|
+
A new migration lands in `latest/`. Grouping migrations by the release that ships them keeps their order visible at a glance as the collection grows — move a migration into a `v<x.y.z>/` folder (and update its paths in `migrations.json`) when you assign its version.
|
|
53
|
+
|
|
54
|
+
It registers the migration in `migrations.json` with the fields for its kind and no `version` — see [Versioning](#versioning) below. The registered key is prefixed with the folder the migration lives in (`latest-<name>`), so reusing a name for a later change can't silently overwrite a migration that has already shipped.
|
|
55
|
+
|
|
56
|
+
## Implementing a Migration
|
|
57
|
+
|
|
58
|
+
A deterministic (or hybrid) migration is a function which mutates the virtual filesystem (the `Tree`), returning a [`MigrationReturnObject`](https://nx.dev/docs/reference/devkit/MigrationReturnObject). Here's a hybrid one, which returns both:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { type MigrationReturnObject, type Tree } from '@nx/devkit';
|
|
62
|
+
import { formatFilesInSubtree } from '@aws/nx-plugin/sdk/utils/format';
|
|
63
|
+
|
|
64
|
+
export default async function migration(
|
|
65
|
+
tree: Tree,
|
|
66
|
+
): Promise<MigrationReturnObject> {
|
|
67
|
+
const nextSteps: string[] = [];
|
|
68
|
+
const agentContext: string[] = [];
|
|
69
|
+
|
|
70
|
+
// Read and update the files your generators produce here.
|
|
71
|
+
|
|
72
|
+
await formatFilesInSubtree(tree);
|
|
73
|
+
|
|
74
|
+
return { nextSteps, agentContext };
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- **`nextSteps`** — the follow-up actions Nx prints for the user after `nx migrate` runs. Every entry should be something they need to do by hand: reconcile a file you skipped (see the guardrails below), or finish something beyond the migration's reach, like an install to refresh a lock file. They aren't a log of what you changed — when a transform applies cleanly, add nothing, otherwise the entries that do need action get buried.
|
|
79
|
+
- **`agentContext`** (hybrid only) — a summary of what the codemod changed, passed to the paired `prompt` when it runs under Nx's agentic flow so the agent can focus on the user-owned parts.
|
|
80
|
+
|
|
81
|
+
For some example operations you can perform in your migration (generating files, modifying existing files using GritQL, reading and updating JSON), refer to the <Link path="/guides/nx-generator">Nx Generator guide</Link>.
|
|
82
|
+
|
|
83
|
+
### Guardrails
|
|
84
|
+
|
|
85
|
+
Migrations run against workspaces you don't control, so it is recommended to adhere to the following (the scaffolded skeleton bakes these in):
|
|
86
|
+
|
|
87
|
+
- **Transform source code with GritQL.** Use the <Link path="/guides/nx-generator">GritQL helpers</Link> (`applyGritQL`, `matchGritQL`) to edit source rather than regexes or string replacements. GritQL matches the AST, so one pattern holds across the formatting, whitespace and quoting a user's copy may have drifted into, where text matching quietly misses equivalent code or corrupts the file. Use `updateJson` for JSON and the matching parser for other structured config.
|
|
88
|
+
- **Pattern-match before writing.** If a target file has diverged from the shape your generators produce, skip it and report it via `nextSteps`, or consider a hybrid migration, rather than clobbering the user's changes. `matchGritQL` is a convenient way to make that check.
|
|
89
|
+
- **Report only what's left to do.** `nextSteps` is for work the user has to pick up, not a record of the edits you made — those are already in their `git diff`.
|
|
90
|
+
- **Idempotent.** Re-running the migration must be a no-op. Guard GritQL rewrites that inject code with a `where { ... <: not contains ... }` clause so a second run doesn't append a duplicate.
|
|
91
|
+
- **Format what you write.** Finish with `formatFilesInSubtree(tree)` so the files your migration wrote are formatted correctly.
|
|
92
|
+
|
|
93
|
+
### Testing Your Migration
|
|
94
|
+
|
|
95
|
+
The scaffolded `migration.spec.ts` uses `createTreeUsingTsSolutionSetup()` to build a virtual workspace matching the shape our preset generates. Test that the migration takes a workspace from the "before" state to the target state, skips (and reports) code it doesn't recognise, and is idempotent:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import type { Tree } from '@nx/devkit';
|
|
99
|
+
import { createTreeUsingTsSolutionSetup } from '@aws/nx-plugin/sdk/utils/test';
|
|
100
|
+
import migration from './migration.js';
|
|
101
|
+
|
|
102
|
+
describe('my migration', () => {
|
|
103
|
+
let tree: Tree;
|
|
104
|
+
|
|
105
|
+
beforeEach(() => {
|
|
106
|
+
tree = createTreeUsingTsSolutionSetup();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should update the generated shape', async () => {
|
|
110
|
+
// Set up a tree with the "before" state
|
|
111
|
+
tree.write('project.json', JSON.stringify({ targets: { foo: {} } }));
|
|
112
|
+
|
|
113
|
+
// Run the migration
|
|
114
|
+
await migration(tree);
|
|
115
|
+
|
|
116
|
+
// Check that the tree matches the target state
|
|
117
|
+
const projectJson = JSON.parse(tree.read('project.json', 'utf-8'));
|
|
118
|
+
expect(projectJson.targets).toHaveProperty('bar');
|
|
119
|
+
expect(projectJson.targets).not.toHaveProperty('foo');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Unit tests only prove the migration does what you wrote it to do. Also test it against a workspace generated by the **published** version of your plugin — the state your users upgrade from — and confirm the result matches what your generators produce today: inspect the diff, generate the same projects into a fresh workspace and compare, check the workspace still builds, then re-run the migration to confirm it changes nothing. Repeat with a workspace you've customised first, to check the migration leaves your edits alone and reports them instead.
|
|
125
|
+
|
|
126
|
+
## Versioning
|
|
127
|
+
|
|
128
|
+
Nx runs a migration when the user's installed plugin version is less than the migration's `version`. This generator writes **no** `version` field — assign it (or stamp it from your release process) when you cut the release that ships the migration, so it runs for everyone upgrading from an earlier version.
|
|
129
|
+
|
|
130
|
+
Once you assign versions, a `migrations.json` registering one migration of each kind looks like this:
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"$schema": "http://json-schema.org/schema",
|
|
135
|
+
"name": "my-plugin",
|
|
136
|
+
"generators": {
|
|
137
|
+
"v2.0.0-rename-foo-target": {
|
|
138
|
+
"version": "2.0.0",
|
|
139
|
+
"description": "Rename the foo target to bar",
|
|
140
|
+
"implementation": "./src/migrations/v2.0.0/rename-foo-target/migration"
|
|
141
|
+
},
|
|
142
|
+
"v3.0.0-migrate-custom-handlers": {
|
|
143
|
+
"version": "3.0.0",
|
|
144
|
+
"description": "Update custom handlers for the new API",
|
|
145
|
+
"prompt": "./src/migrations/v3.0.0/migrate-custom-handlers/prompt.md"
|
|
146
|
+
},
|
|
147
|
+
"v4.0.0-upgrade-framework": {
|
|
148
|
+
"version": "4.0.0",
|
|
149
|
+
"description": "Upgrade the framework and reconcile call sites",
|
|
150
|
+
"implementation": "./src/migrations/v4.0.0/upgrade-framework/migration",
|
|
151
|
+
"prompt": "./src/migrations/v4.0.0/upgrade-framework/prompt.md"
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Running Migrations
|
|
158
|
+
|
|
159
|
+
Users apply your plugin's migrations with the two-step `nx migrate` flow — generate the pending migrations, install, then run them:
|
|
160
|
+
|
|
161
|
+
<NxCommands commands={['migrate my-plugin@latest', 'migrate --run-migrations']} />
|
|
162
|
+
|
|
163
|
+
Agentic and hybrid migrations are applied by the user's local coding agent during `nx migrate --run-migrations` when one is available.
|
|
164
|
+
|
|
165
|
+
For the full flow from the upgrading user's perspective — including example output for deterministic and agentic migrations — see <Link path="get_started/upgrading">Upgrading Your Workspace</Link>.
|