Migrazione da AWS PDK
Questa guida ti accompagna attraverso un esempio di migrazione di un progetto AWS PDK al Nx Plugin for AWS, fornendo anche indicazioni generali su questo argomento.
La migrazione al Nx Plugin for AWS offre i seguenti vantaggi rispetto a PDK:
- Build più veloci
- Più facile da usare (UI e CLI)
- Adatto al vibe-coding (prova il nostro server MCP!)
- Tecnologie più moderne
- Sviluppo locale di API e siti web
- Maggiore controllo (modifica i file forniti per adattarli al tuo caso d’uso)
- E molto altro!
Esempio di Migrazione: Applicazione Shopping List
Sezione intitolata “Esempio di Migrazione: Applicazione Shopping List”In questa guida, useremo la Shopping List Application dal Tutorial PDK come progetto target da migrare. Segui i passaggi in quel tutorial per creare il progetto target se desideri seguire tu stesso.
L’applicazione shopping list consiste nei seguenti tipi di progetto PDK:
MonorepoTsProjectTypeSafeApiProjectCloudscapeReactTsWebsiteProjectInfrastructureTsProject
Crea Workspace
Sezione intitolata “Crea Workspace”Per iniziare, creeremo un nuovo workspace per il nostro nuovo progetto. Sebbene più estremo di una migrazione in loco, questo approccio ci dà il risultato finale più pulito. Creare un workspace Nx è equivalente a usare il MonorepoTsProject di PDK:
pnpm create @aws/nx-workspace@1.0.0-rc.47 shopping-list --iac=cdkyarn create @aws/nx-workspace@1.0.0-rc.47 shopping-list --iac=cdknpm create @aws/nx-workspace@1.0.0-rc.47 -- shopping-list --iac=cdkbun create @aws/nx-workspace@1.0.0-rc.47 shopping-list --iac=cdkApri la directory shopping-list che questo comando crea nel tuo IDE preferito.
Migra l’API
Sezione intitolata “Migra l’API”Il TypeSafeApiProject utilizzato nell’applicazione della lista della spesa faceva uso di:
- Smithy come linguaggio di modellazione
- TypeScript per l’implementazione delle operazioni
- Generazione di hook TypeScript per l’integrazione con un sito web React
Possiamo quindi utilizzare il generatore ts#smithy-api per fornire funzionalità equivalenti.
Generare un’API Smithy TypeScript
Sezione intitolata “Generare un’API Smithy TypeScript”Esegui il generatore ts#api con framework impostato su smithy per configurare il tuo progetto API in packages/api:
pnpm nx g @aws/nx-plugin:ts#api --name=api --framework=smithy --namespace=com.aws --auth=iam --no-interactiveyarn nx g @aws/nx-plugin:ts#api --name=api --framework=smithy --namespace=com.aws --auth=iam --no-interactivenpx nx g @aws/nx-plugin:ts#api --name=api --framework=smithy --namespace=com.aws --auth=iam --no-interactivebunx nx g @aws/nx-plugin:ts#api --name=api --framework=smithy --namespace=com.aws --auth=iam --no-interactivePuoi anche eseguire una prova per vedere quali file verrebbero modificati
pnpm nx g @aws/nx-plugin:ts#api --name=api --framework=smithy --namespace=com.aws --auth=iam --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#api --name=api --framework=smithy --namespace=com.aws --auth=iam --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#api --name=api --framework=smithy --namespace=com.aws --auth=iam --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#api --name=api --framework=smithy --namespace=com.aws --auth=iam --no-interactive --dry-run- Installa il Nx Console VSCode Plugin se non l'hai già fatto
- Apri la console Nx in VSCode
- Clicca su
Generate (UI)nella sezione "Common Nx Commands" - Cerca
@aws/nx-plugin - ts#api - Compila i parametri richiesti
- name: api
- framework: smithy
- namespace: com.aws
- auth: iam
- Clicca su
Generate
Noterai che questo genera un progetto model, così come un progetto backend. Il progetto model contiene il tuo modello Smithy, e backend contiene l’implementazione del server.
Il backend utilizza il Smithy Server Generator for TypeScript. Esploreremo questo aspetto più in dettaglio di seguito.
Migrare il modello Smithy
Sezione intitolata “Migrare il modello Smithy”Ora che abbiamo la struttura di base per il nostro progetto API Smithy, possiamo migrare il modello:
-
Elimina i file Smithy di esempio generati in
packages/api/model/src -
Copia il tuo modello dalla directory
packages/api/model/src/main/smithydel progetto PDK nella directorypackages/api/model/srcdel tuo nuovo progetto. -
Aggiorna il nome del servizio e il namespace in
smithy-build.jsonper corrispondere all’applicazione PDK:smithy-build.json "plugins": {"openapi": {"service": "com.aws#MyApi",... -
Aggiorna il servizio in
main.smithyper aggiungere l’erroreValidationException, che è richiesto quando si utilizza Smithy TypeScript Server SDK.main.smithy use smithy.framework#ValidationException/// My Shopping List API@restJson1service MyApi {version: "1.0"operations: [GetShoppingListsPutShoppingListDeleteShoppingList]errors: [BadRequestErrorNotAuthorizedErrorInternalFailureErrorValidationException]} -
Aggiungi un file
extensions.smithyapackages/api/model/srcdove definiremo un trait che fornisce informazioni di paginazione al client generato:extensions.smithy $version: "2"namespace com.awsuse smithy.openapi#specificationExtension@trait@specificationExtension(as: "x-cursor")structure cursor {inputToken: Stringenabled: Boolean} -
Aggiungi il nuovo trait
@cursorall’operazioneGetShoppingListsinget-shopping-lists.smithy:operations/get-shopping-lists.smithy @readonly@http(method: "GET", uri: "/shopping-list")@paginated(inputToken: "nextToken", outputToken: "nextToken", pageSize: "pageSize", items: "shoppingLists")@cursor(inputToken: "nextToken")@handler(language: "typescript")operation GetShoppingLists {input := with [PaginatedInputMixin] {@httpQuery("shoppingListId")shoppingListId: ShoppingListId}Qualsiasi operazione
@paginateddovrebbe utilizzare anche@cursorse stai usando il generatore di client fornito da Nx Plugin for AWS (tramite il generatoreapi-connection). -
Infine, rimuovi il trait
@handlerda tutte le operazioni poiché non è supportato da Nx Plugin for AWS. Utilizzandots#smithy-api, non abbiamo bisogno dei costrutti CDK delle funzioni lambda auto-generate e dei target di bundling generati da questo trait, poiché utilizziamo un singolo bundle per tutte le funzioni lambda.
A questo punto, eseguiamo una build per verificare le modifiche al modello e assicurarci di avere del codice server generato con cui lavorare. Ci saranno alcuni errori nel progetto backend (@shopping-list/api) ma li risolveremo successivamente.
pnpm nx run-many --target buildyarn nx run-many --target buildnpx nx run-many --target buildbunx nx run-many --target buildMigrare i Lambda Handler
Sezione intitolata “Migrare i Lambda Handler”Puoi considerare il progetto api/backend come in qualche modo equivalente al progetto api/handlers/typescript di Type Safe API.
Una delle principali differenze tra Type Safe API e il generatore ts#smithy-api è che gli handler sono implementati utilizzando il Smithy Server Generator for TypeScript, piuttosto che i wrapper di handler generati da Type Safe API (che si trovano nel progetto api/generated/typescript/runtime).
I lambda handler dell’applicazione della lista della spesa si basano sul pacchetto @aws-sdk/client-dynamodb, quindi installiamolo nel progetto @shopping-list/api:
pnpm add @aws-sdk/client-dynamodb --filter apiyarn workspace @shopping-list/api add @aws-sdk/client-dynamodbnpm install --legacy-peer-deps @aws-sdk/client-dynamodb -w packages/apibun add @aws-sdk/client-dynamodb --cwd packages/apiQuindi, copiamo il file handlers/src/dynamo-client.ts dal progetto PDK in backend/src/operations in modo che sia disponibile per i nostri handler.
Il generatore ts#smithy-api crea uno scaffold di un’operazione Echo di esempio. Poiché l’abbiamo rimossa dal nostro modello, elimina l’handler corrispondente in backend/src/operations/echo.ts. Registreremo le nostre operazioni migrate in service.ts più avanti.
Per migrare gli handler, puoi seguire questi passaggi generali:
-
Copia l’handler dalla directory
packages/api/handlers/typescript/srcdel tuo progetto PDK nella directorypackages/api/backend/src/operationsdel tuo nuovo progetto. -
Rimuovi gli import di
my-api-typescript-runtimee importa invece il tipo di operazione dal TypeScript Server SDK generato, così come ilServiceContextad esempio:import {deleteShoppingListHandler,DeleteShoppingListChainedHandlerFunction,INTERCEPTORS,Response,LoggingInterceptor,} from 'myapi-typescript-runtime';import { DeleteShoppingList as DeleteShoppingListOperation } from '../generated/ssdk/index.js';import { ServiceContext } from '../context.js'; -
Elimina l’export del wrapper dell’handler
export const handler = deleteShoppingListHandler(...INTERCEPTORS,deleteShoppingList,); -
Aggiorna la firma per il tuo handler di operazione per utilizzare l’SSDK:
export const deleteShoppingList: DeleteShoppingListChainedHandlerFunction = async (request) => {export const DeleteShoppingList: DeleteShoppingListOperation<ServiceContext> = async (input, ctx) => { -
Sostituisci l’uso di
LoggingInterceptorconctx.logger. (Si applica anche agli interceptor di metriche e tracing):LoggingInterceptor.getLogger(request).info('...');ctx.logger.info('...'); -
Aggiorna i riferimenti ai parametri di input. Poiché l’SSDK fornisce tipi che corrispondono esattamente al tuo modello Smithy (piuttosto che raggruppare separatamente i parametri path/query/header dal parametro body), aggiorna di conseguenza tutti i riferimenti agli input:
const shoppingListId = request.input.requestParameters.shoppingListId;const shoppingListId = input.shoppingListId; -
Rimuovi l’uso di
Response. Invece restituiamo semplicemente oggetti semplici nell’SSDK.return Response.success({ shoppingListId });return { shoppingListId };Inoltre non lanciamo più o restituiamo
Response, invece lanciamo gli errori generati dall’SSDK:throw Response.badRequest({ message: 'oh no' });return Response.badRequest({ message: 'oh no' });import { BadRequestError } from '../generated/ssdk/index.js';throw new BadRequestError({ message: 'oh no' }); -
Aggiorna tutti gli import per utilizzare la sintassi ESM, ovvero aggiungendo l’estensione
.jsagli import relativi. -
Aggiungi l’operazione a
service.tsservice.ts import { ServiceContext } from './context.js';import { MyApiService } from './generated/ssdk/index.js';import { DeleteShoppingList } from './operations/delete-shopping-list.js';import { GetShoppingLists } from './operations/get-shopping-lists.js';import { PutShoppingList } from './operations/put-shopping-list.js';// Register operations to the service hereexport const Service: MyApiService<ServiceContext> = {PutShoppingList,GetShoppingLists,DeleteShoppingList,};
Migrazione degli Handler della Lista della Spesa
Delete Shopping List
import { DeleteItemCommand } from '@aws-sdk/client-dynamodb';import { deleteShoppingListHandler, DeleteShoppingListChainedHandlerFunction, INTERCEPTORS, Response, LoggingInterceptor,} from 'myapi-typescript-runtime';import { ddbClient } from './dynamo-client';
/** * Type-safe handler for the DeleteShoppingList operation */export const deleteShoppingList: DeleteShoppingListChainedHandlerFunction = async (request) => { LoggingInterceptor.getLogger(request).info( 'Start DeleteShoppingList Operation', );
const shoppingListId = request.input.requestParameters.shoppingListId; await ddbClient.send( new DeleteItemCommand({ TableName: 'shopping_list', Key: { shoppingListId: { S: shoppingListId, }, }, }), );
return Response.success({ shoppingListId, });};
/** * Entry point for the AWS Lambda handler for the DeleteShoppingList operation. * The deleteShoppingListHandler method wraps the type-safe handler and manages marshalling inputs and outputs */export const handler = deleteShoppingListHandler( ...INTERCEPTORS, deleteShoppingList,);import { DeleteItemCommand } from '@aws-sdk/client-dynamodb';import { ddbClient } from './dynamo-client.js';import { DeleteShoppingList as DeleteShoppingListOperation } from '../generated/ssdk/index.js';import { ServiceContext } from '../context.js';
/** * Type-safe handler for the DeleteShoppingList operation */export const DeleteShoppingList: DeleteShoppingListOperation<ServiceContext> = async (input, ctx) => { ctx.logger.info( 'Start DeleteShoppingList Operation', );
const shoppingListId = input.shoppingListId; await ddbClient.send( new DeleteItemCommand({ TableName: 'shopping_list', Key: { shoppingListId: { S: shoppingListId!, }, }, }), );
return { shoppingListId, };};Get Shopping Lists
import { DynamoDBClient, QueryCommand, QueryCommandInput, ScanCommand, ScanCommandInput } from '@aws-sdk/client-dynamodb';import { getShoppingListsHandler, GetShoppingListsChainedHandlerFunction, INTERCEPTORS, Response, LoggingInterceptor, ShoppingList,} from 'myapi-typescript-runtime';import { ddbClient } from './dynamo-client';
/** * Type-safe handler for the GetShoppingLists operation */export const getShoppingLists: GetShoppingListsChainedHandlerFunction = async (request) => { LoggingInterceptor.getLogger(request).info('Start GetShoppingLists Operation');
const nextToken = request.input.requestParameters.nextToken; const pageSize = request.input.requestParameters.pageSize; const shoppingListId = request.input.requestParameters.shoppingListId; const commandInput: ScanCommandInput | QueryCommandInput = { TableName: 'shopping_list', ConsistentRead: true, Limit: pageSize, ExclusiveStartKey: nextToken ? fromToken(nextToken) : undefined, ...(shoppingListId ? { KeyConditionExpression: 'shoppingListId = :shoppingListId', ExpressionAttributeValues: { ':shoppingListId': { S: request.input.requestParameters.shoppingListId!, }, }, } : {}), }; const response = await ddbClient.send(shoppingListId ? new QueryCommand(commandInput) : new ScanCommand(commandInput));
return Response.success({ shoppingLists: (response.Items || []) .map<ShoppingList>(item => ({ shoppingListId: item.shoppingListId.S!, name: item.name.S!, shoppingItems: JSON.parse(item.shoppingItems.S || '[]'), })), nextToken: response.LastEvaluatedKey ? toToken(response.LastEvaluatedKey) : undefined, });};
/** * Decode a stringified token * @param token a token passed to the paginated request */const fromToken = <T>(token?: string): T | undefined => token ? (JSON.parse(Buffer.from(decodeURIComponent(token), 'base64').toString()) as T) : undefined;
/** * Encode pagination details into an opaque stringified token * @param paginationToken pagination token details */const toToken = <T>(paginationToken?: T): string | undefined => paginationToken ? encodeURIComponent(Buffer.from(JSON.stringify(paginationToken)).toString('base64')) : undefined;
/** * Entry point for the AWS Lambda handler for the GetShoppingLists operation. * The getShoppingListsHandler method wraps the type-safe handler and manages marshalling inputs and outputs */export const handler = getShoppingListsHandler(...INTERCEPTORS, getShoppingLists);import { QueryCommand, QueryCommandInput, ScanCommand, ScanCommandInput } from '@aws-sdk/client-dynamodb';import { ddbClient } from './dynamo-client.js';import { GetShoppingLists as GetShoppingListsOperation, ShoppingList } from '../generated/ssdk/index.js';import { ServiceContext } from '../context.js';
/** * Type-safe handler for the GetShoppingLists operation */export const GetShoppingLists: GetShoppingListsOperation<ServiceContext> = async (input, ctx) => { ctx.logger.info('Start GetShoppingLists Operation');
const nextToken = input.nextToken; const pageSize = input.pageSize; const shoppingListId = input.shoppingListId; const commandInput: ScanCommandInput | QueryCommandInput = { TableName: 'shopping_list', ConsistentRead: true, Limit: pageSize, ExclusiveStartKey: nextToken ? fromToken(nextToken) : undefined, ...(shoppingListId ? { KeyConditionExpression: 'shoppingListId = :shoppingListId', ExpressionAttributeValues: { ':shoppingListId': { S: input.shoppingListId!, }, }, } : {}), }; const response = await ddbClient.send(shoppingListId ? new QueryCommand(commandInput) : new ScanCommand(commandInput));
return { shoppingLists: (response.Items || []) .map<ShoppingList>(item => ({ shoppingListId: item.shoppingListId.S!, name: item.name.S!, shoppingItems: JSON.parse(item.shoppingItems.S || '[]'), })), nextToken: response.LastEvaluatedKey ? toToken(response.LastEvaluatedKey) : undefined, };};
/** * Decode a stringified token * @param token a token passed to the paginated request */const fromToken = <T>(token?: string): T | undefined => token ? (JSON.parse(Buffer.from(decodeURIComponent(token), 'base64').toString()) as T) : undefined;
/** * Encode pagination details into an opaque stringified token * @param paginationToken pagination token details */const toToken = <T>(paginationToken?: T): string | undefined => paginationToken ? encodeURIComponent(Buffer.from(JSON.stringify(paginationToken)).toString('base64')) : undefined;Put Shopping List
import { randomUUID } from 'crypto';import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb';import { putShoppingListHandler, PutShoppingListChainedHandlerFunction, INTERCEPTORS, Response, LoggingInterceptor,} from 'myapi-typescript-runtime';import { ddbClient } from './dynamo-client';
/** * Type-safe handler for the PutShoppingList operation */export const putShoppingList: PutShoppingListChainedHandlerFunction = async (request) => { LoggingInterceptor.getLogger(request).info('Start PutShoppingList Operation');
const shoppingListId = request.input.body.shoppingListId ?? randomUUID(); await ddbClient.send(new PutItemCommand({ TableName: 'shopping_list', Item: { shoppingListId: { S: shoppingListId, }, name: { S: request.input.body.name, }, shoppingItems: { S: JSON.stringify(request.input.body.shoppingItems || []), }, }, }));
return Response.success({ shoppingListId, });};
/** * Entry point for the AWS Lambda handler for the PutShoppingList operation. * The putShoppingListHandler method wraps the type-safe handler and manages marshalling inputs and outputs */export const handler = putShoppingListHandler(...INTERCEPTORS, putShoppingList);import { randomUUID } from 'crypto';import { PutItemCommand } from '@aws-sdk/client-dynamodb';import { ddbClient } from './dynamo-client.js';import { PutShoppingList as PutShoppingListOperation } from '../generated/ssdk/index.js';import { ServiceContext } from '../context.js';
/** * Type-safe handler for the PutShoppingList operation */export const PutShoppingList: PutShoppingListOperation<ServiceContext> = async (input, ctx) => { ctx.logger.info('Start PutShoppingList Operation');
const shoppingListId = input.shoppingListId ?? randomUUID(); await ddbClient.send(new PutItemCommand({ TableName: 'shopping_list', Item: { shoppingListId: { S: shoppingListId, }, name: { S: input.name!, }, shoppingItems: { S: JSON.stringify(input.shoppingItems || []), }, }, }));
return { shoppingListId, };};Abbiamo generato il progetto API Smithy con il nome api inizialmente perché volevamo che fosse aggiunto a packages/api per coerenza con il progetto PDK. Poiché la nostra API Smithy ora definisce service MyApi invece di service Api, dobbiamo aggiornare tutte le istanze di getApiServiceHandler con getMyApiServiceHandler.
Apporta questa modifica a handler.ts:
import { getApiServiceHandler } from './generated/ssdk/index.js'; import { getMyApiServiceHandler } from './generated/ssdk/index.js';
process.env.POWERTOOLS_METRICS_NAMESPACE = 'Api';process.env.POWERTOOLS_SERVICE_NAME = 'Api';
const tracer = new Tracer();const logger = new Logger();const metrics = new Metrics();
const serviceHandler = getApiServiceHandler(Service); const serviceHandler = getMyApiServiceHandler(Service);E a local-server.ts:
import { getApiServiceHandler } from './generated/ssdk/index.js';import { getMyApiServiceHandler } from './generated/ssdk/index.js';
const PORT = 3001;
const tracer = new Tracer();const logger = new Logger();const metrics = new Metrics();
const serviceHandler = getApiServiceHandler(Service);const serviceHandler = getMyApiServiceHandler(Service);Inoltre, aggiorna packages/api/backend/project.json e modifica metadata.apiName in my-api:
"metadata": { "generator": "ts#smithy-api", "apiName": "api", "apiName": "my-api", "auth": "iam", "modelProject": "@shopping-list/api-model", "ports": [3001] },Verificare con una Build
Sezione intitolata “Verificare con una Build”Ora possiamo compilare il progetto per verificare che la migrazione abbia funzionato finora:
pnpm nx run-many --target buildyarn nx run-many --target buildnpx nx run-many --target buildbunx nx run-many --target buildMigra il Sito Web
Sezione intitolata “Migra il Sito Web”Il CloudscapeReactTsWebsiteProject utilizzato nell’applicazione shopping list configurava un sito web React con CloudScape e autenticazione Cognito integrata.
Questo tipo di progetto sfruttava create-react-app, che ora è deprecato. Per migrare il sito web in questa guida, utilizzeremo il generatore ts#website, che utilizza tecnologie più moderne e supportate, in particolare Vite.
Come parte della migrazione, passeremo anche da React Router configurato in PDK a TanStack Router, che aggiunge ulteriore type-safety al routing del sito web.
Generare un React Website
Sezione intitolata “Generare un React Website”Esegui il generatore ts#website con framework impostato su react per configurare il tuo progetto website in packages/website. Poiché l’applicazione shopping list è costruita con componenti CloudScape, impostiamo anche ux su cloudscape (il valore predefinito è shadcn):
pnpm nx g @aws/nx-plugin:ts#website --name=website --framework=react --ux=cloudscape --no-interactiveyarn nx g @aws/nx-plugin:ts#website --name=website --framework=react --ux=cloudscape --no-interactivenpx nx g @aws/nx-plugin:ts#website --name=website --framework=react --ux=cloudscape --no-interactivebunx nx g @aws/nx-plugin:ts#website --name=website --framework=react --ux=cloudscape --no-interactivePuoi anche eseguire una prova per vedere quali file verrebbero modificati
pnpm nx g @aws/nx-plugin:ts#website --name=website --framework=react --ux=cloudscape --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#website --name=website --framework=react --ux=cloudscape --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#website --name=website --framework=react --ux=cloudscape --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#website --name=website --framework=react --ux=cloudscape --no-interactive --dry-run- Installa il Nx Console VSCode Plugin se non l'hai già fatto
- Apri la console Nx in VSCode
- Clicca su
Generate (UI)nella sezione "Common Nx Commands" - Cerca
@aws/nx-plugin - ts#website - Compila i parametri richiesti
- name: website
- framework: react
- ux: cloudscape
- Clicca su
Generate
Aggiungere l’Autenticazione Cognito
Sezione intitolata “Aggiungere l’Autenticazione Cognito”Il generatore React website sopra non include l’autenticazione cognito per impostazione predefinita come CloudscapeReactTsWebsiteProject, invece viene aggiunta esplicitamente tramite il generatore ts#website#auth.
pnpm nx g @aws/nx-plugin:ts#website#auth --project=website --cognitoDomain=shopping-list --no-interactiveyarn nx g @aws/nx-plugin:ts#website#auth --project=website --cognitoDomain=shopping-list --no-interactivenpx nx g @aws/nx-plugin:ts#website#auth --project=website --cognitoDomain=shopping-list --no-interactivebunx nx g @aws/nx-plugin:ts#website#auth --project=website --cognitoDomain=shopping-list --no-interactivePuoi anche eseguire una prova per vedere quali file verrebbero modificati
pnpm nx g @aws/nx-plugin:ts#website#auth --project=website --cognitoDomain=shopping-list --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#website#auth --project=website --cognitoDomain=shopping-list --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#website#auth --project=website --cognitoDomain=shopping-list --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#website#auth --project=website --cognitoDomain=shopping-list --no-interactive --dry-run- Installa il Nx Console VSCode Plugin se non l'hai già fatto
- Apri la console Nx in VSCode
- Clicca su
Generate (UI)nella sezione "Common Nx Commands" - Cerca
@aws/nx-plugin - ts#website#auth - Compila i parametri richiesti
- project: website
- cognitoDomain: shopping-list
- Clicca su
Generate
Questo aggiunge componenti React che gestiscono i reindirizzamenti appropriati per garantire che gli utenti effettuino il login utilizzando l’interfaccia utente ospitata di Cognito. Questo aggiunge anche un costrutto CDK per distribuire le risorse Cognito in packages/common/constructs, chiamato UserIdentity.
Connettere il Website all’API
Sezione intitolata “Connettere il Website all’API”In PDK potevi passare i progetti Projen forniti l’uno all’altro per attivare la generazione del codice di integrazione. Questo veniva utilizzato nell’applicazione shopping list per configurare il sito web in modo che potesse integrarsi con l’API.
Con Nx Plugin for AWS, l’integrazione API è supportata tramite il generatore connection. Successivamente, utilizziamo questo generatore in modo che il nostro sito web possa invocare la nostra API Smithy:
pnpm nx g @aws/nx-plugin:connection --sourceProject=website --targetProject=api --no-interactiveyarn nx g @aws/nx-plugin:connection --sourceProject=website --targetProject=api --no-interactivenpx nx g @aws/nx-plugin:connection --sourceProject=website --targetProject=api --no-interactivebunx nx g @aws/nx-plugin:connection --sourceProject=website --targetProject=api --no-interactivePuoi anche eseguire una prova per vedere quali file verrebbero modificati
pnpm nx g @aws/nx-plugin:connection --sourceProject=website --targetProject=api --no-interactive --dry-runyarn nx g @aws/nx-plugin:connection --sourceProject=website --targetProject=api --no-interactive --dry-runnpx nx g @aws/nx-plugin:connection --sourceProject=website --targetProject=api --no-interactive --dry-runbunx nx g @aws/nx-plugin:connection --sourceProject=website --targetProject=api --no-interactive --dry-run- Installa il Nx Console VSCode Plugin se non l'hai già fatto
- Apri la console Nx in VSCode
- Clicca su
Generate (UI)nella sezione "Common Nx Commands" - Cerca
@aws/nx-plugin - connection - Compila i parametri richiesti
- sourceProject: website
- targetProject: api
- Clicca su
Generate
Questo genera i provider client necessari e i target di build per consentire al tuo sito web di chiamare la tua API tramite un client TypeScript generato.
Aggiungere la Dipendenza AWS Northstar
Sezione intitolata “Aggiungere la Dipendenza AWS Northstar”Il CloudscapeReactTsWebsiteProject includeva automaticamente una dipendenza da @aws-northstar/ui che viene utilizzata nella nostra applicazione shopping list, quindi la aggiungiamo al progetto @shopping-list/website:
pnpm add @aws-northstar/ui --filter websiteyarn workspace @shopping-list/website add @aws-northstar/uinpm install --legacy-peer-deps @aws-northstar/ui -w packages/websitebun add @aws-northstar/ui --cwd packages/website@aws-northstar/ui include un componente editor di codice che dipende da ace-builds, utilizzando un’importazione specifica per webpack che Vite non può risolvere. Poiché la nostra applicazione shopping list non utilizza questo componente, lo escludiamo dal bundle aggiungendolo alla configurazione external all’interno delle opzioni build esistenti in packages/website/vite.config.mts:
build: { outDir: '../../dist/packages/website/bundle', emptyOutDir: true, reportCompressedSize: true, commonjsOptions: { transformMixedEsModules: true, }, rollupOptions: { external: ['ace-builds/webpack-resolver'], }, },Spostare i Componenti e le Pagine
Sezione intitolata “Spostare i Componenti e le Pagine”L’applicazione shopping list ha un componente chiamato CreateItem e due pagine, ShoppingList e ShoppingLists. Migreremo questi al nuovo sito web, apportando alcune modifiche poiché stiamo utilizzando TanStack Router e il generatore di codice client TypeScript di Nx Plugin for AWS.
-
Copia
packages/website/src/components/CreateItem/index.tsxdal progetto PDK nella stessa identica posizione nel nuovo progetto. -
Copia
packages/website/src/pages/ShoppingLists/index.tsxinpackages/website/src/routes/index.tsx, poichéShoppingListsè la nostra home page e utilizziamo il routing basato su file con TanStack router. -
Copia
packages/website/src/pages/ShoppingList/index.tsxinpackages/website/src/routes/$shoppingListId.tsx, poichéShoppingListera la pagina che vogliamo mostrare sulla route/:shoppingListId.
Nota che ora avrai alcuni errori di build visibili nel tuo IDE, dovremo apportare alcune modifiche aggiuntive per adattarci al nuovo framework, descritte di seguito.
Migrare da React Router a TanStack Router
Sezione intitolata “Migrare da React Router a TanStack Router”Poiché stiamo utilizzando il routing basato su file, possiamo utilizzare il server di sviluppo locale del sito web per gestire automaticamente la generazione della configurazione delle route.
Avviamo il server del sito web locale:
pnpm nx dev websiteyarn nx dev websitenpx nx dev websitebunx nx dev websiteVedrai alcuni errori, ma il server del sito web locale dovrebbe avviarsi sulla porta 4200, così come il server API Smithy locale sulla porta 3001.
Segui i passaggi seguenti sia in routes/index.tsx che in routes/$shoppingListId.tsx per migrare a TanStack Router:
-
Aggiungi
createFileRouteper registrare ogni route:import { createFileRoute } from "@tanstack/react-router";...export default ShoppingLists;export const Route = createFileRoute('/')({component: ShoppingLists,});import { createFileRoute } from "@tanstack/react-router";...export default ShoppingList;export const Route = createFileRoute('/$shoppingListId')({component: ShoppingList,});Dopo aver salvato il file, noterai che gli errori di tipo con la chiamata a
createFileRoutesono scomparsi. -
Sostituisci l’hook
useNavigate.Aggiorna l’importazione:
import { useNavigate } from 'react-router-dom';import { useNavigate } from '@tanstack/react-router';Aggiorna le chiamate al metodo
navigate(restituito dauseNavigate) per passare le route type-safe:navigate(`/${cell.shoppingListId}`);navigate({to: '/$shoppingListId',params: { shoppingListId: cell.shoppingListId },}); -
Sostituisci l’hook
useParams.Rimuovi l’importazione:
import { useParams } from 'react-router-dom';Aggiorna le chiamate a
useParamscon l’hook fornito dallaRoutecreata sopra. Ora sono type-safe!const { shoppingListId } = useParams();const { shoppingListId } = Route.useParams();
Correggere le Importazioni dei Componenti
Sezione intitolata “Correggere le Importazioni dei Componenti”Poiché i nostri file di route non sono annidati così profondamente nell’albero dei file come lo erano nel nostro progetto PDK, dobbiamo correggere l’importazione per CreateItem sia in routes/index.tsx che in routes/$shoppingListId.tsx:
import CreateItem from "../../components/CreateItem";import CreateItem from "../components/CreateItem";Anche AppLayoutContext è fornito in una posizione leggermente diversa nel nostro nuovo progetto:
import { AppLayoutContext } from "../../layouts/App";import { AppLayoutContext } from "../components/AppLayout";Migrare per Utilizzare il Nuovo Client TypeScript Generato
Sezione intitolata “Migrare per Utilizzare il Nuovo Client TypeScript Generato”Ci stiamo avvicinando ora! Successivamente, dobbiamo migrare per utilizzare il client TypeScript fornito da Nx Plugin for AWS, che ha alcuni miglioramenti rispetto a Type Safe API. Per ottenere questo, segui i passaggi seguenti
-
Importa il nuovo client e i tipi generati invece di quelli vecchi, per esempio:
import {ShoppingList,usePutShoppingList,useDeleteShoppingList,useGetShoppingLists,} from "myapi-typescript-react-query-hooks";import { ShoppingList } from "../generated/my-api/types.gen";import { useMyApi } from "../hooks/useMyApi";import { useInfiniteQuery, useMutation } from "@tanstack/react-query";Nota che
routes/$shoppingListId.tsximporta il tipoShoppingListcome_ShoppingList- in quel file dovremmo fare lo stesso, ma importando nuovamente datypes.gen.Nota anche che importiamo gli hook rilevanti direttamente da
@tanstack/react-query, poiché il client generato fornisce metodi per generare opzioni per gli hook TanStack query, piuttosto che wrapper di hook. -
Istanzia i nuovi hook TanStack Query, per esempio:
const getShoppingLists = useGetShoppingLists({ pageSize: PAGE_SIZE });const putShoppingList = usePutShoppingList();const deleteShoppingList = useDeleteShoppingList();const api = useMyApi();const getShoppingLists = useInfiniteQuery(api.getShoppingLists.infiniteQueryOptions({ pageSize: PAGE_SIZE },{ getNextPageParam: (p) => p.nextToken },),);const putShoppingList = useMutation(api.putShoppingList.mutationOptions());const deleteShoppingList = useMutation(api.deleteShoppingList.mutationOptions(),); -
Rimuovi il wrapper
<operation>RequestContentper le chiamate alle operazioni che accettano parametri nel corpo della richiesta:await putShoppingList.mutateAsync({putShoppingListRequestContent: {name: item,},});
Migrare da TanStack Query v4 a v5
Sezione intitolata “Migrare da TanStack Query v4 a v5”Ci sono alcuni errori rimasti da correggere a causa delle differenze tra TanStack Query v4 (utilizzato da PDK) e v5 che il generatore connection ha aggiunto:
-
Sostituisci
isLoadingconisPendingper le mutazioni, per esempio:putShoppingList.isLoadingputShoppingList.isPending -
L’applicazione shopping list utilizzava
InfiniteQueryTableda@aws-northstar/uiche si aspetta un tipo da TanStack Query v4. Questo funziona effettivamente con query infinite da v5, quindi possiamo semplicemente sopprimere l’errore di tipo:<InfiniteQueryTablequery={getShoppingLists}query={getShoppingLists as any}
Visitare il Sito Web Locale
Sezione intitolata “Visitare il Sito Web Locale”Ora puoi visitare il sito web locale su http://localhost:4200/
Il sito web dovrebbe caricarsi ora che tutto è stato migrato! Poiché l’unica infrastruttura su cui si basa l’applicazione shopping list oltre ad API, Website e Identity è la tabella DynamoDB - se hai una tabella DynamoDB denominata shopping_list nella regione e credenziali AWS locali che possono accedervi, il sito web sarà completamente funzionale!
Se no, va bene, migreremo l’infrastruttura successivamente.
Migrazione della Pagina Shopping List
Pagina Shopping Lists
/* eslint-disable @typescript-eslint/no-floating-promises */import { InfiniteQueryTable } from "@aws-northstar/ui/components";import { Button, Header, Link, SpaceBetween, TableProps,} from "@cloudscape-design/components";import { ShoppingList, usePutShoppingList, useDeleteShoppingList, useGetShoppingLists,} from "myapi-typescript-react-query-hooks";import { useContext, useEffect, useMemo, useState } from "react";import { useNavigate } from "react-router-dom";import CreateItem from "../../components/CreateItem";import { AppLayoutContext } from "../../layouts/App";
const PAGE_SIZE = 50;
/** * Component to render the ShoppingLists "/" route. */const ShoppingLists: React.FC = () => { const [visibleModal, setVisibleModal] = useState(false); const [selectedShoppingList, setSelectedShoppingList] = useState< ShoppingList[] >([]); const getShoppingLists = useGetShoppingLists({ pageSize: PAGE_SIZE }); const putShoppingList = usePutShoppingList(); const deleteShoppingList = useDeleteShoppingList(); const navigate = useNavigate(); const { setAppLayoutProps } = useContext(AppLayoutContext);
useEffect(() => { setAppLayoutProps({ contentType: "table", }); }, [setAppLayoutProps]);
const columnDefinitions = useMemo< TableProps.ColumnDefinition<ShoppingList>[] >( () => [ { id: "shoppingListId", isRowHeader: true, header: "Shopping List Id", cell: (cell) => ( <Link href={`/${cell.shoppingListId}`} onFollow={(e) => { e.preventDefault(); navigate(`/${cell.shoppingListId}`); }} > {cell.shoppingListId} </Link> ), }, { id: "name", header: "Name", cell: (cell) => cell.name, }, { id: "shoppingItems", header: "Shopping Items", cell: (cell) => `${cell.shoppingItems?.length || 0} Items.`, }, ], [navigate], );
return ( <> <CreateItem title="Create Shopping List" callback={async (item) => { await putShoppingList.mutateAsync({ putShoppingListRequestContent: { name: item, }, }); getShoppingLists.refetch(); }} isLoading={putShoppingList.isLoading} visibleModal={visibleModal} setVisibleModal={setVisibleModal} /> <InfiniteQueryTable query={getShoppingLists} itemsKey="shoppingLists" pageSize={PAGE_SIZE} selectionType="single" stickyHeader={true} selectedItems={selectedShoppingList} onSelectionChange={(e) => setSelectedShoppingList(e.detail.selectedItems) } header={ <Header variant="awsui-h1-sticky" actions={ <SpaceBetween size="xs" direction="horizontal"> <Button loading={deleteShoppingList.isLoading} data-testid="header-btn-delete" disabled={selectedShoppingList.length === 0} onClick={async () => { await deleteShoppingList.mutateAsync({ shoppingListId: selectedShoppingList![0].shoppingListId, }); setSelectedShoppingList([]); getShoppingLists.refetch(); }} > Delete </Button> <Button data-testid="header-btn-create" variant="primary" onClick={() => setVisibleModal(true)} > Create Shopping List </Button> </SpaceBetween> } > Shopping Lists </Header> } variant="full-page" columnDefinitions={columnDefinitions} /> </> );};
export default ShoppingLists;/* eslint-disable @typescript-eslint/no-floating-promises */import { InfiniteQueryTable } from "@aws-northstar/ui/components";import { Button, Header, Link, SpaceBetween, TableProps,} from "@cloudscape-design/components";import { useContext, useEffect, useMemo, useState } from "react";import { useNavigate } from "@tanstack/react-router";import CreateItem from "../components/CreateItem";import { AppLayoutContext } from "../components/AppLayout";import { createFileRoute } from "@tanstack/react-router";import { ShoppingList } from "../generated/my-api/types.gen";import { useMyApi } from "../hooks/useMyApi";import { useInfiniteQuery, useMutation } from "@tanstack/react-query";
const PAGE_SIZE = 50;
/** * Component to render the ShoppingLists "/" route. */const ShoppingLists: React.FC = () => { const [visibleModal, setVisibleModal] = useState(false); const [selectedShoppingList, setSelectedShoppingList] = useState< ShoppingList[] >([]); const api = useMyApi(); const getShoppingLists = useInfiniteQuery( api.getShoppingLists.infiniteQueryOptions( { pageSize: PAGE_SIZE }, { getNextPageParam: (res) => res.nextToken }, ), ); const putShoppingList = useMutation(api.putShoppingList.mutationOptions()); const deleteShoppingList = useMutation( api.deleteShoppingList.mutationOptions(), ); const navigate = useNavigate(); const { setAppLayoutProps } = useContext(AppLayoutContext);
useEffect(() => { setAppLayoutProps({ contentType: "table", }); }, [setAppLayoutProps]);
const columnDefinitions = useMemo< TableProps.ColumnDefinition<ShoppingList>[] >( () => [ { id: "shoppingListId", isRowHeader: true, header: "Shopping List Id", cell: (cell) => ( <Link href={`/${cell.shoppingListId}`} onFollow={(e) => { e.preventDefault(); navigate({ to: '/$shoppingListId', params: { shoppingListId: cell.shoppingListId },}); }} > {cell.shoppingListId} </Link> ), }, { id: "name", header: "Name", cell: (cell) => cell.name, }, { id: "shoppingItems", header: "Shopping Items", cell: (cell) => `${cell.shoppingItems?.length || 0} Items.`, }, ], [navigate], );
return ( <> <CreateItem title="Create Shopping List" callback={async (item) => { await putShoppingList.mutateAsync({ name: item, }); getShoppingLists.refetch(); }} isLoading={putShoppingList.isPending} visibleModal={visibleModal} setVisibleModal={setVisibleModal} /> <InfiniteQueryTable query={getShoppingLists as any} itemsKey="shoppingLists" pageSize={PAGE_SIZE} selectionType="single" stickyHeader={true} selectedItems={selectedShoppingList} onSelectionChange={(e) => setSelectedShoppingList(e.detail.selectedItems) } header={ <Header variant="awsui-h1-sticky" actions={ <SpaceBetween size="xs" direction="horizontal"> <Button loading={deleteShoppingList.isPending} data-testid="header-btn-delete" disabled={selectedShoppingList.length === 0} onClick={async () => { await deleteShoppingList.mutateAsync({ shoppingListId: selectedShoppingList![0].shoppingListId, }); setSelectedShoppingList([]); getShoppingLists.refetch(); }} > Delete </Button> <Button data-testid="header-btn-create" variant="primary" onClick={() => setVisibleModal(true)} > Create Shopping List </Button> </SpaceBetween> } > Shopping Lists </Header> } variant="full-page" columnDefinitions={columnDefinitions} /> </> );};
export const Route = createFileRoute('/')({ component: ShoppingLists,});Pagina Shopping List
/* eslint-disable @typescript-eslint/no-floating-promises */import { Board, BoardItem, BoardProps,} from "@cloudscape-design/board-components";import { Button, Container, ContentLayout, Header, SpaceBetween, Spinner,} from "@cloudscape-design/components";import { ShoppingList as _ShoppingList, usePutShoppingList, useGetShoppingLists,} from "myapi-typescript-react-query-hooks";import { useEffect, useState } from "react";import { useParams } from "react-router-dom";import CreateItem from "../../components/CreateItem";
type ListItem = { name: string };
/** * Component to render a singular Shopping List "/:shoppingListId" route. */const ShoppingList: React.FC = () => { const { shoppingListId } = useParams(); const [visibleModal, setVisibleModal] = useState(false); const getShoppingLists = useGetShoppingLists({ shoppingListId }); const putShoppingList = usePutShoppingList(); const shoppingList: _ShoppingList | undefined = getShoppingLists.data?.pages[0].shoppingLists[0]!; const [shoppingItems, setShoppingItems] = useState<BoardProps.Item<ListItem>[]>();
useEffect(() => { setShoppingItems( shoppingList?.shoppingItems?.map((i) => ({ id: i, definition: { minColumnSpan: 4 }, data: { name: i }, })), ); }, [shoppingList?.shoppingItems]);
return ( <ContentLayout header={ <Header variant="awsui-h1-sticky" actions={ <SpaceBetween size="xs" direction="horizontal"> <Button data-testid="header-btn-create" variant="primary" onClick={() => setVisibleModal(true)} > Add Item </Button> </SpaceBetween> } > Shopping list: {shoppingList?.name} </Header> } > <CreateItem isLoading={false} title="Add Item" callback={async (item) => { const items = [ ...(shoppingItems || []), { id: item, definition: { minColumnSpan: 4 }, data: { name: item }, }, ]; setShoppingItems(items); putShoppingList.mutate({ putShoppingListRequestContent: { name: shoppingList.name, shoppingListId: shoppingList.shoppingListId, shoppingItems: items.map((i) => i.data.name), }, }); }} visibleModal={visibleModal} setVisibleModal={setVisibleModal} /> <Container> {!shoppingList ? ( <Spinner /> ) : ( <Board<ListItem> onItemsChange={(event) => { const items = event.detail.items as BoardProps.Item<ListItem>[]; setShoppingItems(items); putShoppingList.mutate({ putShoppingListRequestContent: { name: shoppingList.name, shoppingListId: shoppingList.shoppingListId, shoppingItems: items.map((i) => i.data.name), }, }); }} items={shoppingItems || []} renderItem={(item, actions) => ( <BoardItem header={item.data.name} settings={ <Button iconName="close" variant="icon" onClick={actions.removeItem} /> } i18nStrings={{ dragHandleAriaLabel: "Drag handle", dragHandleAriaDescription: "Use Space or Enter to activate drag, arrow keys to move, Space or Enter to submit, or Escape to discard.", resizeHandleAriaLabel: "Resize handle", resizeHandleAriaDescription: "Use Space or Enter to activate resize, arrow keys to move, Space or Enter to submit, or Escape to discard.", }} /> )} i18nStrings={{ liveAnnouncementDndCommitted: () => "", liveAnnouncementDndDiscarded: () => "", liveAnnouncementDndItemInserted: () => "", liveAnnouncementDndItemReordered: () => "", liveAnnouncementDndItemResized: () => "", liveAnnouncementDndStarted: () => "", liveAnnouncementItemRemoved: () => "", navigationAriaLabel: "", navigationItemAriaLabel: () => "", }} empty={<></>} /> )} </Container> </ContentLayout> );};
export default ShoppingList;// routes/$shoppingListId.tsx/* eslint-disable @typescript-eslint/no-floating-promises */import { Board, BoardItem, BoardProps,} from "@cloudscape-design/board-components";import { Button, Container, ContentLayout, Header, SpaceBetween, Spinner,} from "@cloudscape-design/components";import { useEffect, useState } from "react";import CreateItem from "../components/CreateItem";import { createFileRoute } from "@tanstack/react-router";import { useMyApi } from "../hooks/useMyApi";import { useInfiniteQuery, useMutation } from "@tanstack/react-query";import { ShoppingList as _ShoppingList } from "../generated/my-api/types.gen";
type ListItem = { name: string };
/** * Component to render a singular Shopping List "/:shoppingListId" route. */const ShoppingList: React.FC = () => { const { shoppingListId } = Route.useParams(); const [visibleModal, setVisibleModal] = useState(false); const api = useMyApi(); const getShoppingLists = useInfiniteQuery( api.getShoppingLists.infiniteQueryOptions( { shoppingListId }, { getNextPageParam: (p) => p.nextToken }, ), ); const putShoppingList = useMutation(api.putShoppingList.mutationOptions()); const shoppingList: _ShoppingList | undefined = getShoppingLists.data?.pages?.[0]?.shoppingLists?.[0]; const [shoppingItems, setShoppingItems] = useState<BoardProps.Item<ListItem>[]>();
useEffect(() => { setShoppingItems( shoppingList?.shoppingItems?.map((i) => ({ id: i, definition: { minColumnSpan: 4 }, data: { name: i }, })), ); }, [shoppingList?.shoppingItems]);
return ( <ContentLayout header={ <Header variant="awsui-h1-sticky" actions={ <SpaceBetween size="xs" direction="horizontal"> <Button data-testid="header-btn-create" variant="primary" onClick={() => setVisibleModal(true)} > Add Item </Button> </SpaceBetween> } > Shopping list: {shoppingList?.name} </Header> } > <CreateItem isLoading={false} title="Add Item" callback={async (item) => { const items = [ ...(shoppingItems || []), { id: item, definition: { minColumnSpan: 4 }, data: { name: item }, }, ]; setShoppingItems(items); putShoppingList.mutate({ name: shoppingList?.name ?? 'my list', shoppingListId: shoppingList?.shoppingListId, shoppingItems: items.map((i) => i.data.name), }); }} visibleModal={visibleModal} setVisibleModal={setVisibleModal} /> <Container> {!shoppingList ? ( <Spinner /> ) : ( <Board<ListItem> onItemsChange={(event) => { const items = event.detail.items as BoardProps.Item<ListItem>[]; setShoppingItems(items); putShoppingList.mutate({ name: shoppingList.name, shoppingListId: shoppingList.shoppingListId, shoppingItems: items.map((i) => i.data.name), }); }} items={shoppingItems || []} renderItem={(item, actions) => ( <BoardItem header={item.data.name} settings={ <Button iconName="close" variant="icon" onClick={actions.removeItem} /> } i18nStrings={{ dragHandleAriaLabel: "Drag handle", dragHandleAriaDescription: "Use Space or Enter to activate drag, arrow keys to move, Space or Enter to submit, or Escape to discard.", resizeHandleAriaLabel: "Resize handle", resizeHandleAriaDescription: "Use Space or Enter to activate resize, arrow keys to move, Space or Enter to submit, or Escape to discard.", }} /> )} i18nStrings={{ liveAnnouncementDndCommitted: () => "", liveAnnouncementDndDiscarded: () => "", liveAnnouncementDndItemInserted: () => "", liveAnnouncementDndItemReordered: () => "", liveAnnouncementDndItemResized: () => "", liveAnnouncementDndStarted: () => "", liveAnnouncementItemRemoved: () => "", navigationAriaLabel: "", navigationItemAriaLabel: () => "", }} empty={<></>} /> )} </Container> </ContentLayout> );};
export const Route = createFileRoute('/$shoppingListId')({ component: ShoppingList,});Migra l’Infrastruttura
Sezione intitolata “Migra l’Infrastruttura”L’ultimo progetto che dobbiamo migrare per la nostra applicazione shopping list è l’InfrastructureTsProject. Questo è un progetto TypeScript CDK, per il quale l’equivalente di Nx Plugin for AWS è il generatore ts#infra.
Oltre ai progetti Projen, PDK forniva anche costrutti CDK da cui questi progetti dipendevano. Migreremo l’applicazione shopping list anche da questi costrutti CDK, a favore di quelli generati da Nx Plugin for AWS.
Generare un Progetto di Infrastruttura TypeScript CDK
Sezione intitolata “Generare un Progetto di Infrastruttura TypeScript CDK”Esegui il generatore ts#infra per configurare il tuo progetto di infrastruttura in packages/infra:
pnpm nx g @aws/nx-plugin:ts#infra --name=infra --no-interactiveyarn nx g @aws/nx-plugin:ts#infra --name=infra --no-interactivenpx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactivebunx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactivePuoi anche eseguire una prova per vedere quali file verrebbero modificati
pnpm nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-run- Installa il Nx Console VSCode Plugin se non l'hai già fatto
- Apri la console Nx in VSCode
- Clicca su
Generate (UI)nella sezione "Common Nx Commands" - Cerca
@aws/nx-plugin - ts#infra - Compila i parametri richiesti
- name: infra
- Clicca su
Generate
Migrare l’Infrastruttura CDK
Sezione intitolata “Migrare l’Infrastruttura CDK”L’applicazione shopping list PDK istanziava i seguenti costrutti all’interno dello stack dell’applicazione CDK:
DatabaseConstructper la tabella DynamoDB che memorizza le shopping listUserIdentityper le risorse Cognito, importato direttamente da PDKMyApiper il deployment dell’API Smithy, che utilizzava il costrutto TypeScript CDK generato con integrazioni type-safe, dipendendo dal costrutto CDKTypeSafeRestApidi PDK sotto il cofano.Websiteper il deployment del sito web, che avvolgeva il costrutto CDKStaticWebsitedi PDK.
Successivamente, migreremo ciascuno di questi al nuovo progetto.
Copiare l’Application Stack
Sezione intitolata “Copiare l’Application Stack”Copia packages/infra/src/stacks/application-stack.ts dall’applicazione shopping list PDK nella stessa identica posizione nel tuo nuovo progetto. Vedrai alcuni errori TypeScript che risolveremo di seguito.
Copiare il Database Construct
Sezione intitolata “Copiare il Database Construct”L’applicazione shopping list PDK aveva un costrutto Database in packages/src/constructs/database.ts. Copialo nella stessa identica posizione nel tuo nuovo progetto.
Poiché Nx Plugin for AWS utilizza Checkov per i test di sicurezza, che è un po’ più rigoroso di PDK Nag, dobbiamo anche aggiungere alcune soppressioni:
import { suppressRules } from '@shopping-list/common-constructs';...suppressRules( this.shoppingListTable, ['CKV_AWS_28', 'CKV_AWS_119'], 'Backup and KMS key not required for this project',);In application-stack.ts, aggiorna l’import per il DatabaseConstruct per utilizzare la sintassi ESM:
import { DatabaseConstruct } from '../constructs/database';import { DatabaseConstruct } from '../constructs/database.js';Migrare il UserIdentity Construct
Sezione intitolata “Migrare il UserIdentity Construct”Il costrutto UserIdentity può generalmente essere sostituito senza modifiche regolando gli import.
import { UserIdentity } from "@aws/pdk/identity";import { UserIdentity } from '@shopping-list/common-constructs';...const userIdentity = new UserIdentity(this, `${id}UserIdentity`);Nota che i costrutti sottostanti utilizzati dal nuovo costrutto UserIdentity sono forniti direttamente da aws-cdk-lib, mentre PDK utilizzava @aws-cdk/aws-cognito-identitypool-alpha.
Migrare l’API Construct
Sezione intitolata “Migrare l’API Construct”L’applicazione shopping list PDK aveva un costrutto in constructs/apis/myapi.ts che istanziava un costrutto CDK che Type Safe API generava dal tuo modello Smithy.
Oltre a questo costrutto, poiché il progetto PDK utilizzava il trait @handler, venivano generati anche costrutti CDK di funzioni lambda.
Come Type Safe API, Nx Plugin for AWS fornisce type-safety per le integrazioni basate sul tuo modello Smithy, tuttavia è ottenuto in un modo molto più semplice e flessibile. Invece di generare un intero costrutto CDK al momento della build, vengono generati solo “metadati” minimi, che il packages/common/constructs/src/app/apis/api.ts utilizza in modo generico. Puoi saperne di più su come utilizzare il costrutto nella guida del generatore ts#smithy-api.
Segui i passaggi seguenti:
-
Istanzia il costrutto
Apiinapplication-stack.tsstacks/application-stack.ts import { MyApi } from "../constructs/apis/myapi";import { Api } from '@shopping-list/common-constructs';...const myapi = new MyApi(this, "MyApi", {databaseConstruct,userIdentity,});const api = new Api(this, 'MyApi', {integrations: Api.defaultIntegrations(this).build(),});Nota qui che usiamo
Api.defaultIntegrations(this).build()- il comportamento predefinito è creare una funzione lambda per ogni operazione nella nostra API, che è lo stesso comportamento che avevamo inmyapi.ts. -
Concedi i permessi alle funzioni lambda per accedere alla tabella DynamoDB.
Nell’applicazione shopping list PDK, il
DatabaseConsructveniva passato aMyApi, e gestiva l’aggiunta dei permessi rilevanti a ciascun costrutto di funzione generato. Lo faremo direttamente nel fileapplication-stack.tsaccedendo alla proprietàintegrationstype-safe del costruttoApi:stacks/application-stack.ts // Grant our lambda functions scoped access to call DynamodatabaseConstruct.shoppingListTable.grantReadData(api.integrations.getShoppingLists.handler,);[api.integrations.putShoppingList.handler,api.integrations.deleteShoppingList.handler,].forEach((f) => databaseConstruct.shoppingListTable.grantWriteData(f)); -
Concedi i permessi agli utenti autenticati per invocare l’API.
All’interno del
myapi.tsdell’applicazione PDK, agli utenti autenticati venivano anche concessi i permessi IAM per invocare l’API. Faremo l’equivalente inapplication-stack.ts:stacks/application-stack.ts api.grantInvokeAccess(userIdentity.identityPool.authenticatedRole);
Migrare il Website Construct
Sezione intitolata “Migrare il Website Construct”Infine, aggiungiamo il costrutto Website da packages/common/constructs/src/app/static-websites/website.ts a application-stack.ts, poiché questo è l’equivalente del packages/infra/src/constructs/websites/website.ts dell’applicazione shopping list PDK.
import { Website } from "../constructs/websites/website";import { Website } from '@shopping-list/common-constructs';...new Website(this, "Website", { userIdentity, myapi,});new Website(this, 'Website');Nota che non passiamo l’identità o l’API al sito web - la configurazione runtime è gestita all’interno di ogni costrutto fornito da Nx Plugin for AWS, dove UserIdentity e Api registrano i valori necessari, e Website gestisce il deployment su /runtime-config.json sul tuo sito web statico.
Costruiamo ora il progetto ora che abbiamo migrato tutte le parti rilevanti della codebase al nostro nuovo progetto.
pnpm nx run-many --target buildyarn nx run-many --target buildnpx nx run-many --target buildbunx nx run-many --target buildOra che abbiamo la nostra codebase completamente migrata, possiamo occuparci del deploy. Ci sono due percorsi che possiamo seguire a questo punto.
Tutte Nuove Risorse (Semplice)
Sezione intitolata “Tutte Nuove Risorse (Semplice)”L’approccio più semplice è trattare questa come un’applicazione completamente nuova, il che significa che “ricominceremo da capo” con una nuova tabella DynamoDB e un nuovo Cognito User Pool - perdendo tutti gli utenti e le loro liste della spesa. Per questo approccio, semplicemente:
-
Elimina la tabella DynamoDB denominata
shopping_list -
Esegui il deploy della nuova applicazione:
Terminal window pnpm nx deploy infra shopping-list-infra-sandbox/*Terminal window yarn nx deploy infra shopping-list-infra-sandbox/*Terminal window npx nx deploy infra shopping-list-infra-sandbox/*Terminal window bunx nx deploy infra shopping-list-infra-sandbox/*
🎉 E abbiamo finito! 🎉
Migrare le Risorse Stateful Esistenti senza Interruzioni (Più Complesso)
Sezione intitolata “Migrare le Risorse Stateful Esistenti senza Interruzioni (Più Complesso)”In realtà, è più probabile che tu voglia migrare le risorse AWS esistenti in modo che siano gestite dalla nuova codebase, evitando al contempo qualsiasi downtime per i tuoi clienti.
Per la nostra applicazione shopping list, le risorse stateful che ci interessano sono la tabella DynamoDB che contiene le liste della spesa dei nostri utenti e lo User Pool che contiene i dettagli di tutti i nostri utenti registrati. Il nostro piano ad alto livello sarà quello di mantenere queste due risorse chiave e spostarle in modo che siano gestite dal nostro nuovo stack, quindi aggiornare il DNS per puntare al nostro nuovo sito web (e API se esposta ai clienti).
-
Aggiorna la tua nuova applicazione per fare riferimento alle risorse esistenti che desideri mantenere.
Per l’applicazione shopping list, lo facciamo per la tabella DynamoDB
constructs/database.ts this.shoppingListTable = new Table(this, 'ShoppingList', {...this.shoppingListTable = Table.fromTableName(this,'ShoppingList','shopping_list',);E per il Cognito User Pool
packages/common/constructs/src/core/user-identity.ts this.userPool = this.createUserPool();this.userPool = UserPool.fromUserPoolId(this,'UserPool','<your-user-pool-id>',); -
Esegui il build e il deploy della nuova applicazione:
Terminal window pnpm nx run-many --target buildTerminal window yarn nx run-many --target buildTerminal window npx nx run-many --target buildTerminal window bunx nx run-many --target buildTerminal window pnpm nx deploy infra shopping-list-infra-sandbox/*Terminal window yarn nx deploy infra shopping-list-infra-sandbox/*Terminal window npx nx deploy infra shopping-list-infra-sandbox/*Terminal window bunx nx deploy infra shopping-list-infra-sandbox/*Ora abbiamo la nostra nuova applicazione attiva che fa riferimento alle risorse esistenti, non ancora in ricezione di traffico.
-
Esegui test di integrazione completi per assicurarti che la nuova applicazione funzioni come previsto. Per l’applicazione shopping list, carica il sito web e verifica di poter accedere e creare, visualizzare, modificare ed eliminare liste della spesa.
-
Ripristina le modifiche che fanno riferimento alle risorse esistenti nella tua nuova applicazione, ma non eseguire ancora il deploy.
constructs/database.ts this.shoppingListTable = new Table(this, 'ShoppingList', {...this.shoppingListTable = Table.fromTableName(this,'ShoppingList','shopping_list',);E per il Cognito User Pool
packages/common/constructs/src/core/user-identity.ts this.userPool = this.createUserPool();this.userPool = UserPool.fromUserPoolId(this,'UserPool','<your-user-pool-id>',);E poi esegui un build
Terminal window pnpm nx run-many --target buildTerminal window yarn nx run-many --target buildTerminal window npx nx run-many --target buildTerminal window bunx nx run-many --target build -
Usa
cdk importnella cartellapackages/infradella tua nuova applicazione per vedere quali risorse ci verrà richiesto di importare.New Application cd packages/infrapnpm exec cdk import shopping-list-infra-sandbox/Application --forceProcedi attraverso i prompt premendo invio. L’importazione fallirà perché le risorse sono gestite da un altro stack - questo è previsto, abbiamo fatto questo passaggio solo per confermare quali risorse dovremo mantenere. Vedrai un output come questo:
Terminal window shopping-list-infra-sandbox/Application/ApplicationUserIdentity/UserPool/smsRole/Resource (AWS::IAM::Role): enter RoleName (empty to skip)shopping-list-infra-sandbox/Application/ApplicationUserIdentity/UserPool/Resource (AWS::Cognito::UserPool): enter UserPoolId (empty to skip)shopping-list-infra-sandbox/Application/Database/ShoppingList/Resource (AWS::DynamoDB::Table): import with TableName=shopping_list (y/n) yQuesto ci dice che ci sono in realtà 3 risorse che dovremo importare nel nostro nuovo stack.
-
Aggiorna il tuo vecchio progetto PDK per impostare
RemovalPolicysuRETAINper le risorse scoperte dal passaggio precedente. Al momento della scrittura questo è il default sia per lo User Pool che per la tabella DynamoDB, ma dobbiamo aggiornarlo per l’SMS Role che abbiamo scoperto sopra:application-stack.ts const userIdentity = new UserIdentity(this, `${id}UserIdentity`, {userPool,});const smsRole = userIdentity.userPool.node.findAll().filter(c => CfnResource.isCfnResource(c) &&c.node.path.includes('/smsRole/'))[0] as CfnResource;smsRole.applyRemovalPolicy(RemovalPolicy.RETAIN); -
Esegui il deploy del tuo progetto PDK in modo che le removal policy vengano applicate
PDK Application cd packages/infranpx projen deploy -
Dai un’occhiata alla console CloudFormation e registra i valori che ti sono stati richiesti nel passaggio
cdk importsopra- L’ID dello User Pool, ad es.
us-west-2_XXXXX - Il nome dell’SMS Role, ad es.
infra-sandbox-UserIdentityUserPoolsmsRoleXXXXXX
- L’ID dello User Pool, ad es.
-
Aggiorna il tuo progetto PDK per fare riferimento alle risorse esistenti invece di crearle
constructs/database.ts this.shoppingListTable = new Table(this, 'ShoppingList', {...this.shoppingListTable = Table.fromTableName(this,'ShoppingList','shopping_list',);E per il Cognito User Pool
application-stack.ts const userPool = UserPool.fromUserPoolId(this,'UserPool','<your-user-pool-id>',);const userIdentity = new UserIdentity(this, `${id}UserIdentity`, {// PDK construct accepts UserPool not IUserPool, but this still works!userPool: userPool as any,}); -
Esegui nuovamente il deploy del tuo progetto PDK, questo significherà che le risorse non sono più gestite dallo stack CloudFormation del nostro progetto PDK.
PDK Application cd packages/infranpx projen deploy -
Ora che le risorse non sono gestite, possiamo eseguire
cdk importnella nostra nuova applicazione per eseguire effettivamente l’importazione:New Application cd packages/infrapnpm exec cdk import shopping-list-infra-sandbox/Application --forceInserisci i valori quando richiesto, l’importazione dovrebbe completarsi con successo.
-
Esegui nuovamente il deploy della nuova applicazione per assicurarti che vengano apportate eventuali modifiche a queste risorse esistenti (ora gestite dal tuo nuovo stack):
Terminal window pnpm nx deploy infra shopping-list-infra-sandbox/*Terminal window yarn nx deploy infra shopping-list-infra-sandbox/*Terminal window npx nx deploy infra shopping-list-infra-sandbox/*Terminal window bunx nx deploy infra shopping-list-infra-sandbox/* -
Esegui un test completo della tua nuova applicazione ancora una volta
-
Aggiorna i record DNS per puntare al tuo nuovo sito web (e API se necessario).
Raccomandiamo un approccio graduale utilizzando il Weighted Routing di Route53, per cui una frazione delle richieste viene inizialmente indirizzata alla nuova applicazione. Man mano che monitori le tue metriche puoi aumentare il peso per la nuova applicazione fino a quando nessun traffico viene inviato alla tua vecchia applicazione PDK.
Se non hai alcun DNS e hai utilizzato i domini generati automaticamente per il sito web e l’API, puoi sempre considerare di fare il proxy delle richieste (ad es. tramite un CloudFront HTTP origin o API Gateway HTTP integration(s)).
-
Monitora le metriche dell’applicazione PDK per assicurarti che non ci sia traffico e infine distruggi il vecchio stack CloudFormation:
Terminal window cd packages/infranpx projen destroy
È stato un po’ più complesso, ma abbiamo migrato con successo i nostri utenti senza interruzioni alla nuova applicazione! 🎉🎉🎉
Ora abbiamo i nuovi vantaggi di Nx Plugin for AWS rispetto a PDK:
- Build più veloci
- Supporto per lo sviluppo locale dell’API
- Una codebase adatta al vibe-coding (prova il nostro server MCP!)
- Codice client/server type-safe più intuitivo
- E molto altro!
Domande Frequenti
Sezione intitolata “Domande Frequenti”Questa sezione fornisce indicazioni per le funzionalità di PDK che non sono coperte dall’esempio di migrazione sopra.
Come regola generale quando si passa da PDK, raccomandiamo di iniziare qualsiasi progetto con un Nx Workspace, date le sue somiglianze con il PDK Monorepo. Raccomandiamo anche di usare i nostri generatori come primitive su cui costruire qualsiasi nuovo tipo.
pnpm create @aws/nx-workspace my-projectyarn create @aws/nx-workspace my-projectnpm create @aws/nx-workspace -- my-projectbun create @aws/nx-workspace my-projectCDK Graph
Sezione intitolata “CDK Graph”CDK Graph costruisce grafici delle risorse CDK connesse e forniva due plugin:
Diagram Plugin
Sezione intitolata “Diagram Plugin”Il CDK Graph Diagram Plugin genera diagrammi dell’architettura AWS dalla tua infrastruttura CDK.
Per un approccio deterministico simile, un’alternativa valida è CDK-Dia.
Con i progressi nell’AI Generativa, molti modelli fondazionali sono in grado di creare diagrammi di alta qualità dalla tua infrastruttura CDK. Consigliamo di provare l’AWS Diagram MCP Server. Consulta questo post del blog per una guida dettagliata.
Threat Composer Plugin
Sezione intitolata “Threat Composer Plugin”Il CDK Graph Threat Composer Plugin genera un Threat Composer iniziale di modello di minaccia dal tuo codice CDK.
Questo plugin funzionava semplicemente filtrando un modello di minaccia di base contenente minacce di esempio e filtrandole in base alle risorse utilizzate dal tuo stack.
Se sei interessato a queste specifiche minacce di esempio, puoi copiare e filtrare il modello di minaccia di base, oppure usarlo come contesto per aiutare un modello fondazionale a generarne uno simile.
AWS Arch
Sezione intitolata “AWS Arch”AWS Arch forniva mappature tra le risorse CloudFormation e le loro icone di architettura associate per CDK Graph sopra.
Fare riferimento alla pagina AWS Architecture Icons per le risorse relative alle icone. Diagrams fornisce anche un modo per costruire diagrammi come codice.
Se stavi utilizzando questo direttamente, considera di fare un fork del progetto e prenderne la proprietà!
Pipeline
Sezione intitolata “Pipeline”PDK forniva un PDKPipelineProject che configurava un progetto di infrastruttura CDK e utilizzava un costrutto CDK che racchiudeva alcune risorse di CDK Pipelines.
Per migrare da questo, puoi utilizzare direttamente i costrutti CDK Pipelines. In pratica, tuttavia, è probabilmente più semplice utilizzare qualcosa come GitHub actions o GitLab CI/CD, dove definisci CDK Stages ed esegui il comando deploy per lo stage appropriato direttamente.
PDK Nag
Sezione intitolata “PDK Nag”PDK Nag racchiude CDK Nag e fornisce un insieme di regole specifiche per la creazione di prototipi.
Per migrare da PDK Nag, utilizza CDK Nag direttamente. Se hai bisogno dello stesso insieme di regole, puoi creare un “pack” personalizzato seguendo la documentazione qui.
Type Safe API
Sezione intitolata “Type Safe API”I componenti più comunemente utilizzati di Type Safe API sono trattati nell’esempio di migrazione precedente, tuttavia ci sono altre funzionalità, per le quali i dettagli di migrazione sono riportati di seguito.
API modellate con OpenAPI
Sezione intitolata “API modellate con OpenAPI”Nx Plugin for AWS supporta API modellate in Smithy, ma non quelle modellate direttamente in OpenAPI. Il generatore ts#smithy-api è un buon punto di partenza che puoi poi modificare. Puoi definire la tua specifica OpenAPI nella cartella src del progetto model invece di Smithy, e modificare il build.Dockerfile per utilizzare il tuo strumento di generazione del codice desiderato per client/server se non sono disponibili su NPM. Se i tuoi strumenti desiderati sono su NPM, puoi semplicemente installarli come dipendenze di sviluppo nel tuo workspace Nx e chiamarli direttamente come target di build Nx.
Backend
Sezione intitolata “Backend”Per backend type-safe modellati in OpenAPI, puoi considerare l’utilizzo di uno dei generatori di server di OpenAPI Generator. Questi non genereranno direttamente per AWS Lambda, ma puoi utilizzare AWS Lambda Web Adapter per colmare il divario per molti di essi.
Per i client TypeScript, puoi utilizzare il generatore ts#website e il generatore connection con un esempio di ts#api (con framework impostato su smithy) per vedere come vengono generati e integrati i client con un sito web. Questo configura target di build che generano client invocando i nostri generatori open-api#ts-client o open-api#ts-hooks. Puoi utilizzare questi generatori tu stesso puntandoli alla tua specifica OpenAPI.
Per altri linguaggi, puoi anche verificare se uno dei generatori di OpenAPI Generator soddisfa le tue esigenze.
Puoi anche costruire un generatore personalizzato utilizzando il generatore ts#nx-generator. Fai riferimento alla documentazione di quel generatore per i dettagli su come generare codice da OpenAPI. Puoi utilizzare i template di Nx Plugin for AWS come punto di partenza. Puoi anche fare riferimento ai template dalla codebase PDK per ulteriore ispirazione, notando che la struttura dati su cui operano i template è leggermente diversa da Nx Plugin for AWS.
API modellate con TypeSpec
Sezione intitolata “API modellate con TypeSpec”Per TypeSpec, si applica anche la sezione precedente per OpenAPI. Puoi iniziare generando un ts#smithy-api, installare il compilatore TypeSpec e i pacchetti OpenAPI nel tuo workspace Nx, e aggiornare il target compile del progetto model per eseguire tsp compile invece, assicurandoti che produca una specifica OpenAPI nella directory dist.
Backend
Sezione intitolata “Backend”L’approccio consigliato sarebbe utilizzare il generatore di server HTTP TypeSpec per JavaScript per generare il codice del tuo server, poiché funziona direttamente sul tuo modello TypeSpec.
Puoi utilizzare AWS Lambda Web Adapter per eseguire il server generato su AWS Lambda.
Puoi anche utilizzare una qualsiasi delle opzioni OpenAPI sopra indicate.
TypeSpec ha i propri generatori di codice per i client in tutti e tre i linguaggi supportati da Type Safe API:
Si applica anche la sezione OpenAPI precedente poiché TypeSpec può compilare in OpenAPI.
API modellate con Smithy
Sezione intitolata “API modellate con Smithy”L’esempio di migrazione precedente delinea la migrazione per utilizzare il generatore ts#smithy-api. Questa sezione copre le opzioni per backend e client Python e Java.
Backend
Sezione intitolata “Backend”Il generatore di codice Smithy per Java. Questo ha un generatore di server Java così come un adattatore per eseguire il server Java generato su AWS Lambda.
Smithy non ha un generatore di server per Python, quindi dovrai passare tramite OpenAPI. Fai riferimento alla sezione precedente riguardante API modellate con OpenAPI per le opzioni potenziali.
Il generatore di codice Smithy per Java. Questo ha un generatore di client Java.
Per i client Python, puoi dare un’occhiata a Smithy Python.
Per TypeScript, dai un’occhiata a Smithy TypeScript, oppure utilizza lo stesso approccio che abbiamo adottato in ts#smithy-api passando tramite OpenAPI (abbiamo optato per questo in quanto ci dà coerenza tra API tRPC, FastAPI e Smithy tramite hook TanStack Query).
Smithy Shape Library
Sezione intitolata “Smithy Shape Library”Type Safe API forniva un tipo di progetto Projen chiamato SmithyShapeLibraryProject che configurava un progetto contenente modelli Smithy che potevano essere riutilizzati da più API basate su Smithy.
L’equivalente è il generatore smithy#project con type impostato su shapes:
pnpm nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapesyarn nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapesnpx nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapesbunx nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapesPuoi anche eseguire una prova per vedere quali file verrebbero modificati
pnpm nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapes --dry-runyarn nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapes --dry-runnpx nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapes --dry-runbunx nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapes --dry-run- Installa il Nx Console VSCode Plugin se non l'hai già fatto
- Apri la console Nx in VSCode
- Clicca su
Generate (UI)nella sezione "Common Nx Commands" - Cerca
@aws/nx-plugin - smithy#project - Compila i parametri richiesti
- name: my-shapes
- type: shapes
- Clicca su
Generate
Sposta le forme dal tuo SmithyShapeLibraryProject nella cartella src del progetto generato, quindi fai riferimento alla guida del progetto Smithy per come collegare la libreria come dipendenza del modello della tua API.
Interceptors
Sezione intitolata “Interceptors”Type Safe API forniva i seguenti interceptor predefiniti:
- Interceptor di logging, tracing e metriche utilizzando Powertools for AWS Lambda
- Interceptor try-catch per gestire le eccezioni non catturate
- Interceptor CORS per restituire header CORS
Il generatore ts#smithy-api strumenta logging, tracing e metriche con Powertools for AWS Lambda utilizzando Middy. Il comportamento dell’interceptor try-catch è integrato nel Smithy TypeScript SSDK, e gli header CORS vengono aggiunti in handler.ts.
Per interceptor di logging, tracing e metriche in qualsiasi linguaggio, utilizza direttamente Powertools for AWS Lambda.
Per migrare interceptor personalizzati, consigliamo di utilizzare le seguenti librerie:
- TypeScript - Middy
- Python - Powertools for AWS Lambda Middleware Factory
- Java - Strumenta i metodi prima/dopo la tua logica di business utilizzando aws-lambda-java-libs per un approccio semplice, oppure considera AspectJ per costruire il tuo middleware come annotazioni.
Generazione della documentazione
Sezione intitolata “Generazione della documentazione”Type Safe API forniva la generazione della documentazione utilizzando Redocly CLI. Questo è molto facile da aggiungere a un progetto esistente una volta che l’hai migrato come sopra.
-
Installa Redocly CLI
Terminal window pnpm add -Dw @redocly/cliTerminal window yarn add -D @redocly/cliTerminal window npm install --legacy-peer-deps -D @redocly/cliTerminal window bun add -D @redocly/cli -
Aggiungi un target di generazione della documentazione al tuo progetto
modelutilizzandoredocly build-docs, per esempio:model/project.json {..."documentation": {"cache": true,"outputs": ["{workspaceRoot}/dist/{projectRoot}/documentation"],"executor": "nx:run-commands","options": {"command": "redocly build-docs dist/packages/api/model/build/openapi/openapi.json --output=dist/packages/api/model/documentation/index.html","cwd": "{workspaceRoot}"},"dependsOn": ["compile"]}}
Puoi anche considerare i generatori di documentazione di OpenAPI Generator.
Mock Integrations
Sezione intitolata “Mock Integrations”Type Safe API generava mock per te all’interno del suo pacchetto di infrastruttura generato.
Puoi passare a JSON Schema Faker che può creare i dati mock basati su JSON Schema. Questo può funzionare direttamente su una specifica OpenAPI, e ha una CLI che potresti eseguire come parte della build del tuo progetto model.
Puoi aggiornare la tua infrastruttura CDK per leggere il file JSON prodotto da JSON Schema Faker, e restituire l’appropriata MockIntegration di API Gateway per un’integrazione, basata sul metadata.gen.ts generato (supponendo che tu abbia utilizzato il generatore ts#smithy-api).
Backend in linguaggi misti
Sezione intitolata “Backend in linguaggi misti”Type Safe API supportava l’implementazione di API con una miscela di diversi linguaggi nel backend. Questo può essere ottenuto anche fornendo “override” alle integrazioni quando si istanzia il costrutto API in CDK:
const pythonLambdaHandler = new Function(this, 'PythonImplementation', { runtime: Runtime.PYTHON_3_12, ...});
new MyApi(this, 'MyApi', { integrations: Api.defaultIntegrations(this) .withOverrides({ echo: { integration: new LambdaIntegration(pythonLambdaHandler), handler: pythonLambdaHandler, }, }) .build(),});Dovrai creare uno “stub” del tuo servizio/router affinché il tuo servizio possa compilare se utilizzi ts#smithy-api e il TypeScript Server SDK, ad esempio:
export const Service: ApiService<ServiceContext> = { ... Echo: () => { throw new Error(`Not Implemented`); },};Validazione dell’input
Sezione intitolata “Validazione dell’input”Type Safe API aggiungeva la validazione nativa di API Gateway per i corpi delle richieste basata sulla tua specifica OpenAPI poiché utilizzava il costrutto SpecRestApi sotto il cofano.
Con il generatore ts#smithy-api, la validazione viene eseguita dal Server SDK stesso. Questo è lo stesso per la maggior parte dei generatori di server.
Se desideri implementare la validazione nativa di API Gateway, potresti farlo modificando packages/common/constructs/src/core/api/rest-api.ts per leggere il JSON schema rilevante per il corpo della richiesta di ciascuna operazione dalla tua specifica OpenAPI.
API WebSocket
Sezione intitolata “API WebSocket”Sfortunatamente non esiste un percorso di migrazione diretto per l’API websocket di Type Safe API utilizzando API Gateway e Lambda con lo sviluppo di API model-driven. Tuttavia, questa sezione della guida mira almeno a offrire alcune idee.
Considera l’utilizzo di AsyncAPI per modellare la tua API invece di OpenAPI o TypeSpec poiché questo è progettato per gestire API asincrone. Il template NodeJS di AsyncAPI può generare un backend websocket Node che potresti ospitare su ECS per esempio.
Puoi anche considerare AppSync Events per l’infrastruttura, e utilizzare Powertools. Questo post del blog vale la pena leggerlo!
Un’altra opzione è utilizzare API GraphQL con websocket su AppSync, per cui abbiamo una issue GitHub a cui puoi dare un +1! Fai riferimento alla guida per sviluppatori AppSync per dettagli e link a progetti di esempio.
Puoi anche considerare di creare i tuoi generatori di codice che interpretano le stesse estensioni vendor di Type Safe API. Fai riferimento alla sezione API modellate con OpenAPI per i dettagli sulla costruzione di generatori di codice personalizzati basati su OpenAPI. Puoi trovare i template che Type Safe API utilizza per i gestori Lambda di API Gateway Websocket API qui, e il client qui.
Puoi anche considerare di migrare per utilizzare il generatore ts#trpc-api per utilizzare tRPC. Al momento della scrittura non abbiamo ancora supporto per sottoscrizioni/streaming ma se questo è qualcosa di cui hai bisogno aggiungi un +1 alla nostra issue GitHub che traccia questo.
Smithy è agnostico al protocollo, ma non ha ancora supporto per il protocollo Websocket, fai riferimento a questa issue GitHub che traccia il supporto.
Infrastructure in Python or Java
Sezione intitolata “Infrastructure in Python or Java”PDK supportava l’infrastruttura CDK scritta in Python e Java. Al momento della scrittura, non supportiamo questo nel Nx Plugin for AWS.
Il percorso consigliato sarebbe quello di migrare la tua infrastruttura CDK a TypeScript, oppure di utilizzare i nostri generatori e migrare il pacchetto di costrutti comuni al linguaggio desiderato. Puoi utilizzare l’IA Generativa per accelerare questo tipo di migrazioni, ad esempio Kiro CLI. Puoi far iterare un agente AI sulla migrazione fino a quando i template CloudFormation sintetizzati sono identici.
Lo stesso vale per l’infrastruttura generata da Type Safe API in Python o Java - puoi tradurre il costrutto generico rest-api.ts dal pacchetto di costrutti comuni e implementare il tuo semplice generatore di metadati per il linguaggio di destinazione (fai riferimento alla sezione API Modellate con OpenAPI).
Puoi utilizzare il generatore py#project per un progetto Python di base a cui aggiungere il tuo codice CDK (e spostare il tuo file cdk.json, aggiungendo i target rilevanti). Puoi utilizzare il plugin @nx/gradle di Nx per progetti Java, oppure @jnxplus/nx-maven per Maven.
Use of Projen
Sezione intitolata “Use of Projen”PDK è stato costruito su Projen. Projen e Nx Generators hanno differenze abbastanza fondamentali, il che significa che sebbene sia tecnicamente possibile combinarli, è probabilmente un anti-pattern. Projen gestisce i file di progetto come codice in modo tale che non possano essere modificati direttamente, mentre i generatori Nx forniscono i file di progetto una volta sola e poi il codice può essere liberamente modificato.
Se desideri continuare a utilizzare Projen, puoi implementare tu stesso i tipi di progetto Projen desiderati. Per seguire i pattern di Nx Plugin for AWS, puoi eseguire i nostri generatori o esaminare il loro codice sorgente su GitHub per vedere come sono costruiti i tipi di progetto desiderati e implementare le parti rilevanti utilizzando le primitive di Projen.