-
Notifications
You must be signed in to change notification settings - Fork 878
/
Copy pathindex.ts
146 lines (125 loc) · 4.75 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
// Copyright 2016-2025, Pulumi Corporation. All rights reserved.
import * as aws from "@pulumi/aws";
import * as cloud from "@pulumi/cloud-aws";
import * as pulumi from "@pulumi/pulumi";
import * as express from "express";
import * as cache from "./cache";
type AsyncRequestHandler = (req: express.Request, res: express.Response, next: express.NextFunction) => Promise<void>;
const asyncMiddleware = (fn: AsyncRequestHandler) => {
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
// Create a table `urls`, with `name` as primary key.
const urlTable = new aws.dynamodb.Table("urls", {
attributes: [{
name: "name",
type: "S",
}],
hashKey: "name",
billingMode: "PAY_PER_REQUEST",
});
async function scanTable() {
const items: any[] = [];
const db = new aws.sdk.DynamoDB.DocumentClient();
const params = {
TableName: urlTable.name.get(),
ConsistentRead: true,
ExclusiveStartKey: undefined,
};
do {
const result = await db.scan(params).promise();
if (result.Items) {
items.push(...result.Items);
}
params.ExclusiveStartKey = <any>result.LastEvaluatedKey;
}
while (params.ExclusiveStartKey !== undefined);
return items;
}
// Create a cache of frequently accessed urls.
const urlCache = new cache.Cache("urlcache");
// Create a web server.
const httpServer = new cloud.HttpServer("urlshortener", () => {
const app = express();
// GET /url lists all URLs currently registered.
app.get("/url", asyncMiddleware(async (req, res) => {
try {
const items = await scanTable();
res.status(200).json(items);
console.log(`GET /url retrieved ${items.length} items`);
} catch (err) {
res.status(500).json(err.stack);
console.log(`GET /url error: ${err.stack}`);
}
}));
// GET /url/{name} redirects to the target URL based on a short-name.
app.get("/url/:name", asyncMiddleware(async (req, res) => {
const name = req.params.name;
try {
// First try the Redis cache.
let url = await urlCache.get(name);
if (url) {
console.log(`Retrieved value from Redis: ${url}`);
res.setHeader("X-Powered-By", "redis");
}
else {
// If we didn't find it in the cache, consult the table.
const db = new aws.sdk.DynamoDB.DocumentClient();
const result = await db.get({
TableName: urlTable.name.get(),
Key: {name},
ConsistentRead: true,
}).promise();
const value = result.Item;
url = value && value.url;
if (url) {
urlCache.set(name, url); // cache it for next time.
}
}
// If we found an entry, 301 redirect to it; else, 404.
if (url) {
res.setHeader("Location", url);
res.status(302);
res.end("");
console.log(`GET /url/${name} => ${url}`);
}
else {
res.status(404);
res.end("");
console.log(`GET /url/${name} is missing (404)`);
}
} catch (err) {
res.status(500).json(err.stack);
console.log(`GET /url/${name} error: ${err.stack}`);
}
}));
// POST /url registers a new URL with a given short-name.
app.post("/url", asyncMiddleware(async (req, res) => {
const url = <string>req.query["url"];
const name = <string>req.query["name"];
try {
const db = new aws.sdk.DynamoDB.DocumentClient();
await db.put({
TableName: urlTable.name.get(),
Item: { name, url },
}).promise();
await urlCache.set(name, url);
res.json({ shortenedURLName: name });
console.log(`POST /url/${name} => ${url}`);
} catch (err) {
res.status(500).json(err.stack);
console.log(`POST /url/${name} => ${url} error: ${err.stack}`);
}
}));
// Serve all files in the www directory to the root.
// Note: www will be auto-included using config. either
// cloud-aws:functionIncludePaths or
// staticRoutes(app, "/", "www");
app.use("/", express.static("www"));
app.get("*", (req, res) => {
res.json({ uncaught: { url: req.url, baseUrl: req.baseUrl, originalUrl: req.originalUrl, version: process.version } });
});
return app;
});
export let endpointUrl = pulumi.interpolate `${httpServer.url}index.html`;