Skip to content

Commit

Permalink
fix(tooling): add API proxy code and move worker to PL account (#1237)
Browse files Browse the repository at this point in the history
  • Loading branch information
hugomrdias authored Oct 28, 2020
1 parent 4ae1e90 commit 1a7c4c7
Show file tree
Hide file tree
Showing 7 changed files with 318 additions and 2 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ package-lock.json
node_modules
resources
static/_gen
data/toc.json
data/toc.json
api/dist
api/worker
9 changes: 9 additions & 0 deletions api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## Usage

Install Cloudflare Wrangler

```bash
wrangler login # to get the token

wrangler publish # to publish the worker to specs-api.protocol-labs.workers.dev
```
162 changes: 162 additions & 0 deletions api/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
const Router = require('./router')
const dlv = require('dlv')
const merge = require('merge-options')
const nanoid = require('nanoid/non-secure')

const cacheBust = nanoid.nanoid()
/**
* Example of how router can be used in an application
* */
addEventListener('fetch', (event) => {
event.respondWith(handleRequest(event))
})

async function handleRequest(event) {
const r = new Router()
// Replace with the appropriate paths and handlers
r.get('.*/cov', () => cov(event))
r.get('.*/github', () => github(event))
r.get('/', () => new Response('Hello worker!')) // return a default message for the root route

try {
return await r.route(event.request)
} catch (err) {
console.log('handleRequest -> err', err.stack)
return new Response(err.message, {
status: 500,
statusText: 'internal server error',
headers: {
'content-type': 'text/plain',
},
})
}
}

async function cov(event) {
const url = new URL(event.request.url)
// https://github.com/filecoin-project/lotus
const repo = url.searchParams.get('repo').split('/').slice(3).join('/')
const data = await get(event, {
url: `https://codecov.io/api/gh/${repo}`,
transform: (data) => {
const out = {
cov: dlv(data, 'commit.totals.c', 0),
ci: dlv(data, 'commit.ci_passed', false),
repo: dlv(data, 'repo.name', 'N/A'),
org: dlv(data, 'owner.username', 'N/A'),
lang: dlv(data, 'repo.language', 'N/A'),
}
return out
},
})
return data
}

async function github(event) {
const url = new URL(event.request.url)
const file = url.searchParams.get('file').split('/')
// https://github.com/filecoin-project/lotus/blob/master/paychmgr/paych.go
const repo = file.slice(3, 5).join('/')
const path = file.slice(7).join('/')
const ref = file[6]
const headers = {
'User-Agent': 'hugomrdias',
// Authorization: `token ${GITHUB_TOKEN}`
}

const treeUrlRsp = await get(event, {
url: `https://api.github.com/repos/${repo}/commits?sha=${ref}&per_page=1&page=1`,
headers,
})
const treeUrl = await treeUrlRsp.json()

const data = await get(event, {
url: `https://api.github.com/repos/${repo}/contents/${path}?ref=${ref}`,
transform: (data) => {
return {
content: data.content,
size: data.size,
url: `https://github.com/${repo}/tree/${treeUrl[0].sha}/${path}`,
}
},
headers,
})
return data
}

async function get(event, options) {
const { url, transform, force, headers } = merge(
{
url: '',
transform: (d) => d,
force: false,
headers: {},
},
options
)

const cache = caches.default
const cacheKey = url + cacheBust
const cacheTTL = 86400 * 2 // 2 days
const cacheRevalidateTTL = 3600 * 2 // 2 hours
const cachedResponse = await cache.match(cacheKey)

if (force || !cachedResponse) {
console.log('Cache miss for ', cacheKey)
// if not in cache get from the origin
const response = await fetch(url, {
headers: {
...headers,
'If-None-Match': cachedResponse
? cachedResponse.headers.get('ETag')
: null,
},
})

if (response.ok) {
const { headers } = response
const contentType = headers.get('content-type') || ''

if (contentType.includes('application/json')) {
// transform the data
const data = transform(await response.json())

// build new response with the transformed body
const transformedResponse = new Response(JSON.stringify(data), {
headers: {
'Content-Type': 'application/json;charset=UTF-8',
'Cache-Control': `max-age=${cacheTTL}`,
'X-RateLimit-Limit': headers.get('X-RateLimit-Limit'),
'X-RateLimit-Remaining': headers.get('X-RateLimit-Remaining'),
'X-RateLimit-Reset': headers.get('X-RateLimit-Reset'),
ETag: headers.get('ETag'),
},
})

// save response to cache
event.waitUntil(cache.put(cacheKey, transformedResponse.clone()))

return transformedResponse
} else {
throw new Error(
`Request error content type not supported. ${contentType}`
)
}
} else if (response.status === 304) {
// renew cache response
event.waitUntil(cache.put(cacheKey, cachedResponse.clone()))
return cachedResponse.clone()
} else {
return response
}
} else {
console.log('Cache hit for ', cacheKey, cachedResponse.headers.get('age'))
const cacheAge = cachedResponse.headers.get('age')

if (cacheAge > cacheRevalidateTTL) {
console.log('Cache is too old, revalidating...')
event.waitUntil(get(event, { url, transform, force: true }))
}
return cachedResponse
}
}
16 changes: 16 additions & 0 deletions api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "specs-api",
"version": "1.0.0",
"description": "spec proxy api",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Hugo Dias <hugomrdias@gmail.com>",
"license": "MIT",
"dependencies": {
"dlv": "^1.1.3",
"merge-options": "^3.0.3",
"nanoid": "^3.1.16"
}
}
121 changes: 121 additions & 0 deletions api/router.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* Helper functions that when passed a request will return a
* boolean indicating if the request uses that HTTP method,
* header, host or referrer.
*/
const Method = (method) => (req) =>
req.method.toLowerCase() === method.toLowerCase()
const Connect = Method('connect')
const Delete = Method('delete')
const Get = Method('get')
const Head = Method('head')
const Options = Method('options')
const Patch = Method('patch')
const Post = Method('post')
const Put = Method('put')
const Trace = Method('trace')

