From 04f221b173ef928e78d6750ba94960c420e09485 Mon Sep 17 00:00:00 2001 From: Lucas Dale <47707510+lucasdale99@users.noreply.github.com> Date: Tue, 12 Nov 2024 14:59:51 -0500 Subject: [PATCH 01/14] Update issue templates --- .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From 3cc8031ff6207b379302e5dc5fb2a5fdf390720d Mon Sep 17 00:00:00 2001 From: Lucas Dale Date: Tue, 12 Nov 2024 15:43:47 -0500 Subject: [PATCH 02/14] Add workflow to deploy --- .github/workflows/main.yaml | 25 ++++++++++++++++++++++--- README.md | 2 +- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index d18549f..1ea74b8 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -11,6 +11,25 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 20 - # - run: npm install -g pnpm - # - run: pnpm ci - # - run: pnpm test + - name: Install pnpm + run: npm install -g pnpm + + - name: Install dependencies + run: pnpm install + + - name: Install AWS Creds + run: | + mkdir -p ~/.aws + echo "[default]" > ~/.aws/credentials + echo "aws_access_key_id=${{ secrets.AWS_ACCESS_ID }}" >> ~/.aws/credentials + echo "aws_secret_access_key=${{ secrets.AWS_SECRET_ACCESS_KEY }}" >> ~/.aws/credentials + + - name: Set SST Config Secret + run: | + npx sst secrets set COMMIT_SHA=${{ github.sha }} --stage production + + - name: Deploy with SST + run: pnpm run deploy + + - name: Clean up AWS Profile + run: rm -rf ~/.aws diff --git a/README.md b/README.md index b1d9bf4..feb9db9 100644 --- a/README.md +++ b/README.md @@ -92,5 +92,5 @@ sst secret set NEXT_PUBLIC_URL --env ## Contributing 1. Fork the repository -2. Create a feature branch +2. Create a feature branch or create an issue 3. Submit a pull request From 127d8c220654eac07adae5d45501ee9c0a23cdf2 Mon Sep 17 00:00:00 2001 From: Lucas Dale Date: Tue, 12 Nov 2024 17:09:51 -0500 Subject: [PATCH 03/14] Add staging workflow --- .github/workflows/main.yaml | 14 +++++++++- .github/workflows/stage.yaml | 53 ++++++++++++++++++++++++++++++++++++ cli/migrator.ts | 51 ++++++++++++++++++++++++++++++++++ lib/secrets.ts | 4 +-- package.json | 3 ++ pnpm-lock.yaml | 10 +++++++ 6 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/stage.yaml create mode 100644 cli/migrator.ts diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 1ea74b8..d30005b 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -1,4 +1,4 @@ -name: Deploy LetUsDev +name: One-off Deploy on: workflow_dispatch: @@ -11,6 +11,18 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 20 + + - name: Cache Next.js Build + uses: actions/cache@v4 + with: + path: | + .next/ + .open-next/ + .sst/ + key: cache-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles("**.[jt]s', '**.[jt]xs', ) }} + restore-keys: | + cache-${{ hashFiles('**/pnpm-lock.yaml') }} + - name: Install pnpm run: npm install -g pnpm diff --git a/.github/workflows/stage.yaml b/.github/workflows/stage.yaml new file mode 100644 index 0000000..2eeb579 --- /dev/null +++ b/.github/workflows/stage.yaml @@ -0,0 +1,53 @@ +name: Stage Deploy via SST + +on: + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Cache Next.js Build + uses: actions/cache@v4 + with: + path: | + .next/ + .open-next/ + .sst/ + key: cache-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles("**.[jt]s', '**.[jt]xs', ) }} + restore-keys: | + cache-${{ hashFiles('**/pnpm-lock.yaml') }} + + - name: Install pnpm + run: npm install -g pnpm + + - name: Install dependencies + run: pnpm install + + - name: Install AWS Creds + run: | + mkdir -p ~/.aws + echo "[default]" > ~/.aws/credentials + echo "aws_access_key_id=${{ secrets.AWS_ACCESS_ID }}" >> ~/.aws/credentials + echo "aws_secret_access_key=${{ secrets.AWS_SECRET_ACCESS_KEY }}" >> ~/.aws/credentials + + - name: Set SST Config Secret + run: | + npx sst secrets set DATABASE_URL=${{ secrets.DATABASE_URL_STAGING }} --stage staging + npx sst secrets set NEXT_PUBLIC_SITE_URL=${{ secrets.NEXT_PUBLIC_SITE_URL_STAGING }} --stage staging + npx sst secrets set COMMIT_SHA=${{ github.sha }} --stage staging + + - name: Run staging DB Migrations + run: | + STAGE=staging tsx cli/migrator.ts + + - name: Deploy with SST + run: pnpm run deploy:stage + + - name: Clean up AWS Profile + run: rm -rf ~/.aws diff --git a/cli/migrator.ts b/cli/migrator.ts new file mode 100644 index 0000000..378f41f --- /dev/null +++ b/cli/migrator.ts @@ -0,0 +1,51 @@ +import { migrate } from "drizzle-orm/postgres-js/migrator"; +import { drizzle } from "drizzle-orm/neon-serverless"; +import { Pool, neonConfig } from "@neondatabase/serverless"; +import ws from "ws"; + +import * as schema from "../lib/schema"; +import getSecret from "../lib/secrets"; + +async function performMigration() { + const dbUrlResponse = await getSecret("DATABASE_URL"); + const dbUrl = dbUrlResponse?.Parameter?.Value; + if (!dbUrl) { + console.error("DATABASE_URL not found"); + return; + } + + neonConfig.webSocketConstructor = ws; // <-- this is the key bit + + const pool = new Pool({ connectionString: dbUrl }); + pool.on("error", (err) => console.error(err)); // deal with e.g. re-connect + + const client = await pool.connect(); + + try { + await client.query("BEGIN"); + const db = await drizzle(client, { schema }); + await migrate(db, { migrationsFolder: "src/migrations" }); + await client.query("COMMIT"); + } catch (err) { + await client.query("ROLLBACK"); + throw err; + } finally { + client.release(); + } + + await pool.end(); +} + +if (require.main === module) { + console.log("Running migrations"); + + performMigration() + .then((val) => { + console.log("Migration performed"); + process.exit(0); + }) + .catch((err) => { + console.log("err", err); + process.exit(1); + }); +} diff --git a/lib/secrets.ts b/lib/secrets.ts index 82ee9b0..42d1c9d 100644 --- a/lib/secrets.ts +++ b/lib/secrets.ts @@ -1,11 +1,11 @@ import { GetParameterCommand, SSMClient } from "@aws-sdk/client-ssm"; //Grab Secrets from AWS Parameter Store -const STAGE = "production"; +const STAGE = process.env.STAGE ? process.env.STAGE : "production"; const PROJECT = "letusdev"; const REGION = "us-east-1"; -export async function getSecret(secretName: string) { +export default async function getSecret(secretName: string) { if (!secretName) { throw new Error("Secret name is required"); } diff --git a/package.json b/package.json index e4a3ee6..725d574 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,8 @@ "dev": "next dev", "start": "next start", "deploy": "sst deploy --stage production", + "migrate": "tsx cli/migrator.ts", + "deploy:stage": "sst deploy --stage staging", "takeDown": "npx sst remove --stage production", "generate": "npx drizzle-kit generate", "db": "sst shell drizzle-kit" @@ -49,6 +51,7 @@ "@types/node": "20.11.5", "@types/react": "18.2.48", "@types/react-dom": "18.2.18", + "@types/ws": "^8.5.13", "aws-cdk-lib": "2.142.1", "drizzle-kit": "^0.27.1", "tsx": "^4.19.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e2f595..1c91efb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -114,6 +114,9 @@ importers: '@types/react-dom': specifier: 18.2.18 version: 18.2.18 + '@types/ws': + specifier: ^8.5.13 + version: 8.5.13 aws-cdk-lib: specifier: 2.142.1 version: 2.142.1(constructs@10.4.2) @@ -1951,6 +1954,9 @@ packages: '@types/uuid@9.0.8': resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} + '@types/ws@8.5.13': + resolution: {integrity: sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==} + '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} @@ -5849,6 +5855,10 @@ snapshots: '@types/uuid@9.0.8': {} + '@types/ws@8.5.13': + dependencies: + '@types/node': 20.11.5 + '@ungap/structured-clone@1.2.0': {} abbrev@2.0.0: {} From 6fbcbc3a9641af9c0d05eccc49e02a86720fb4f5 Mon Sep 17 00:00:00 2001 From: Lucas Dale Date: Tue, 12 Nov 2024 17:10:36 -0500 Subject: [PATCH 04/14] Add npx to command on staging workflow --- .github/workflows/stage.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stage.yaml b/.github/workflows/stage.yaml index 2eeb579..4316517 100644 --- a/.github/workflows/stage.yaml +++ b/.github/workflows/stage.yaml @@ -44,7 +44,7 @@ jobs: - name: Run staging DB Migrations run: | - STAGE=staging tsx cli/migrator.ts + STAGE=staging npx tsx cli/migrator.ts - name: Deploy with SST run: pnpm run deploy:stage From b020d5f146424a6880ed76669ca76f14f5b351bf Mon Sep 17 00:00:00 2001 From: Lucas Dale Date: Tue, 12 Nov 2024 17:14:03 -0500 Subject: [PATCH 05/14] Fix stage yaml --- .github/workflows/stage.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stage.yaml b/.github/workflows/stage.yaml index 4316517..4174266 100644 --- a/.github/workflows/stage.yaml +++ b/.github/workflows/stage.yaml @@ -19,7 +19,7 @@ jobs: .next/ .open-next/ .sst/ - key: cache-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles("**.[jt]s', '**.[jt]xs', ) }} + key: cache-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**.[jt]s', '**.[jt]xs') }} restore-keys: | cache-${{ hashFiles('**/pnpm-lock.yaml') }} From d8ad1e444a2fba878ff85e0d304bfda0cbcaea07 Mon Sep 17 00:00:00 2001 From: Lucas Dale Date: Tue, 12 Nov 2024 17:16:28 -0500 Subject: [PATCH 06/14] Fix secret command in sst --- .github/workflows/main.yaml | 2 +- .github/workflows/stage.yaml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index d30005b..db370b5 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -38,7 +38,7 @@ jobs: - name: Set SST Config Secret run: | - npx sst secrets set COMMIT_SHA=${{ github.sha }} --stage production + npx sst secret set COMMIT_SHA=${{ github.sha }} --stage production - name: Deploy with SST run: pnpm run deploy diff --git a/.github/workflows/stage.yaml b/.github/workflows/stage.yaml index 4174266..a41bd3f 100644 --- a/.github/workflows/stage.yaml +++ b/.github/workflows/stage.yaml @@ -38,9 +38,9 @@ jobs: - name: Set SST Config Secret run: | - npx sst secrets set DATABASE_URL=${{ secrets.DATABASE_URL_STAGING }} --stage staging - npx sst secrets set NEXT_PUBLIC_SITE_URL=${{ secrets.NEXT_PUBLIC_SITE_URL_STAGING }} --stage staging - npx sst secrets set COMMIT_SHA=${{ github.sha }} --stage staging + npx sst secret set DATABASE_URL=${{ secrets.DATABASE_URL_STAGING }} --stage staging + npx sst secret set NEXT_PUBLIC_SITE_URL=${{ secrets.NEXT_PUBLIC_SITE_URL_STAGING }} --stage staging + npx sst secret set COMMIT_SHA=${{ github.sha }} --stage staging - name: Run staging DB Migrations run: | From 9523909752b2689dda019b38fe7287e09945dfff Mon Sep 17 00:00:00 2001 From: Lucas Dale Date: Tue, 12 Nov 2024 17:22:13 -0500 Subject: [PATCH 07/14] Fix workflows with correct syntax --- .github/workflows/main.yaml | 2 +- .github/workflows/stage.yaml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index db370b5..284862d 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -38,7 +38,7 @@ jobs: - name: Set SST Config Secret run: | - npx sst secret set COMMIT_SHA=${{ github.sha }} --stage production + npx sst secret set COMMIT_SHA ${{ github.sha }} --stage production - name: Deploy with SST run: pnpm run deploy diff --git a/.github/workflows/stage.yaml b/.github/workflows/stage.yaml index a41bd3f..2da332a 100644 --- a/.github/workflows/stage.yaml +++ b/.github/workflows/stage.yaml @@ -38,9 +38,9 @@ jobs: - name: Set SST Config Secret run: | - npx sst secret set DATABASE_URL=${{ secrets.DATABASE_URL_STAGING }} --stage staging - npx sst secret set NEXT_PUBLIC_SITE_URL=${{ secrets.NEXT_PUBLIC_SITE_URL_STAGING }} --stage staging - npx sst secret set COMMIT_SHA=${{ github.sha }} --stage staging + npx sst secret set DATABASE_URL ${{ secrets.DATABASE_URL_STAGING }} --stage staging + npx sst secret set NEXT_PUBLIC_SITE_URL ${{ secrets.NEXT_PUBLIC_SITE_URL_STAGING }} --stage staging + npx sst secret set COMMIT_SHA ${{ github.sha }} --stage staging - name: Run staging DB Migrations run: | From bf616d0934e5a0d8d17f249af24a4c404f3cfc0c Mon Sep 17 00:00:00 2001 From: Lucas Dale Date: Tue, 12 Nov 2024 17:32:14 -0500 Subject: [PATCH 08/14] Fixing naming of aws vars and secrets --- .github/workflows/main.yaml | 2 +- .github/workflows/stage.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 284862d..b7bea3b 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -33,7 +33,7 @@ jobs: run: | mkdir -p ~/.aws echo "[default]" > ~/.aws/credentials - echo "aws_access_key_id=${{ secrets.AWS_ACCESS_ID }}" >> ~/.aws/credentials + echo "aws_access_key_id=${{ secrets.AWS_ACCESS_KEY_ID }}" >> ~/.aws/credentials echo "aws_secret_access_key=${{ secrets.AWS_SECRET_ACCESS_KEY }}" >> ~/.aws/credentials - name: Set SST Config Secret diff --git a/.github/workflows/stage.yaml b/.github/workflows/stage.yaml index 2da332a..137f04b 100644 --- a/.github/workflows/stage.yaml +++ b/.github/workflows/stage.yaml @@ -33,7 +33,7 @@ jobs: run: | mkdir -p ~/.aws echo "[default]" > ~/.aws/credentials - echo "aws_access_key_id=${{ secrets.AWS_ACCESS_ID }}" >> ~/.aws/credentials + echo "aws_access_key_id=${{ secrets.AWS_ACCESS_KEY_ID }}" >> ~/.aws/credentials echo "aws_secret_access_key=${{ secrets.AWS_SECRET_ACCESS_KEY }}" >> ~/.aws/credentials - name: Set SST Config Secret From 7fd7adabd265dc2849eae8760026fa5a65e03247 Mon Sep 17 00:00:00 2001 From: Lucas Dale Date: Tue, 12 Nov 2024 22:22:59 -0500 Subject: [PATCH 09/14] Remove unneeded migration strategy --- .github/workflows/stage.yaml | 6 +---- cli/migrator.ts | 51 ------------------------------------ lib/secrets.ts | 25 ------------------ package.json | 2 +- pnpm-lock.yaml | 10 ------- 5 files changed, 2 insertions(+), 92 deletions(-) delete mode 100644 cli/migrator.ts delete mode 100644 lib/secrets.ts diff --git a/.github/workflows/stage.yaml b/.github/workflows/stage.yaml index 137f04b..98e95ca 100644 --- a/.github/workflows/stage.yaml +++ b/.github/workflows/stage.yaml @@ -39,13 +39,9 @@ jobs: - name: Set SST Config Secret run: | npx sst secret set DATABASE_URL ${{ secrets.DATABASE_URL_STAGING }} --stage staging - npx sst secret set NEXT_PUBLIC_SITE_URL ${{ secrets.NEXT_PUBLIC_SITE_URL_STAGING }} --stage staging + npx sst secret set NEXT_PUBLIC_URL ${{ secrets.NEXT_PUBLIC_URL_STAGING }} --stage staging npx sst secret set COMMIT_SHA ${{ github.sha }} --stage staging - - name: Run staging DB Migrations - run: | - STAGE=staging npx tsx cli/migrator.ts - - name: Deploy with SST run: pnpm run deploy:stage diff --git a/cli/migrator.ts b/cli/migrator.ts deleted file mode 100644 index 378f41f..0000000 --- a/cli/migrator.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { migrate } from "drizzle-orm/postgres-js/migrator"; -import { drizzle } from "drizzle-orm/neon-serverless"; -import { Pool, neonConfig } from "@neondatabase/serverless"; -import ws from "ws"; - -import * as schema from "../lib/schema"; -import getSecret from "../lib/secrets"; - -async function performMigration() { - const dbUrlResponse = await getSecret("DATABASE_URL"); - const dbUrl = dbUrlResponse?.Parameter?.Value; - if (!dbUrl) { - console.error("DATABASE_URL not found"); - return; - } - - neonConfig.webSocketConstructor = ws; // <-- this is the key bit - - const pool = new Pool({ connectionString: dbUrl }); - pool.on("error", (err) => console.error(err)); // deal with e.g. re-connect - - const client = await pool.connect(); - - try { - await client.query("BEGIN"); - const db = await drizzle(client, { schema }); - await migrate(db, { migrationsFolder: "src/migrations" }); - await client.query("COMMIT"); - } catch (err) { - await client.query("ROLLBACK"); - throw err; - } finally { - client.release(); - } - - await pool.end(); -} - -if (require.main === module) { - console.log("Running migrations"); - - performMigration() - .then((val) => { - console.log("Migration performed"); - process.exit(0); - }) - .catch((err) => { - console.log("err", err); - process.exit(1); - }); -} diff --git a/lib/secrets.ts b/lib/secrets.ts deleted file mode 100644 index 42d1c9d..0000000 --- a/lib/secrets.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { GetParameterCommand, SSMClient } from "@aws-sdk/client-ssm"; - -//Grab Secrets from AWS Parameter Store -const STAGE = process.env.STAGE ? process.env.STAGE : "production"; -const PROJECT = "letusdev"; -const REGION = "us-east-1"; - -export default async function getSecret(secretName: string) { - if (!secretName) { - throw new Error("Secret name is required"); - } - - const client = new SSMClient({ region: REGION }); - const paramName = `/sst/${PROJECT}/${STAGE}/Secret/${secretName}/value`; - - const paramOptions = { - Name: paramName, - WithDecryption: true, - }; - - const command = new GetParameterCommand(paramOptions); - const response = await client.send(command); - - return response; -} diff --git a/package.json b/package.json index 725d574..5f7d72f 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,6 @@ "dev": "next dev", "start": "next start", "deploy": "sst deploy --stage production", - "migrate": "tsx cli/migrator.ts", "deploy:stage": "sst deploy --stage staging", "takeDown": "npx sst remove --stage production", "generate": "npx drizzle-kit generate", @@ -45,6 +44,7 @@ "tailwindcss": "3.4.1", "tailwindcss-animate": "^1.0.7", "typescript": "5.3.3", + "ws": "^8.18.0", "zod": "^3.23.8" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1c91efb..0e2f595 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -114,9 +114,6 @@ importers: '@types/react-dom': specifier: 18.2.18 version: 18.2.18 - '@types/ws': - specifier: ^8.5.13 - version: 8.5.13 aws-cdk-lib: specifier: 2.142.1 version: 2.142.1(constructs@10.4.2) @@ -1954,9 +1951,6 @@ packages: '@types/uuid@9.0.8': resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} - '@types/ws@8.5.13': - resolution: {integrity: sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==} - '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} @@ -5855,10 +5849,6 @@ snapshots: '@types/uuid@9.0.8': {} - '@types/ws@8.5.13': - dependencies: - '@types/node': 20.11.5 - '@ungap/structured-clone@1.2.0': {} abbrev@2.0.0: {} From 819d9805abb52a72e15a4a5208337126dc8f8c75 Mon Sep 17 00:00:00 2001 From: Lucas Dale Date: Tue, 12 Nov 2024 22:27:10 -0500 Subject: [PATCH 10/14] Fix pnpm packages --- package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/package.json b/package.json index 5f7d72f..be92b1e 100644 --- a/package.json +++ b/package.json @@ -44,14 +44,12 @@ "tailwindcss": "3.4.1", "tailwindcss-animate": "^1.0.7", "typescript": "5.3.3", - "ws": "^8.18.0", "zod": "^3.23.8" }, "devDependencies": { "@types/node": "20.11.5", "@types/react": "18.2.48", "@types/react-dom": "18.2.18", - "@types/ws": "^8.5.13", "aws-cdk-lib": "2.142.1", "drizzle-kit": "^0.27.1", "tsx": "^4.19.2" From 0fd3bfab39cae33e9cba315e89cc74a6a3441414 Mon Sep 17 00:00:00 2001 From: Lucas Dale Date: Tue, 12 Nov 2024 22:36:32 -0500 Subject: [PATCH 11/14] Update to use the resource MyEmail --- emails/subscribed.tsx | 16 ++-------------- emails/unsubscribed.tsx | 16 ++-------------- sst.config.ts | 3 --- utils/email.ts | 3 ++- 4 files changed, 6 insertions(+), 32 deletions(-) diff --git a/emails/subscribed.tsx b/emails/subscribed.tsx index 9301a7a..80f75aa 100644 --- a/emails/subscribed.tsx +++ b/emails/subscribed.tsx @@ -1,18 +1,6 @@ -import { - Body, - Button, - Container, - Head, - Hr, - Html, - Img, - Link, - Preview, - Section, - Text, -} from "@react-email/components"; +import { Text } from "@react-email/components"; import * as React from "react"; -import { EmailWrapper, url } from "."; +import { EmailWrapper } from "."; const Subscribed: React.FC = () => { return ( diff --git a/emails/unsubscribed.tsx b/emails/unsubscribed.tsx index 808a109..db092b5 100644 --- a/emails/unsubscribed.tsx +++ b/emails/unsubscribed.tsx @@ -1,18 +1,6 @@ -import { - Body, - Button, - Container, - Head, - Hr, - Html, - Img, - Link, - Preview, - Section, - Text, -} from "@react-email/components"; +import { Text } from "@react-email/components"; import * as React from "react"; -import { EmailWrapper, url } from "."; +import { EmailWrapper } from "."; const Unsubscribed: React.FC = () => { return ( diff --git a/sst.config.ts b/sst.config.ts index 400c4f5..091c1e6 100644 --- a/sst.config.ts +++ b/sst.config.ts @@ -10,9 +10,6 @@ export default $config({ }; }, async run() { - const email = new sst.aws.Email("MyEmail", { - sender: "lucas@strukt.io", - }); const database_url = new sst.Secret("DATABASE_URL"); const next_public_url = new sst.Secret("NEXT_PUBLIC_URL"); new sst.aws.Nextjs("letusdev", { diff --git a/utils/email.ts b/utils/email.ts index 7ca7cc9..ff64d28 100644 --- a/utils/email.ts +++ b/utils/email.ts @@ -1,6 +1,7 @@ import { render } from "@react-email/render"; import React from "react"; import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2"; +import { Resource } from "sst"; type ReactComponent = | React.FunctionComponent @@ -25,7 +26,7 @@ export async function mail(template: ReactComponent, mail: MailProps) { const send = async () => await new SESv2Client().send( new SendEmailCommand({ - FromEmailAddress: "lucas@strukt.io", + FromEmailAddress: Resource.MyEmail.sender, Destination: { ToAddresses: mail.to || [], }, From 95f35b4e420474c03abca1664c9169520574f31b Mon Sep 17 00:00:00 2001 From: Lucas Dale Date: Tue, 12 Nov 2024 23:11:53 -0500 Subject: [PATCH 12/14] Refactor work experience to come from db --- actions/blog/getBlog.ts | 25 ++++++++++++++++++++ actions/blog/getBlogs.ts | 13 +++++++++++ actions/work/getWorkExperience.ts | 18 ++++++++++++++ app/api/blog/[slug]/route.ts | 30 ------------------------ app/api/blog/route.ts | 10 -------- app/api/migration/route.ts | 6 ----- app/blog/[slug]/page.tsx | 17 ++++---------- components/BlogList.tsx | 11 +++------ data/workExperience.ts | 39 ------------------------------- emails/index.tsx | 2 +- lib/schema.ts | 27 ++++++++++++++++++++- package.json | 2 +- sst-env.d.ts | 9 ------- utils/email.ts | 3 +-- 14 files changed, 92 insertions(+), 120 deletions(-) create mode 100644 actions/blog/getBlog.ts create mode 100644 actions/blog/getBlogs.ts create mode 100644 actions/work/getWorkExperience.ts delete mode 100644 app/api/blog/[slug]/route.ts delete mode 100644 app/api/blog/route.ts delete mode 100644 app/api/migration/route.ts delete mode 100644 data/workExperience.ts diff --git a/actions/blog/getBlog.ts b/actions/blog/getBlog.ts new file mode 100644 index 0000000..2af3499 --- /dev/null +++ b/actions/blog/getBlog.ts @@ -0,0 +1,25 @@ +"use server"; + +import { eq } from "drizzle-orm"; +import { db } from "@/lib/db"; +import { blogsTable } from "@/lib/schema"; + +export async function getBlog(slug: string) { + try { + const incomingSlug = slug.replace(/^\/+|\/+$/g, ""); + + const blogData = await db + .select() + .from(blogsTable) + .where(eq(blogsTable.slug, `/${incomingSlug}`)); + + if (!blogData.length) { + throw new Error("Blog not found"); + } + + return blogData[0]; + } catch (error) { + console.error("Error fetching blog:", error); + throw new Error("Failed to fetch blog"); + } +} diff --git a/actions/blog/getBlogs.ts b/actions/blog/getBlogs.ts new file mode 100644 index 0000000..f253b42 --- /dev/null +++ b/actions/blog/getBlogs.ts @@ -0,0 +1,13 @@ +"use server"; +import { db } from "@/lib/db"; +import { blogsTable } from "@/lib/schema"; + +export async function getBlogs() { + try { + const blogs = await db.select().from(blogsTable).execute(); + return blogs; + } catch (error) { + console.error("Error fetching blogs:", error); + throw new Error("Failed to fetch blogs"); + } +} diff --git a/actions/work/getWorkExperience.ts b/actions/work/getWorkExperience.ts new file mode 100644 index 0000000..8fbd5b8 --- /dev/null +++ b/actions/work/getWorkExperience.ts @@ -0,0 +1,18 @@ +"use server"; + +import { db } from "../../lib/db"; +import { workExperienceTable } from "../../lib/schema"; + +export async function getWorkExperience() { + try { + const workExperience = await db + .select() + .from(workExperienceTable) + .orderBy(workExperienceTable.startDate); + + return workExperience; + } catch (error) { + console.error("Error fetching work experience:", error); + throw new Error("Failed to fetch work experience"); + } +} diff --git a/app/api/blog/[slug]/route.ts b/app/api/blog/[slug]/route.ts deleted file mode 100644 index 67c3ff6..0000000 --- a/app/api/blog/[slug]/route.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { eq } from "drizzle-orm"; -import { NextResponse } from "next/server"; -import { db } from "@/lib/db"; -import { blogsTable } from "@/lib/schema"; - -export async function GET( - request: Request, - { params }: { params: { slug: string } } -) { - try { - const incomingSlug = params.slug.replace(/^\/+|\/+$/g, ""); - - const blogData = await db - .select() - .from(blogsTable) - .where(eq(blogsTable.slug, `/${incomingSlug}`)); - - if (!blogData.length) { - return NextResponse.json({ error: "Blog not found" }, { status: 404 }); - } - - return NextResponse.json({ data: blogData[0] }); - } catch (error) { - console.error("Error fetching blog:", error); - return NextResponse.json( - { error: "Failed to fetch blog" }, - { status: 500 } - ); - } -} diff --git a/app/api/blog/route.ts b/app/api/blog/route.ts deleted file mode 100644 index 30c79d7..0000000 --- a/app/api/blog/route.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { db } from "@/lib/db"; -import { blogsTable } from "@/lib/schema"; -import { NextResponse } from "next/server"; -export const fetchCache = "force-no-store"; -export const revalidate = 0; - -export async function GET() { - const data = await db.select().from(blogsTable).execute(); - return NextResponse.json({ data }); -} diff --git a/app/api/migration/route.ts b/app/api/migration/route.ts deleted file mode 100644 index 615ed3e..0000000 --- a/app/api/migration/route.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { execSync } from "child_process"; - -export async function GET(request: Request) { - execSync(`npm run db push`, { stdio: "inherit" }); - return new Response("Migrated"); -} diff --git a/app/blog/[slug]/page.tsx b/app/blog/[slug]/page.tsx index 273e0d0..394e916 100644 --- a/app/blog/[slug]/page.tsx +++ b/app/blog/[slug]/page.tsx @@ -1,6 +1,7 @@ import { notFound } from "next/navigation"; import BlogContent from "./components/BlogContent"; import SubscriberForm from "../components/SubscriberForm"; +import { getBlog } from "@/actions/blog/getBlog"; interface BlogParams { params: { @@ -9,25 +10,15 @@ interface BlogParams { } export default async function BlogPost({ params }: BlogParams) { - const blog = await fetch( - `${process.env.NEXT_PUBLIC_URL}/api/blog/${params.slug}`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - } - ); - - const blogData = await blog.json(); + const blog = await getBlog(params.slug); - if (!blogData || !blogData.data) { + if (!blog) { return notFound(); } return ( <> - +
diff --git a/components/BlogList.tsx b/components/BlogList.tsx index 2cd013e..fe6bde5 100644 --- a/components/BlogList.tsx +++ b/components/BlogList.tsx @@ -2,20 +2,15 @@ import { IBlogList } from "@/lib/types"; import BlogCard from "./BlogCard"; import { Suspense } from "react"; import BlogListSkeleton from "./BlogListSkeleton"; +import { getBlogs } from "@/actions/blog/getBlogs"; export default async function BlogList() { - const data = await fetch(`${process.env.NEXT_PUBLIC_URL}/api/blog`, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); - const blogs = await data.json(); + const blogs = await getBlogs(); return ( }>
- {blogs.data?.map((blog: IBlogList, index: number) => ( + {blogs?.map((blog: IBlogList, index: number) => ( { - let base = process.env.URL ?? ""; + let base = process.env.NEXT_PUBLIC_URL ?? ""; return `${base}${route}`; }; diff --git a/lib/schema.ts b/lib/schema.ts index 7521f61..165b4d6 100644 --- a/lib/schema.ts +++ b/lib/schema.ts @@ -1,4 +1,12 @@ -import { pgTable, serial, uuid, text, timestamp } from "drizzle-orm/pg-core"; +import { + pgTable, + serial, + uuid, + text, + timestamp, + varchar, +} from "drizzle-orm/pg-core"; +import { sql } from "drizzle-orm"; export const blogsTable = pgTable("blogs_table", { id: serial("id").primaryKey().notNull(), @@ -20,5 +28,22 @@ export const subscribersTable = pgTable("subscribers_table", { deletedAt: timestamp("deleted_at"), }); +export const workExperienceTable = pgTable("work_experience", { + id: serial("id").primaryKey(), + company: varchar("company", { length: 255 }).notNull(), + position: varchar("position", { length: 255 }).notNull(), + startDate: varchar("start_date", { length: 255 }).notNull(), + endDate: varchar("end_date", { length: 255 }).notNull(), + content: text("content").notNull(), + createdAt: timestamp("created_at") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp("updated_at") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), +}); + export type SelectBlog = typeof blogsTable.$inferSelect; export type InsertSubscriber = typeof subscribersTable.$inferInsert; +export type WorkExperience = typeof workExperienceTable.$inferSelect; +export type NewWorkExperience = typeof workExperienceTable.$inferInsert; diff --git a/package.json b/package.json index be92b1e..b65964b 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "dev": "next dev", "start": "next start", "deploy": "sst deploy --stage production", - "deploy:stage": "sst deploy --stage staging", + "deploy:staging": "sst deploy --stage staging", "takeDown": "npx sst remove --stage production", "generate": "npx drizzle-kit generate", "db": "sst shell drizzle-kit" diff --git a/sst-env.d.ts b/sst-env.d.ts index 8dc9a65..8c6c10f 100644 --- a/sst-env.d.ts +++ b/sst-env.d.ts @@ -10,18 +10,9 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } - "MyEmail": { - "configSet": string - "sender": string - "type": "sst.aws.Email" - } "NEXT_PUBLIC_URL": { "type": "sst.sst.Secret" "value": string } - "letusdev": { - "type": "sst.aws.Nextjs" - "url": string - } } } diff --git a/utils/email.ts b/utils/email.ts index ff64d28..7ca7cc9 100644 --- a/utils/email.ts +++ b/utils/email.ts @@ -1,7 +1,6 @@ import { render } from "@react-email/render"; import React from "react"; import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2"; -import { Resource } from "sst"; type ReactComponent = | React.FunctionComponent @@ -26,7 +25,7 @@ export async function mail(template: ReactComponent, mail: MailProps) { const send = async () => await new SESv2Client().send( new SendEmailCommand({ - FromEmailAddress: Resource.MyEmail.sender, + FromEmailAddress: "lucas@strukt.io", Destination: { ToAddresses: mail.to || [], }, From 492f756e7c207d5b51fd621a7fa304eda50c4238 Mon Sep 17 00:00:00 2001 From: Lucas Dale Date: Tue, 12 Nov 2024 23:45:11 -0500 Subject: [PATCH 13/14] Fix staging deployment --- app/experience/page.tsx | 6 +++--- app/page.tsx | 2 -- components/TimelineList.tsx | 7 ++++--- sst-env.d.ts | 4 ++++ sst.config.ts | 5 ++++- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/app/experience/page.tsx b/app/experience/page.tsx index 9887b1b..67af8de 100644 --- a/app/experience/page.tsx +++ b/app/experience/page.tsx @@ -6,11 +6,11 @@ import { BreadcrumbPage, BreadcrumbSeparator, } from "@/components/ui/breadcrumb"; -import { workExperience } from "@/data/workExperience"; import { Home } from "lucide-react"; +import { getWorkExperience } from "@/actions/work/getWorkExperience"; -export default function ExperiencePage() { - const experiences = workExperience; +export default async function ExperiencePage() { + const experiences = await getWorkExperience(); const formatDate = (date: string): string => { if (date === "Current") return "Present"; return new Date(date).toLocaleDateString("en-US", { diff --git a/app/page.tsx b/app/page.tsx index 3a869d7..f80f246 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -3,8 +3,6 @@ import Image from "next/image"; import Me from "@/public/me.png"; import { TimelineList } from "@/components/TimelineList"; import { AnimatedDescription } from "@/components/AnimatedDescription"; -import BlogList from "@/components/BlogList"; -import { Card } from "@/components/ui/card"; import BlogCard from "@/components/BlogCard"; export default async function Index() { const descriptions = [ diff --git a/components/TimelineList.tsx b/components/TimelineList.tsx index 13f3b86..0becc88 100644 --- a/components/TimelineList.tsx +++ b/components/TimelineList.tsx @@ -1,5 +1,5 @@ +import { getWorkExperience } from "@/actions/work/getWorkExperience"; import { TimelineCard } from "@/components/TimelineCard"; -import { workExperience } from "@/data/workExperience"; interface TimelineItem { company: string; @@ -10,13 +10,14 @@ interface TimelineItem { slug: string; } -export const TimelineList: React.FC = () => { +export const TimelineList: React.FC = async () => { + const experiences = await getWorkExperience(); const formatDate = (date: string): string => { if (date === "Current") return "Current"; return new Date(date).getFullYear().toString(); }; - const timelineData: TimelineItem[] = workExperience.map((exp) => ({ + const timelineData: TimelineItem[] = experiences.map((exp) => ({ company: exp.company, startDate: exp.startDate, endDate: exp.endDate, diff --git a/sst-env.d.ts b/sst-env.d.ts index 8c6c10f..a4f7924 100644 --- a/sst-env.d.ts +++ b/sst-env.d.ts @@ -14,5 +14,9 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } + "letusdev": { + "type": "sst.aws.Nextjs" + "url": string + } } } diff --git a/sst.config.ts b/sst.config.ts index 091c1e6..d688184 100644 --- a/sst.config.ts +++ b/sst.config.ts @@ -18,7 +18,10 @@ export default $config({ NEXT_PUBLIC_URL: next_public_url.value, }, domain: { - name: "letusdev.io", + name: + $app.stage === "production" + ? "letusdev.io" + : `${$app.stage}.letusdev.io`, dns: sst.aws.dns({ zone: "Z0600725332UFN0OF4ISC", }), From 4e142f80d79a32d90e3133dac11d37ffcc3baa16 Mon Sep 17 00:00:00 2001 From: Lucas Dale Date: Tue, 12 Nov 2024 23:46:53 -0500 Subject: [PATCH 14/14] Fix staging pnpm script command --- .github/workflows/stage.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stage.yaml b/.github/workflows/stage.yaml index 98e95ca..f899932 100644 --- a/.github/workflows/stage.yaml +++ b/.github/workflows/stage.yaml @@ -43,7 +43,7 @@ jobs: npx sst secret set COMMIT_SHA ${{ github.sha }} --stage staging - name: Deploy with SST - run: pnpm run deploy:stage + run: pnpm run deploy:staging - name: Clean up AWS Profile run: rm -rf ~/.aws