create-stb 1.0.6 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,39 +0,0 @@
1
- {
2
- "name": "create-stb",
3
- "version": "1.0.0",
4
- "description": "create-stb boilerplate template",
5
- "license": "MIT",
6
- "scripts": {
7
- "build": "tsc",
8
- "clean": "rm -rf .build",
9
- "deploy": "serverless deploy",
10
- "dynamo:destroy": "docker compose down -v",
11
- "dynamo:down": "docker compose down",
12
- "dynamo:up": "docker compose up -d",
13
- "format": "prettier --write .",
14
- "offline": "serverless offline --stage dev",
15
- "tables:create": "ts-node src/scripts/create-tables.ts",
16
- "tables:seed": "ts-node src/scripts/seed-tables.ts"
17
- },
18
- "dependencies": {
19
- "@aws-sdk/client-dynamodb": "3.910.0",
20
- "@aws-sdk/util-dynamodb": "3.910.0",
21
- "@faker-js/faker": "10.1.0",
22
- "axios": "1.13.2",
23
- "dotenv": "17.2.3"
24
- },
25
- "devDependencies": {
26
- "@types/aws-lambda": "8.10.158",
27
- "@types/dotenv": "6.1.1",
28
- "@types/node": "24.7.2",
29
- "prettier": "3.6.2",
30
- "serverless": "4.20.2",
31
- "serverless-offline": "14.4.0",
32
- "typescript": "5.9.3"
33
- },
34
- "author": "Sam Newhouse",
35
- "repository": {
36
- "type": "git",
37
- "url": "git://github.com/SamNewhouse/create-stb.git"
38
- }
39
- }
@@ -1,52 +0,0 @@
1
- service: create-stb
2
-
3
- provider:
4
- name: aws
5
- runtime: nodejs20.x
6
- region: eu-west-2
7
- stage: ${opt:stage, 'dev'}
8
- environment:
9
- DYNAMODB_EXAMPLE: Example
10
- JWT_SECRET: ${env:JWT_SECRET, 'dev-secret-change-in-production'}
11
- DYNAMODB_ENDPOINT: ${env:DYNAMODB_ENDPOINT, ''}
12
- STAGE: ${self:provider.stage}
13
-
14
- package:
15
- individually: true
16
- patterns:
17
- - "!src/scripts/**"
18
-
19
- plugins:
20
- - serverless-offline
21
-
22
- custom:
23
- dynamodb:
24
- stages:
25
- - dev
26
- start:
27
- port: 8000
28
- inMemory: true
29
- migrate: true
30
-
31
- functions:
32
- example-get:
33
- handler: src/handlers/example/get.handler
34
- events:
35
- - http:
36
- path: example/{id}
37
- method: get
38
- cors: true
39
-
40
- resources:
41
- Resources:
42
- ExampleTable:
43
- Type: AWS::DynamoDB::Table
44
- Properties:
45
- TableName: Example
46
- AttributeDefinitions:
47
- - AttributeName: id
48
- AttributeType: S
49
- KeySchema:
50
- - AttributeName: id
51
- KeyType: HASH
52
- BillingMode: PAY_PER_REQUEST
@@ -1,11 +0,0 @@
1
- import * as dotenv from "dotenv";
2
-
3
- dotenv.config();
4
-
5
- export const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID;
6
- export const AWS_REGION = process.env.AWS_REGION;
7
- export const AWS_REGION_FALLBACK = process.env.AWS_REGION_FALLBACK;
8
- export const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY;
9
- export const DYNAMODB_ENDPOINT = process.env.DYNAMODB_ENDPOINT;
10
- export const JWT_SECRET = process.env.JWT_SECRET;
11
- export const STAGE = process.env.STAGE;
@@ -1,6 +0,0 @@
1
- export const ENV: string = process.env.ENV!;
2
-
3
- export const AWS_REGION: string = process.env.AWS_REGION!;
4
-
5
- export const AWS_ACCESS_KEY_ID: string = process.env.AWS_ACCESS_KEY_ID!;
6
- export const AWS_ACCESS_SECRET_KEY: string = process.env.AWS_ACCESS_SECRET_KEY!;
@@ -1,24 +0,0 @@
1
- import * as Dynamodb from "../lib/dynamodb";
2
- import { Example, Tables } from "../types";
3
- import { shortUUID } from "../utils/helpers";
4
-
5
- /**
6
- * Retrieves a single Example item by its unique identifier (id).
7
- * @param id - The primary key value to look up.
8
- * @returns The Example object if found, or null if not found.
9
- */
10
- export async function get(id: string): Promise<Example> {
11
- const client = Dynamodb.getClient();
12
- return Dynamodb.getItem(client, Tables.Example, { id });
13
- }
14
-
15
- /**
16
- * Generates a generic set of Example records with random short ids.
17
- * Uses shortUUID() for unique 8-character values.
18
- * @returns An array of 10 Example objects, each with a random id.
19
- */
20
- export function generatePlainExamples(): Example[] {
21
- return Array.from({ length: 10 }, () => ({
22
- id: shortUUID(),
23
- }));
24
- }
@@ -1,17 +0,0 @@
1
- import { APIGatewayProxyHandler } from "aws-lambda";
2
- import { get } from "../../functions/example";
3
- import { success, handleError, badRequest, notFound } from "../../lib/http";
4
-
5
- export const handler: APIGatewayProxyHandler = async (event) => {
6
- try {
7
- const id = event.pathParameters?.id;
8
- if (!id) return badRequest("id is required");
9
-
10
- const example = await get(id);
11
- if (!example) return notFound("Example not found");
12
-
13
- return success(example);
14
- } catch (err) {
15
- return handleError(err);
16
- }
17
- };
@@ -1,203 +0,0 @@
1
- import {
2
- DynamoDBClient,
3
- ListTablesCommand,
4
- CreateTableCommand,
5
- PutItemCommand,
6
- GetItemCommand,
7
- BatchGetItemCommand,
8
- ScanCommand,
9
- QueryCommand,
10
- UpdateItemCommand,
11
- DeleteItemCommand,
12
- } from "@aws-sdk/client-dynamodb";
13
- import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
14
- import { DYNAMODB_ENDPOINT, AWS_SECRET_ACCESS_KEY, AWS_REGION_FALLBACK } from "../config/variables";
15
- import { AWS_ACCESS_KEY_ID, AWS_REGION } from "../env";
16
-
17
- /**
18
- * Returns a singleton DynamoDBClient configured for this environment.
19
- */
20
- let dynamoClient: DynamoDBClient | null = null;
21
-
22
- export function getClient(): DynamoDBClient {
23
- if (!dynamoClient) {
24
- const config: any = {};
25
- if (DYNAMODB_ENDPOINT) {
26
- config.endpoint = DYNAMODB_ENDPOINT;
27
- config.credentials = {
28
- accessKeyId: AWS_ACCESS_KEY_ID || "dummy",
29
- secretAccessKey: AWS_SECRET_ACCESS_KEY || "dummy",
30
- };
31
- }
32
- config.region = AWS_REGION || AWS_REGION_FALLBACK || "eu-west-2";
33
- dynamoClient = new DynamoDBClient(config);
34
- }
35
- return dynamoClient;
36
- }
37
-
38
- /**
39
- * Lists all DynamoDB tables for the provided client.
40
- */
41
- export async function listTables(client: DynamoDBClient = getClient()): Promise<string[]> {
42
- const result = await client.send(new ListTablesCommand({}));
43
- return result.TableNames ?? [];
44
- }
45
-
46
- /**
47
- * Creates a new DynamoDB table using the given configuration.
48
- */
49
- export async function createTable(
50
- client: DynamoDBClient = getClient(),
51
- tableConfig: any,
52
- ): Promise<void> {
53
- await client.send(new CreateTableCommand(tableConfig));
54
- }
55
-
56
- /**
57
- * Checks if a table exists.
58
- */
59
- export async function tableExists(
60
- client: DynamoDBClient = getClient(),
61
- tableName: string,
62
- ): Promise<boolean> {
63
- try {
64
- const tables = await listTables(client);
65
- return tables.includes(tableName);
66
- } catch {
67
- return false;
68
- }
69
- }
70
-
71
- /**
72
- * Inserts (puts) a single item into a DynamoDB table.
73
- */
74
- export async function putItem(
75
- client: DynamoDBClient = getClient(),
76
- tableName: string,
77
- item: any,
78
- ): Promise<void> {
79
- await client.send(
80
- new PutItemCommand({
81
- TableName: tableName,
82
- Item: marshall(item),
83
- }),
84
- );
85
- }
86
-
87
- /**
88
- * Retrieves a single item by its primary key.
89
- */
90
- export async function getItem(
91
- client: DynamoDBClient = getClient(),
92
- tableName: string,
93
- key: Record<string, any>,
94
- ): Promise<any | null> {
95
- const result = await client.send(
96
- new GetItemCommand({
97
- TableName: tableName,
98
- Key: marshall(key),
99
- ConsistentRead: true,
100
- }),
101
- );
102
- return result.Item ? unmarshall(result.Item) : null;
103
- }
104
-
105
- /**
106
- * Batch gets multiple items by keys.
107
- */
108
- export async function getBatch(
109
- client: DynamoDBClient = getClient(),
110
- tableName: string,
111
- ids: string[],
112
- ): Promise<any[]> {
113
- if (!ids.length) return [];
114
- const keys = ids.map((id) => marshall({ id }));
115
- const params = {
116
- RequestItems: {
117
- [tableName]: {
118
- Keys: keys,
119
- },
120
- },
121
- };
122
- const result = await client.send(new BatchGetItemCommand(params));
123
- const items = result.Responses?.[tableName] || [];
124
- return items.map((item) => unmarshall(item));
125
- }
126
-
127
- /**
128
- * Scans a table, with optional filter expression and values.
129
- */
130
- export async function scan(
131
- client: DynamoDBClient = getClient(),
132
- tableName: string,
133
- filterExpression?: string,
134
- expressionAttributeValues?: Record<string, any>,
135
- ): Promise<any[]> {
136
- const params: any = { TableName: tableName };
137
- if (filterExpression) params.FilterExpression = filterExpression;
138
- if (expressionAttributeValues)
139
- params.ExpressionAttributeValues = marshall(expressionAttributeValues);
140
-
141
- const result = await client.send(new ScanCommand(params));
142
- return result.Items ? result.Items.map((item) => unmarshall(item)) : [];
143
- }
144
-
145
- /**
146
- * Queries a table with the given key condition and optional index.
147
- */
148
- export async function query(
149
- client: DynamoDBClient = getClient(),
150
- tableName: string,
151
- keyConditionExpression: string,
152
- expressionAttributeValues: Record<string, any>,
153
- indexName?: string,
154
- ): Promise<any[]> {
155
- const params: any = {
156
- TableName: tableName,
157
- KeyConditionExpression: keyConditionExpression,
158
- ExpressionAttributeValues: marshall(expressionAttributeValues),
159
- };
160
- if (indexName) params.IndexName = indexName;
161
-
162
- const result = await client.send(new QueryCommand(params));
163
- return result.Items ? result.Items.map((item) => unmarshall(item)) : [];
164
- }
165
-
166
- /**
167
- * Updates an item by key with a given update expression.
168
- */
169
- export async function update(
170
- client: DynamoDBClient = getClient(),
171
- tableName: string,
172
- key: Record<string, any>,
173
- updateExpression: string,
174
- expressionAttributeValues: Record<string, any>,
175
- expressionAttributeNames?: Record<string, string>,
176
- ): Promise<any> {
177
- const params: any = {
178
- TableName: tableName,
179
- Key: marshall(key),
180
- UpdateExpression: updateExpression,
181
- ExpressionAttributeValues: marshall(expressionAttributeValues),
182
- ReturnValues: "ALL_NEW",
183
- };
184
- if (expressionAttributeNames) params.ExpressionAttributeNames = expressionAttributeNames;
185
- const result = await client.send(new UpdateItemCommand(params));
186
- return result.Attributes ? unmarshall(result.Attributes) : null;
187
- }
188
-
189
- /**
190
- * Deletes an item by key.
191
- */
192
- export async function deleteItem(
193
- client: DynamoDBClient = getClient(),
194
- tableName: string,
195
- key: Record<string, any>,
196
- ): Promise<void> {
197
- await client.send(
198
- new DeleteItemCommand({
199
- TableName: tableName,
200
- Key: marshall(key),
201
- }),
202
- );
203
- }
@@ -1,261 +0,0 @@
1
- import { APIGatewayProxyResult } from "aws-lambda";
2
- import axios, { AxiosResponse, AxiosError } from "axios";
3
-
4
- /**
5
- * Standardized API response body structure
6
- */
7
- export interface ApiResponse<T = any> {
8
- success: boolean;
9
- data?: T;
10
- error?: string;
11
- message?: string;
12
- }
13
-
14
- /**
15
- * Default CORS headers applied to all HTTP responses
16
- */
17
- const defaultHeaders = {
18
- "Content-Type": "application/json",
19
- "Access-Control-Allow-Origin": "*",
20
- "Access-Control-Allow-Headers": "Content-Type,Authorization",
21
- "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
22
- };
23
-
24
- /**
25
- * Default Axios instance with common configuration
26
- */
27
- const httpClient = axios.create({
28
- timeout: 30000, // 30 second timeout
29
- headers: {
30
- "Content-Type": "application/json",
31
- },
32
- });
33
-
34
- /**
35
- * Creates a successful HTTP response with data
36
- * @param data - The response data payload
37
- * @param statusCode - HTTP status code (defaults to 200)
38
- * @param message - Optional success message
39
- * @returns Formatted HTTP response as APIGatewayProxyResult
40
- */
41
- export function success<T>(data: T, statusCode = 200, message?: string): APIGatewayProxyResult {
42
- const response: ApiResponse<T> = {
43
- success: true,
44
- data,
45
- ...(message && { message }),
46
- };
47
-
48
- return {
49
- statusCode,
50
- headers: defaultHeaders,
51
- body: JSON.stringify(response),
52
- };
53
- }
54
-
55
- /**
56
- * Creates an error HTTP response
57
- * @param errorMessage - Error message to return
58
- * @param statusCode - HTTP status code (defaults to 500)
59
- * @returns Formatted HTTP error response as APIGatewayProxyResult
60
- */
61
- export function error(errorMessage: string, statusCode = 500): APIGatewayProxyResult {
62
- const response: ApiResponse = {
63
- success: false,
64
- error: errorMessage,
65
- };
66
-
67
- return {
68
- statusCode,
69
- headers: defaultHeaders,
70
- body: JSON.stringify(response),
71
- };
72
- }
73
-
74
- /**
75
- * Creates a 400 Bad Request response
76
- * @param errorMessage - Error message describing what was invalid
77
- * @returns HTTP 400 response as APIGatewayProxyResult
78
- */
79
- export function badRequest(errorMessage: string): APIGatewayProxyResult {
80
- return error(errorMessage, 400);
81
- }
82
-
83
- /**
84
- * Creates a 401 Unauthorized response
85
- * @param errorMessage - Error message (defaults to "Unauthorized")
86
- * @returns HTTP 401 response as APIGatewayProxyResult
87
- */
88
- export function unauthorized(errorMessage: string = "Unauthorized"): APIGatewayProxyResult {
89
- return error(errorMessage, 401);
90
- }
91
-
92
- /**
93
- * Creates a 403 Forbidden response
94
- * @param errorMessage - Error message (defaults to "Forbidden")
95
- * @returns HTTP 403 response as APIGatewayProxyResult
96
- */
97
- export function forbidden(errorMessage: string = "Forbidden"): APIGatewayProxyResult {
98
- return error(errorMessage, 403);
99
- }
100
-
101
- /**
102
- * Creates a 404 Not Found response
103
- * @param errorMessage - Error message (defaults to "Not Found")
104
- * @returns HTTP 404 response as APIGatewayProxyResult
105
- */
106
- export function notFound(errorMessage: string = "Not Found"): APIGatewayProxyResult {
107
- return error(errorMessage, 404);
108
- }
109
-
110
- /**
111
- * Creates a 409 Conflict response
112
- * @param errorMessage - Error message describing the conflict
113
- * @returns HTTP 409 response as APIGatewayProxyResult
114
- */
115
- export function conflict(errorMessage: string): APIGatewayProxyResult {
116
- return error(errorMessage, 409);
117
- }
118
-
119
- /**
120
- * Safely parses JSON request body with error handling
121
- * @param body - Raw request body string from API Gateway event
122
- * @returns Parsed JSON object
123
- * @throws Error if body is null/empty or contains invalid JSON
124
- */
125
- export function parseBody<T = any>(body: string | null): T {
126
- if (!body) {
127
- throw new Error("Request body is required");
128
- }
129
-
130
- try {
131
- return JSON.parse(body);
132
- } catch {
133
- throw new Error("Invalid JSON in request body");
134
- }
135
- }
136
-
137
- /**
138
- * Makes an HTTP GET request using Axios
139
- * @param url - The URL to make the GET request to
140
- * @param headers - Optional additional headers
141
- * @returns Promise resolving to the response data
142
- */
143
- export async function get<T = any>(url: string, headers?: Record<string, string>): Promise<T> {
144
- try {
145
- const response: AxiosResponse<T> = await httpClient.get(url, { headers });
146
- return response.data;
147
- } catch (err) {
148
- throw handleAxiosError(err);
149
- }
150
- }
151
-
152
- /**
153
- * Makes an HTTP POST request using Axios
154
- * @param url - The URL to make the POST request to
155
- * @param data - The data to send in the request body
156
- * @param headers - Optional additional headers
157
- * @returns Promise resolving to the response data
158
- */
159
- export async function post<T = any>(
160
- url: string,
161
- data?: any,
162
- headers?: Record<string, string>,
163
- ): Promise<T> {
164
- try {
165
- const response: AxiosResponse<T> = await httpClient.post(url, data, { headers });
166
- return response.data;
167
- } catch (err) {
168
- throw handleAxiosError(err);
169
- }
170
- }
171
-
172
- /**
173
- * Makes an HTTP PUT request using Axios
174
- * @param url - The URL to make the PUT request to
175
- * @param data - The data to send in the request body
176
- * @param headers - Optional additional headers
177
- * @returns Promise resolving to the response data
178
- */
179
- export async function put<T = any>(
180
- url: string,
181
- data?: any,
182
- headers?: Record<string, string>,
183
- ): Promise<T> {
184
- try {
185
- const response: AxiosResponse<T> = await httpClient.put(url, data, { headers });
186
- return response.data;
187
- } catch (err) {
188
- throw handleAxiosError(err);
189
- }
190
- }
191
-
192
- /**
193
- * Makes an HTTP DELETE request using Axios
194
- * @param url - The URL to make the DELETE request to
195
- * @param headers - Optional additional headers
196
- * @returns Promise resolving to the response data
197
- */
198
- export async function del<T = any>(url: string, headers?: Record<string, string>): Promise<T> {
199
- try {
200
- const response: AxiosResponse<T> = await httpClient.delete(url, { headers });
201
- return response.data;
202
- } catch (err) {
203
- throw handleAxiosError(err);
204
- }
205
- }
206
-
207
- /**
208
- * Handles Axios errors and converts them to standardized Error objects
209
- * @param err - The Axios error or unknown error
210
- * @returns A standardized Error object
211
- */
212
- function handleAxiosError(err: unknown): Error {
213
- if (axios.isAxiosError(err)) {
214
- const axiosError = err as AxiosError;
215
-
216
- if (axiosError.response) {
217
- // Server responded with error status
218
- return new Error(`HTTP ${axiosError.response.status}: ${axiosError.response.statusText}`);
219
- } else if (axiosError.request) {
220
- // Request was made but no response received
221
- return new Error("Network error: No response received");
222
- } else {
223
- // Something else happened
224
- return new Error(`Request error: ${axiosError.message}`);
225
- }
226
- }
227
- return new Error("Unknown HTTP error");
228
- }
229
-
230
- /**
231
- * Converts caught errors into standardized HTTP error responses
232
- * Automatically maps common error types to appropriate status codes
233
- * @param err - The caught error (unknown type for safety)
234
- * @returns Formatted HTTP error response as APIGatewayProxyResult
235
- */
236
- export function handleError(err: unknown): APIGatewayProxyResult {
237
- let errorMessage = "Unknown error";
238
- let statusCode = 500;
239
-
240
- if (err instanceof Error) {
241
- errorMessage = err.message;
242
-
243
- // Map specific error messages to appropriate status codes
244
- if (err.message.includes("required") || err.message.includes("Invalid JSON")) {
245
- statusCode = 400;
246
- } else if (err.message.includes("HTTP 401")) {
247
- statusCode = 401;
248
- } else if (err.message.includes("HTTP 403")) {
249
- statusCode = 403;
250
- } else if (err.message.includes("HTTP 404")) {
251
- statusCode = 404;
252
- } else if (err.message.includes("Network error")) {
253
- statusCode = 503; // Service Unavailable
254
- }
255
- }
256
-
257
- return error(errorMessage, statusCode);
258
- }
259
-
260
- // Export the configured Axios instance for advanced usage
261
- export { httpClient };
@@ -1,57 +0,0 @@
1
- /**
2
- * Seeds a DynamoDB table with the provided data array.
3
- * Logs errors for each failed item insert but continues seeding.
4
- *
5
- * @param put - Function to perform the insert (e.g. yourTablePutFn(tableName, item))
6
- * @param tableName - The table to seed
7
- * @param data - Array of items to insert; can be any shape
8
- */
9
- export async function seedTable(
10
- put: (tableName: string, item: any) => Promise<void>,
11
- tableName: string,
12
- data: any[],
13
- ): Promise<void> {
14
- console.log(`Seeding ${tableName} with ${data.length} items...`);
15
- for (const item of data) {
16
- try {
17
- await put(tableName, item);
18
- } catch (error) {
19
- console.error(` ❌ Error seeding ${tableName}:`, error);
20
- }
21
- }
22
- }
23
-
24
- /**
25
- * Generates a data array, optionally post-processes it, and seeds it into a DynamoDB table.
26
- * Returns the data array after processing.
27
- *
28
- * @param put - Function to perform the insert (e.g. yourTablePutFn(tableName, item))
29
- * @param tableName - The table to seed
30
- * @param generator - Function generating the raw seed data array
31
- * @param postProcess - Optional transformation or filtering to apply to the generated data
32
- */
33
- export async function generateAndSeed<T>(
34
- put: (tableName: string, item: T) => Promise<void>,
35
- tableName: string,
36
- generator: () => T[],
37
- postProcess?: (data: T[]) => T[] | void
38
- ): Promise<T[]> {
39
- const start = performance.now();
40
- let data = generator();
41
- if (postProcess) {
42
- const result = postProcess(data);
43
- if (result) data = result;
44
- }
45
- // Seed with dependency-injected put
46
- for (const item of data) {
47
- try {
48
- await put(tableName, item);
49
- } catch (error) {
50
- console.error(` ❌ Error seeding ${tableName}:`, error);
51
- }
52
- }
53
- const end = performance.now();
54
-
55
- console.log(`✅ ${tableName} generated & seeded in ${((end - start) / 1000).toFixed(3)}s (${data.length} items)\n`);
56
- return data;
57
- }