-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
CDK for fern-cloud works, time to try adding the lambda
- Loading branch information
Showing
7 changed files
with
271 additions
and
81 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
const HAS_HTTPS_REGEX = /^https?:\/\//i; | ||
|
||
export class ParsedBaseUrl { | ||
public readonly hostname: string; | ||
public readonly path: string | undefined; | ||
|
||
private constructor({ | ||
hostname, | ||
path, | ||
}: { | ||
hostname: string; | ||
path: string | undefined; | ||
}) { | ||
this.hostname = hostname; | ||
this.path = path; | ||
} | ||
|
||
public getFullUrl(): string { | ||
if (this.path == null) { | ||
return this.hostname; | ||
} | ||
return `${this.hostname}${this.path}`; | ||
} | ||
|
||
public toURL(): URL { | ||
return new URL(`https://${this.getFullUrl()}`); | ||
} | ||
|
||
public static parse(url: string): ParsedBaseUrl { | ||
try { | ||
let urlWithHttpsPrefix = url; | ||
if (!HAS_HTTPS_REGEX.test(url)) { | ||
urlWithHttpsPrefix = "https://" + url; | ||
} | ||
const parsedURL = new URL(urlWithHttpsPrefix); | ||
return new ParsedBaseUrl({ | ||
hostname: parsedURL.hostname, | ||
path: | ||
parsedURL.pathname === "/" || parsedURL.pathname === "" | ||
? undefined | ||
: parsedURL.pathname, | ||
}); | ||
} catch (e) { | ||
throw new Error( | ||
`Failed to parse URL: ${url}. The error was ${(e as Error)?.message}` | ||
); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,59 +1,168 @@ | ||
/* eslint-disable turbo/no-undeclared-env-vars */ | ||
import { FernVenusApi, FernVenusApiClient } from "@fern-api/venus-api-sdk"; | ||
import { APIGatewayProxyHandler } from "aws-lambda"; | ||
import { Client } from "pg"; | ||
import { createClient } from "redis"; | ||
import { ParsedBaseUrl } from "./ParsedBaseUrl"; | ||
import { DocumentData, GetDocsEvent } from "./types"; | ||
|
||
export const handler: APIGatewayProxyHandler = async (event) => { | ||
const client = new Client({ | ||
host: process.env.RDS_PROXY_ENDPOINT, | ||
database: process.env.DATABASE_NAME, | ||
port: 5432, | ||
ssl: { | ||
rejectUnauthorized: false, // Configure based on your security requirements | ||
}, | ||
// copied from https://github.com/fern-api/fern-platform/blob/main/servers/fdr/src/services/auth/AuthService.ts#L96 | ||
function getVenusClient({ token }: { token?: string }): FernVenusApiClient { | ||
return new FernVenusApiClient({ | ||
environment: process.env.venusUrl || "", | ||
token, | ||
}); | ||
} | ||
|
||
try { | ||
const { docId } = JSON.parse(event.body || "{}") as GetDocsEvent; | ||
const BEARER_REGEX = /^bearer\s+/i; | ||
export function getTokenFromAuthHeader(authHeader: string) { | ||
return authHeader.replace(BEARER_REGEX, ""); | ||
} | ||
|
||
if (!docId) { | ||
return { | ||
statusCode: 400, | ||
body: JSON.stringify({ error: "docId is required" }), | ||
}; | ||
} | ||
async function checkUserBelongsToOrg({ | ||
authHeader, | ||
orgId, | ||
}: { | ||
authHeader: string | undefined; | ||
orgId: string; | ||
}): Promise<void> { | ||
if (authHeader == null) { | ||
throw new Error("Authorization header was not specified"); | ||
} | ||
const token = getTokenFromAuthHeader(authHeader); | ||
const venus = getVenusClient({ token }); | ||
const response = await venus.organization.isMember( | ||
FernVenusApi.OrganizationId(orgId) | ||
); | ||
if (!response.ok) { | ||
throw new Error("Failed to resolve user's organizations"); | ||
} | ||
const belongsToOrg = response.body; | ||
if (!belongsToOrg) { | ||
throw new Error("User does not belong to organization"); | ||
} | ||
} | ||
|
||
await client.connect(); | ||
export const handler: APIGatewayProxyHandler = async (event) => { | ||
const { url } = JSON.parse(event.body || "") as GetDocsEvent; | ||
|
||
const query = ` | ||
SELECT id, content, version, created_at, updated_at | ||
FROM documents | ||
WHERE id = $1 | ||
`; | ||
if (!url) { | ||
return { | ||
statusCode: 400, | ||
body: JSON.stringify({ error: "url is required" }), | ||
}; | ||
} | ||
|
||
const result = await client.query<DocumentData>(query, [docId]); | ||
const parsedUrl = ParsedBaseUrl.parse(url); | ||
|
||
if (result.rows.length === 0) { | ||
return { | ||
statusCode: 404, | ||
body: JSON.stringify({ error: "Document not found" }), | ||
}; | ||
let client; | ||
|
||
const authHeader = event.headers.Authorization; | ||
if (authHeader) { | ||
try { | ||
await checkUserBelongsToOrg({ authHeader, orgId: "fern" }); | ||
} catch { | ||
throw new Error("User does not belong to fern"); | ||
} | ||
|
||
return { | ||
statusCode: 200, | ||
body: JSON.stringify(result.rows[0]), | ||
}; | ||
} catch (error) { | ||
console.error("Error:", error); | ||
// first - try to get from redis cache | ||
const redis = createClient({ | ||
url: `redis://${process.env.REDIS_ENDPOINT}`, | ||
}); | ||
|
||
await redis.connect(); | ||
const cachedResponse = await redis.get(parsedUrl.getFullUrl()); | ||
if (cachedResponse != null) { | ||
console.log(`Cache HIT for ${url}`); | ||
console.log(cachedResponse); | ||
// const filesV2 = Object.fromEntries( | ||
// await Promise.all( | ||
// Object.entries(cachedResponse.dbFiles).map( | ||
// async ([fileId, dbFileInfo]) => { | ||
// const presignedUrl = | ||
// await this.app.services.s3.getPresignedDocsAssetsDownloadUrl({ | ||
// key: dbFileInfo.s3Key, | ||
// isPrivate: | ||
// cachedResponse.usesPublicS3 === true ? false : true, | ||
// }); | ||
|
||
// switch (dbFileInfo.type) { | ||
// case "image": { | ||
// const { s3Key, ...image } = dbFileInfo; | ||
// return [fileId, { ...image, url: presignedUrl }]; | ||
// } | ||
// default: | ||
// return [fileId, { type: "url", url: presignedUrl }]; | ||
// } | ||
// } | ||
// ) | ||
// ) | ||
// ); | ||
// return { | ||
// ...cachedResponse.response, | ||
// definition: { | ||
// ...cachedResponse.response.definition, | ||
// filesV2, | ||
// }, | ||
// }; | ||
} else { | ||
console.log(`Cache MISS for ${url}`); | ||
console.log("Connecting to RDS"); | ||
|
||
// second - try to get from rds db | ||
try { | ||
client = new Client({ | ||
host: process.env.RDS_PROXY_ENDPOINT, | ||
port: 5432, | ||
// The RDS Proxy will use IAM authentication, so we don't need username/password | ||
ssl: { | ||
rejectUnauthorized: false, | ||
}, | ||
}); | ||
|
||
await client.connect(); | ||
|
||
const query = ` | ||
SELECT url, docsDefinition | ||
FROM Docs | ||
WHERE url = $1 | ||
`; | ||
|
||
const result = await client.query<DocumentData>(query, [parsedUrl]); | ||
|
||
if (result.rows.length === 0) { | ||
return { | ||
statusCode: 404, | ||
body: JSON.stringify({ error: "Document not found" }), | ||
}; | ||
} | ||
|
||
console.log("found result"); | ||
console.log(result.rows[0]); | ||
return { | ||
statusCode: 200, | ||
body: JSON.stringify(result.rows[0]), | ||
}; | ||
} catch (error) { | ||
console.error("Error:", error); | ||
return { | ||
statusCode: 500, | ||
body: JSON.stringify({ | ||
error: "Internal server error", | ||
message: error instanceof Error ? error.message : "Unknown error", | ||
}), | ||
}; | ||
} finally { | ||
if (client) { | ||
await client.end().catch(console.error); | ||
} | ||
} | ||
} | ||
return { | ||
statusCode: 500, | ||
body: JSON.stringify({ | ||
error: "Internal server error", | ||
message: error instanceof Error ? error.message : "Unknown error", | ||
}), | ||
body: "Internal server error", | ||
}; | ||
} finally { | ||
await client.end(); | ||
} else { | ||
throw new Error("Authorization header was not specified"); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.