@energica-city/shared-amplify-utils
TypeScript utilities every backend handler in this platform is built from: error handling, structured logging, type-safe database operations with in-memory caching, and Lambda middleware chains.
Three repositories depend on this one — Eve-Backend-v2, cdk-test-proyect and
eve_adminwebv3. energica_payments does not. A change here reaches production in three places, so
it is the highest-leverage repository in the workspace and the easiest to break something distant
from.
Installation
npm install @energica-city/shared-amplify-utils
⚠ Everything is imported from the package root. package.json declares a single "." export, so
subpath imports do not resolve:
import { logger, throwError, QueryFactory } from "@energica-city/shared-amplify-utils"; // ✅
import { logger } from "@energica-city/shared-amplify-utils/log"; // ❌ no such export
The section headings below name the source directory a utility lives in, not an import path.
Developing
Source lives at the repository root, not under src/.
npm install
npm run build # tsc → dist/
npm test # vitest, once
npm run test:watch # watching
npm run lint && npm run format:check
npm run gate # the repo's whole gate — what the pre-commit hook runs
npm run fix # prettier --write, then eslint --fix
npm run build:verify # = npm run build; what the pre-push hook runs
The workspace gate (../tools/run-all-tests.sh shared) runs the repo's gate script: the typecheck,
lint, format and test passes together.
Publishing
npm run release:patch # a fix that changes no behaviour
npm run release:minor # anything a consumer must act on — see CHANGELOG.md
npm run release:major
Every release states in CHANGELOG.md what a consumer must change to adopt it.
prepublishOnly builds, tests, lints and format-checks first, so a broken publish fails before it
leaves the machine. Publishing is a separate step from pushing — unlike three of the five
repositories here, git push ships nothing.
⚠ Publishing is only half the job. The three consumers pin a ^ range, so a new version reaches
them only when their package.json is updated and they are redeployed. Bumping this library means
updating all three and saying so.
Modules
Error — error/
Standardized error handling with structured logging and context preservation.
Core Functions:
throwError(message, context)- Throw with structured context and automatic loggingextractErrorMessage(error)- Extract consistent messages from any error typecreateErrorContext(data)- Create filtered context objects
WebSocket Errors:
throwWebSocketError(statusCode, code, message, context)- WebSocket-specific errorsisWebSocketError(error)- Type guard for WebSocket errorsWebSocketErrorCodes- Predefined error codes (VALIDATION_ERROR, AUTHENTICATION_ERROR, etc.)
import { throwError, createErrorContext } from "@energica-city/shared-amplify-utils";
throwError("Operation failed", createErrorContext({ userId: "123", operation: "create" }));
Logging — log/
One logger API with two engines, chosen by the package's exports conditions:
- Node (Lambda) — AWS Lambda Powertools. Lean JSON lines:
timestamp,level,message,requestId, the trace keys (buyOrder,stationId,eveId,connectorId,residenceId,stage), then data; at most 16 KB per line. - Browser (
browsercondition) — the console engine: same API, same redaction, no trail.
Every line is redacted: emails, phones, RUTs and names masked; card, bank, token, address and free-text fields redacted; free strings scrubbed. The trace keys stay readable — they are how a line is found.
The trail holds detail — database reads, raw requests, GraphQL arguments — and writes it only
when the invocation fails (an ERROR line, an uncaught throw, a pending timeout, trail.flush()), or
directly when the function's level is DEBUG.
import { logger, trail, withInvocation, configureTrail } from "@energica-city/shared-amplify-utils";
configureTrail({ maxBytes: 256 * 1024 }); // once, at module load
export const handler = (event, context) =>
withInvocation(context, async () => {
logger.setContext({ buyOrder: event.buyOrder });
trail.info("charge read", { charge }); // only if this invocation fails
logger.info("charge settled", { amountUsedAccounting });
});
A Lambda killed at its timeout runs no catch. configureInvocation makes withInvocation call a
hook shortly before the kill, after flushing the trail, so the repo can raise an incident:
import { configureInvocation } from "@energica-city/shared-amplify-utils";
configureInvocation({
timeoutMarginMs: 3000, // default
onTimeout: async ({ requestId, functionName, logGroup, remainingMs }) => {
await reportIncident({ requestId, functionName, logGroup, remainingMs });
},
}); // once, at module load; a later call replaces the settings
A hook that throws or rejects is logged and swallowed; a no-op in the browser.
Levels by name or number, from AWS_LAMBDA_LOG_LEVEL (the function's logging config), then
POWERTOOLS_LOG_LEVEL, then LOG_LEVEL; INFO otherwise.
Query Factory — queries/
Type-safe CRUD operations for Amplify Data models with automatic client management, connection pooling, and optional in-memory caching with LRU eviction.
Core Features:
- Type-safe database operations
- Automatic client management and connection pooling
- Schema-driven identifier extraction
- Optional per-factory read cache, off unless
enabled: true - Every write clears what it outdates;
invalidate(id)for other writers - Conditional writes (
condition),authModeon every operation - Truncated pagination throws unless
allowTruncation: true - Composite key support for complex identifiers
Basic Usage:
import { QueryFactory, initializeQueries } from "@energica-city/shared-amplify-utils";
await initializeQueries<MainTypes>(outputs);
const UserQueries = await QueryFactory<"User", MainTypes>({ name: "User" });
const user = await UserQueries.create({
input: { username: "john", email: "[email protected]" },
});
const foundUser = await UserQueries.get({ input: { userId: user.id } });
await UserQueries.update({ input: { userId: user.id, validated: true } });
await UserQueries.delete({ input: { userId: user.id } });
With Caching:
const UserQueries = await QueryFactory<"User", MainTypes>({
name: "User",
cache: { enabled: true, ttl: 60_000 }, // off unless enabled: true
});
await UserQueries.get({ input: { userId: "123" } }); // database
await UserQueries.get({ input: { userId: "123" } }); // cache
await UserQueries.update({ input: { userId: "123", validated: true } }); // clears it
UserQueries.invalidate({ userId: "123" }); // after a write by anyone else
⚠ A cache lives in a warm Lambda container across invocations. A handler that decides money from a read — settlement, refunds, a duplicate check — should not cache that model at all.
As the Lambda's own role:
await initializeQueries({ amplifyOutputs, schema, credentials: "lambdaRole" }); // iam, signed as the role
Middleware — middleware/
Universal composable middleware chains for AWS Lambda with REST, WebSocket, and GraphQL support.
Core Middleware Chain
import { MiddlewareChain } from "@energica-city/shared-amplify-utils";
const chain = new MiddlewareChain<MyInput, MyOutput>();
chain.use("auth", authMiddleware).use("validation", validationMiddleware);
const result = await chain.execute(input, finalHandler);
REST Middleware
Complete REST API middleware system for AWS API Gateway with HTTP-specific error handling.
import {
createRestChain,
wrapRestHandler,
createRestErrorHandler,
createRestRequestLogger,
createRestRequestValidator,
createRestModelInitializer,
getValidatedBody,
getModelsFromInput,
} from "@energica-city/shared-amplify-utils";
const chain = createRestChain<MainTypes>()
.use("errorHandler", createRestErrorHandler())
.use("logger", createRestRequestLogger())
.use("modelInitializer", createRestModelInitializer({ entities: ["User"] }))
.use("validator", createRestRequestValidator({ bodySchema: userSchema }));
export const handler = wrapRestHandler(chain, async input => {
const { User } = getModelsFromInput(input);
const body = getValidatedBody(input);
return createSuccessResponse(await User.create({ input: body }));
});
WebSocket Middleware
WebSocket-specific middleware for AWS API Gateway WebSocket APIs with real-time connection management.
import {
createWebSocketChain,
wrapWebSocketHandler,
createWebSocketErrorHandler,
createWebSocketModelInitializer,
getModelFromInput,
} from "@energica-city/shared-amplify-utils";
const chain = createWebSocketChain<MainTypes>()
.use("errorHandler", createWebSocketErrorHandler())
.use("modelInitializer", createWebSocketModelInitializer({ entities: ["Message"] }));
export const handler = wrapWebSocketHandler(chain, async input => {
const MessageModel = getModelFromInput(input, "Message");
// WebSocket handler logic
});
GraphQL Middleware
GraphQL resolver middleware for AWS AppSync with Amplify Data integration and structured error handling.
import {
createGraphQLChain,
wrapGraphQLResolver,
createGraphQLErrorHandler,
createGraphQLRequestLogger,
createGraphQLModelInitializer,
getModelsFromInput,
extractArguments,
} from "@energica-city/shared-amplify-utils";
const chain = createGraphQLChain<MainTypes>()
.use("modelInitializer", createGraphQLModelInitializer({ entities: ["User", "Post"] }))
.use("logger", createGraphQLRequestLogger())
.use("errorHandler", createGraphQLErrorHandler());
export const handler = wrapGraphQLResolver(chain, async input => {
const { User } = getModelsFromInput(input);
const { userId } = extractArguments(input);
return await User.get({ input: { userId } });
});
Validation Utils
Universal validation patterns and data sanitization utilities.
import { ValidationPatterns, sanitizeObject } from "@energica-city/shared-amplify-utils";
// Pre-built patterns
ValidationPatterns.uuid(); // UUID validation
ValidationPatterns.email(); // Email format validation
ValidationPatterns.pagination(); // Page/limit parameters
// Data sanitization
const clean = sanitizeObject(data, { excludeFields: ["secret"], maxDepth: 3 });
Configuration
Environment Variables:
AWS_LAMBDA_LOG_LEVEL/POWERTOOLS_LOG_LEVEL/LOG_LEVEL: log level, by name or 0–4EVE_TRAIL_MAX_BYTES: the trail buffer size, ifconfigureTrailis not calledSTRUCTURED_LOGGING: force the console engine's JSON output ('true'/'false')NODE_ENV:developmentis the only value that shows stacks and internal messages
AWS Lambda Auto-Detection:
- The Node entry writes through Powertools;
withInvocationadds the request id to every line - A read cache, when enabled, persists across warm invocations
Requirements
- Node.js 20+
- TypeScript 5.6+
- AWS Amplify 6.0+
- Yup 1.6+ (for validation)
- lru-cache 11.0+ (for in-memory caching)
Exports
// Root exports only
export * from "./error";
export * from "./log";
export * from "./queries";
export * from "./middleware";
// All imports from root package
import { throwError } from "@energica-city/shared-amplify-utils";
import { logger } from "@energica-city/shared-amplify-utils";
import { QueryFactory, AppError } from "@energica-city/shared-amplify-utils";
One "." export, with a browser condition that swaps the logging engine; there are no subpath
exports.