Skip to content

Commit

Permalink
Merge pull request #12 from 47chapters/dev
Browse files Browse the repository at this point in the history
v2.0.0: scheduler support and shadcn UI dependency
  • Loading branch information
tjanczuk authored Dec 22, 2023
2 parents d743a78 + dad05ee commit 5d71869
Show file tree
Hide file tree
Showing 215 changed files with 7,911 additions and 1,031 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ New SaaS businesses cut corners that are hard to fix later. LetsGo gives you a f

This project provides you with the architecture and the tooling that will put your startup on a solid foundation from day one. It helps you save months of work leading to the launch while allowing you to focus on the essence of your product. The day you let the first customer in, you have no technical debt. As you grow, you can continue focusing your resources on what matters most: your customers and your product.

<img width="837" alt="image" src="https://github.com/tjanczuk/letsgo/assets/822369/f7fe2317-d7de-4698-b093-416a52a1a145">
<img width="950" alt="LetsGo Architecture" src="https://github.com/47chapters/letsgo/assets/822369/c9e803b3-c2ee-4b2e-b8a4-16e107342c4e">

LetsGo does it by providing a prescriptive architecture implemented with a modern set of technologies and robust operational tooling for managing your app in AWS. On day one you get more than most startups build in the first two years:

Expand Down Expand Up @@ -51,6 +51,7 @@ Let's go!
[Develop the API](./docs/how-to/develop-the-api.md)
[Develop the worker](./docs/how-to/develop-the-worker.md)
[Enqueue asynchronous work](./docs/how-to/enqueue-asynchronous-work.md)
[Schedule asynchronous work](./docs/how-to/schedule-asynchronous-work.md)
[Access data in the database from code](./docs/how-to/access-data-in-the-database-from-code.md)
[Process the contact form](./docs/how-to/process-the-contact-form.md)
[Manage configuration](./docs/how-to/manage-configuration.md)
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/routes/me.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const meHandler: RequestHandler = async (req, res, next) => {
isBuiltInIssuer(
(request.user?.decodedJwt?.payload as JwtPayload).iss || ""
);
const returnAccessToken = req.query.returnAccessToken !== undefined;
let tenants = await getTenantsOfIdentity({
identity: request.user.identity,
});
Expand Down Expand Up @@ -54,6 +55,7 @@ export const meHandler: RequestHandler = async (req, res, next) => {
const body: GetMeResponse = {
identityId: request.user.identityId,
identity: request.user.identity,
accessToken: returnAccessToken ? request.user.jwt : undefined,
tenants,
};
return res.json(body);
Expand Down
16 changes: 13 additions & 3 deletions apps/api/src/routes/tenant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ router.get(
// Redirect the browser back to the dashboard with appropriate next step
let location: string;
if (response.status === "succeeded") {
location = createAbsoluteWebUrl(`/manage/${tenantId}/settings`);
location = createAbsoluteWebUrl(`/manage/${tenantId}/subscription`);
} else if (response.status === "processing") {
location = createAbsoluteWebUrl(
`/manage/${tenantId}/paymentmethod/processing`
Expand Down Expand Up @@ -320,7 +320,12 @@ router.post(
// No card number yet, it will be created next
};
await putTenant(tenant);
res.json(response);
// Subscription is activated immediately if the Stripe user had a balance on their account.
if (response.status === "active") {
res.status(200).send();
} else {
res.json(response);
}
return;
} else {
// current plan is not Stripe, new plan is not Stripe
Expand Down Expand Up @@ -424,8 +429,13 @@ router.get(
const invitations = await getInvitations({
tenantId: req.params.tenantId,
});
const sortedInvitations = invitations.sort((i1, i2) => {
const e1 = new Date(i1.expiresAt).getTime();
const e2 = new Date(i2.expiresAt).getTime();
return e1 > e2 ? 1 : e1 < e2 ? -1 : 0;
});
const response: GetInvitationsResponse = {
invitations: invitations.map(pruneResponse),
invitations: sortedInvitations.map(pruneResponse),
};
res.json(response);
} catch (e) {
Expand Down
3 changes: 2 additions & 1 deletion apps/ops/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@
"@aws-sdk/client-ecr": "^3.421.0",
"@aws-sdk/client-iam": "^3.421.0",
"@aws-sdk/client-lambda": "^3.427.0",
"@aws-sdk/client-scheduler": "^3.476.0",
"@aws-sdk/client-sqs": "^3.425.0",
"@aws-sdk/client-ssm": "^3.421.0",
"@aws-sdk/client-sts": "^3.421.0",
"@letsgo/constants": "*",
"@letsgo/queue": "*",
"@letsgo/db": "*",
"@letsgo/queue": "*",
"@letsgo/trust": "*",
"chalk": "4",
"commander": "^11.0.0",
Expand Down
2 changes: 1 addition & 1 deletion apps/ops/src/aws/iam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const LambdaAssumeRolePolicy = {
{
Effect: "Allow",
Principal: {
Service: "lambda.amazonaws.com",
Service: ["lambda.amazonaws.com", "scheduler.amazonaws.com"],
},
Action: "sts:AssumeRole",
},
Expand Down
40 changes: 40 additions & 0 deletions apps/ops/src/aws/lambda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
CreateEventSourceMappingCommandOutput,
UpdateEventSourceMappingCommand,
GetEventSourceMappingCommand,
DeleteEventSourceMappingCommand,
} from "@aws-sdk/client-lambda";
import { setLogGroupRetentionPolicy } from "./cloudwatch";
import { Logger } from "../commands/defaults";
Expand All @@ -24,6 +25,7 @@ import { getOneLetsGoQueue, getQueueArnFromQueueUrl } from "./sqs";
import { getTagsAsObject } from "./defaults";
import chalk from "chalk";
import { ServiceUrls, getServiceUrlEnvironmentVariables } from "./apprunner";
import { getAccountId } from "./sts";

const MaxFunctionWaitTime = 60 * 5; // 5 minutes
const MaxEventSourceMappingWaitTime = 60 * 5; // 5 minutes
Expand Down Expand Up @@ -145,6 +147,38 @@ export async function enableOrDisableEventSourceMapping(
}
}

async function deleteEventSourceMapping(
region: string,
deployment: string,
functionName: string,
logger: Logger
) {
logger(`deleting event source mapping...`, "aws:lambda");
const queueUrl = await getOneLetsGoQueue(region, deployment, logger);
if (!queueUrl) {
logger(
chalk.yellow(`cannot locate event source mapping: queue not found`),
"aws:sqs"
);
return;
}
const queueArn = await getQueueArnFromQueueUrl(region, queueUrl || "");
const eventSource = await getEventSourceMapping(
region,
functionName,
queueArn,
logger
);
const lambda = getLambdaClient(region);
if (eventSource) {
const command = new DeleteEventSourceMappingCommand({
UUID: eventSource.UUID || "",
});
await lambda.send(command);
}
logger("event source mapping deleted", "aws:lambda");
}

async function getEventSourceMappingFromUuid(region: string, uuid: string) {
const lambda = getLambdaClient(region);
const getEventSourceMappingCommand = new GetEventSourceMappingCommand({
Expand Down Expand Up @@ -641,9 +675,11 @@ export async function ensureLambda(

export async function deleteLambda(
region: string,
deployment: string,
functionName: string,
logger: Logger
) {
await deleteEventSourceMapping(region, deployment, functionName, logger);
const existing = await getLambda(region, functionName);
if (!existing) {
return;
Expand All @@ -656,3 +692,7 @@ export async function deleteLambda(
await lambda.send(deleteCommand);
logger(`function ${functionName} deleted`, "aws:lambda");
}

export async function getFunctionArn(region: string, functionName: string) {
return `arn:aws:lambda:${region}:${await getAccountId()}:function:${functionName}`;
}
Loading

0 comments on commit 5d71869

Please sign in to comment.