Skip to content

Commit c778988

Browse files
committed
#5 add initial serverless project
1 parent 1f32b3d commit c778988

11 files changed

Lines changed: 12508 additions & 0 deletions

File tree

serverless/README.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Detention Data API
2+
3+
This is the backend API for the Detention Data project, built with the Serverless Framework and AWS services.
4+
5+
## Architecture
6+
7+
- **AWS Lambda**: Serverless functions for API endpoints and scheduled data collection
8+
- **Amazon DynamoDB**: NoSQL database for storing detention data
9+
- **Amazon API Gateway**: REST API with API key authentication
10+
- **AWS EventBridge**: Scheduled triggers for data collection
11+
- **AWS CloudWatch**: Logging and monitoring
12+
13+
## Project Structure
14+
15+
```
16+
serverless/
17+
├── api/ # API service
18+
│ ├── handlers/ # Lambda function handlers
19+
│ │ ├── data-collection.ts # Scheduled data collection
20+
│ │ ├── detainee.ts # Detainee CRUD operations
21+
│ │ └── status.ts # Health check endpoint
22+
│ └── serverless.yml # Serverless configuration
23+
├── package.json # Dependencies and scripts
24+
├── tsconfig.json # TypeScript configuration
25+
└── eslint.config.js # ESLint configuration
26+
```
27+
28+
## API Endpoints
29+
30+
- `GET /status` - Health check
31+
- `GET /detainee/{detaineeId}` - Get detainee records
32+
- `GET /detainees/active` - List active detainees
33+
34+
## Scheduled Functions
35+
36+
- **Data Collection**: Runs daily at 10 AM UTC to collect detention data from configured county sources
37+
38+
## Development
39+
40+
1. Install dependencies:
41+
42+
```bash
43+
npm install
44+
```
45+
46+
2. Deploy to dev environment:
47+
48+
```bash
49+
npx serverless deploy --stage dev
50+
```
51+
52+
3. Run tests:
53+
```bash
54+
npm test
55+
```
56+
57+
## Configuration
58+
59+
The scheduled data collection can be configured by adding additional schedule events in `serverless.yml`:
60+
61+
```yaml
62+
functions:
63+
dataCollection:
64+
events:
65+
- schedule:
66+
rate: cron(30 10 * * ? *)
67+
input: '{"countyId": "mecklenburg", "source": "mecklenburg-county"}'
68+
```
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import {
2+
APIGatewayProxyEvent,
3+
APIGatewayProxyResult,
4+
ScheduledEvent,
5+
} from "aws-lambda";
6+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
7+
import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb";
8+
9+
const dynamoClient = new DynamoDBClient({ region: process.env.AWS_REGION });
10+
const docClient = DynamoDBDocumentClient.from(dynamoClient);
11+
12+
interface JailDataEvent {
13+
countyId: string;
14+
source: string;
15+
}
16+
17+
interface DetentionRecord {
18+
detaineeId: string;
19+
timestamp: string;
20+
status: "ACTIVE" | "INACTIVE";
21+
createdDate: string;
22+
countyId: string;
23+
source: string;
24+
// Add other fields as needed
25+
firstName?: string;
26+
lastName?: string;
27+
bookingDate?: string;
28+
charges?: string[];
29+
ttl?: number;
30+
}
31+
32+
export const execute = async (
33+
event: ScheduledEvent | APIGatewayProxyEvent
34+
): Promise<APIGatewayProxyResult | void> => {
35+
try {
36+
console.log("Data collection started", JSON.stringify(event, null, 2));
37+
38+
// Parse input from scheduled event or API Gateway
39+
let inputData: JailDataEvent;
40+
41+
if ("source" in event && event.source === "aws.events") {
42+
// Scheduled event
43+
const scheduledEvent = event as ScheduledEvent;
44+
inputData = JSON.parse(
45+
scheduledEvent.detail ? JSON.stringify(scheduledEvent.detail) : "{}"
46+
);
47+
} else {
48+
// API Gateway event (for manual testing)
49+
const apiEvent = event as APIGatewayProxyEvent;
50+
inputData = JSON.parse(apiEvent.body || "{}");
51+
}
52+
53+
const { countyId, source } = inputData;
54+
55+
if (!countyId || !source) {
56+
const error = "Missing required parameters: countyId and source";
57+
console.error(error);
58+
59+
if ("httpMethod" in event) {
60+
return {
61+
statusCode: 400,
62+
body: JSON.stringify({ error }),
63+
};
64+
}
65+
return;
66+
}
67+
68+
// TODO: Implement actual data collection logic
69+
// This is a stub that would be replaced with real county data scraping
70+
const mockData = await collectJailData(countyId, source);
71+
72+
// Store the collected data
73+
const results = await Promise.all(
74+
mockData.map((record) => storeDetentionRecord(record))
75+
);
76+
77+
console.log(
78+
`Successfully processed ${results.length} records for ${countyId}`
79+
);
80+
81+
if ("httpMethod" in event) {
82+
return {
83+
statusCode: 200,
84+
body: JSON.stringify({
85+
message: `Successfully processed ${results.length} records`,
86+
countyId,
87+
source,
88+
}),
89+
};
90+
}
91+
} catch (error) {
92+
console.error("Error in data collection:", error);
93+
94+
if ("httpMethod" in event) {
95+
return {
96+
statusCode: 500,
97+
body: JSON.stringify({ error: "Internal server error" }),
98+
};
99+
}
100+
throw error;
101+
}
102+
};
103+
104+
async function collectJailData(
105+
countyId: string,
106+
source: string
107+
): Promise<DetentionRecord[]> {
108+
// This is a stub - replace with actual data collection logic
109+
console.log(`Collecting data for county: ${countyId}, source: ${source}`);
110+
111+
// Mock data for demonstration
112+
const now = new Date();
113+
const today = now.toISOString().split("T")[0];
114+
const timestamp = now.toISOString();
115+
116+
return [
117+
{
118+
detaineeId: `${countyId}-${Date.now()}-001`,
119+
timestamp,
120+
status: "ACTIVE",
121+
createdDate: today,
122+
countyId,
123+
source,
124+
firstName: "John",
125+
lastName: "Doe",
126+
bookingDate: today,
127+
charges: ["DWI", "Traffic Violation"],
128+
ttl: Math.floor(Date.now() / 1000) + 365 * 24 * 60 * 60, // 1 year TTL
129+
},
130+
];
131+
}
132+
133+
async function storeDetentionRecord(record: DetentionRecord): Promise<void> {
134+
const params = {
135+
TableName: process.env.JAILDATA_TABLE!,
136+
Item: record,
137+
};
138+
139+
await docClient.send(new PutCommand(params));
140+
console.log(`Stored record for detainee: ${record.detaineeId}`);
141+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
2+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
3+
import {
4+
DynamoDBDocumentClient,
5+
GetCommand,
6+
QueryCommand,
7+
} from "@aws-sdk/lib-dynamodb";
8+
9+
const dynamoClient = new DynamoDBClient({ region: process.env.AWS_REGION });
10+
const docClient = DynamoDBDocumentClient.from(dynamoClient);
11+
12+
export const get = async (
13+
event: APIGatewayProxyEvent
14+
): Promise<APIGatewayProxyResult> => {
15+
try {
16+
const detaineeId = event.pathParameters?.detaineeId;
17+
18+
if (!detaineeId) {
19+
return {
20+
statusCode: 400,
21+
body: JSON.stringify({ error: "Missing detaineeId parameter" }),
22+
};
23+
}
24+
25+
const params = {
26+
TableName: process.env.JAILDATA_TABLE!,
27+
KeyConditionExpression: "detaineeId = :detaineeId",
28+
ExpressionAttributeValues: {
29+
":detaineeId": detaineeId,
30+
},
31+
ScanIndexForward: false, // Most recent first
32+
Limit: 10, // Get last 10 records
33+
};
34+
35+
const result = await docClient.send(new QueryCommand(params));
36+
37+
if (!result.Items || result.Items.length === 0) {
38+
return {
39+
statusCode: 404,
40+
body: JSON.stringify({ error: "Detainee not found" }),
41+
};
42+
}
43+
44+
return {
45+
statusCode: 200,
46+
body: JSON.stringify({
47+
detaineeId,
48+
records: result.Items,
49+
count: result.Items.length,
50+
}),
51+
};
52+
} catch (error) {
53+
console.error("Error getting detainee:", error);
54+
return {
55+
statusCode: 500,
56+
body: JSON.stringify({ error: "Internal server error" }),
57+
};
58+
}
59+
};
60+
61+
export const listActive = async (
62+
event: APIGatewayProxyEvent
63+
): Promise<APIGatewayProxyResult> => {
64+
try {
65+
const limit = event.queryStringParameters?.limit
66+
? parseInt(event.queryStringParameters.limit)
67+
: 100;
68+
const daysBack = event.queryStringParameters?.days
69+
? parseInt(event.queryStringParameters.days)
70+
: 7;
71+
72+
// Calculate date range
73+
const endDate = new Date();
74+
const startDate = new Date(
75+
endDate.getTime() - daysBack * 24 * 60 * 60 * 1000
76+
);
77+
const startDateStr = startDate.toISOString().split("T")[0];
78+
79+
const params = {
80+
TableName: process.env.JAILDATA_TABLE!,
81+
IndexName: "StatusCreatedDateIndex",
82+
KeyConditionExpression: "#status = :status AND createdDate >= :startDate",
83+
ExpressionAttributeNames: {
84+
"#status": "status",
85+
},
86+
ExpressionAttributeValues: {
87+
":status": "ACTIVE",
88+
":startDate": startDateStr,
89+
},
90+
ScanIndexForward: false, // Most recent first
91+
Limit: limit,
92+
};
93+
94+
const result = await docClient.send(new QueryCommand(params));
95+
96+
return {
97+
statusCode: 200,
98+
body: JSON.stringify({
99+
activeDetainees: result.Items || [],
100+
count: result.Items?.length || 0,
101+
dateRange: {
102+
start: startDateStr,
103+
end: endDate.toISOString().split("T")[0],
104+
},
105+
}),
106+
};
107+
} catch (error) {
108+
console.error("Error listing active detainees:", error);
109+
return {
110+
statusCode: 500,
111+
body: JSON.stringify({ error: "Internal server error" }),
112+
};
113+
}
114+
};

serverless/api/handlers/status.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
2+
3+
export const get = async (
4+
event: APIGatewayProxyEvent
5+
): Promise<APIGatewayProxyResult> => {
6+
try {
7+
// Basic health check
8+
const status = {
9+
service: "detention-data-api",
10+
status: "healthy",
11+
timestamp: new Date().toISOString(),
12+
environment: process.env.STAGE || "unknown",
13+
version: "1.0.0",
14+
};
15+
16+
return {
17+
statusCode: 200,
18+
body: JSON.stringify(status),
19+
};
20+
} catch (error) {
21+
console.error("Error in status check:", error);
22+
return {
23+
statusCode: 500,
24+
body: JSON.stringify({
25+
service: "jaildata-api",
26+
status: "unhealthy",
27+
error: "Internal server error",
28+
timestamp: new Date().toISOString(),
29+
}),
30+
};
31+
}
32+
};

0 commit comments

Comments
 (0)