const Header = (header, val) => (req) => req.headers.get(header) === val
const Host = (host) => Header('host', host.toLowerCase())
const Referrer = (host) => Header('referrer', host.toLowerCase())

const Path = (regExp) => (req) => {
const url = new URL(req.url)
const path = url.pathname
const match = path.match(regExp) || []
return match[0] === path
}

/**
* The Router handles determines which handler is matched given the
* conditions present for each request.
*/
class Router {
constructor() {
this.routes = []
}

handle(conditions, handler) {
this.routes.push({
conditions,
handler,
})
return this
}

connect(url, handler) {
return this.handle([Connect, Path(url)], handler)
}

delete(url, handler) {
return this.handle([Delete, Path(url)], handler)
}

get(url, handler) {
return this.handle([Get, Path(url)], handler)
}

head(url, handler) {
return this.handle([Head, Path(url)], handler)
}

options(url, handler) {
return this.handle([Options, Path(url)], handler)
}

patch(url, handler) {
return this.handle([Patch, Path(url)], handler)
}

post(url, handler) {
return this.handle([Post, Path(url)], handler)
}

put(url, handler) {
return this.handle([Put, Path(url)], handler)
}

trace(url, handler) {
return this.handle([Trace, Path(url)], handler)
}

all(handler) {
return this.handle([], handler)
}

route(req) {
const route = this.resolve(req)

if (route) {
return route.handler(req)
}

return new Response('resource not found', {
status: 404,
statusText: 'not found',
headers: {
'content-type': 'text/plain',
},
})
}

/**
* resolve returns the matching route for a request that returns
* true for all conditions (if any).
*/
resolve(req) {
return this.routes.find((r) => {
if (!r.conditions || (Array.isArray(r) && !r.conditions.length)) {
return true
}

if (typeof r.conditions === 'function') {
return r.conditions(req)
}

return r.conditions.every((c) => c(req))
})
}
}

module.exports = Router
6 changes: 6 additions & 0 deletions api/wrangler.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
name = "specs-api"
type = "webpack"
account_id = "fffa4b4363a7e5250af8357087263b3a"
workers_dev = true
route = ""
zone_id = ""
2 changes: 1 addition & 1 deletion config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ enableGitInfo = true
# weight = 10

[params]
API = 'https://specs-api.hugomrdias.workers.dev'
API = 'https://specs-api.protocol-labs.workers.dev'
# (Optional, default true) Controls table of contents visibility on right side of pages.
# Start and end levels can be controlled with markup.tableOfContents setting.
# You can also specify this parameter per page in front matter.
Expand Down

0 comments on commit 1a7c4c7

Please sign in to comment.