Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@ jobs:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_DEFAULT_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_DEFAULT_ACCOUNT_ID }}

- name: Run database migrations
run: cd packages/core && npx sst shell --stage ${{ github.ref_name }} drizzle-kit push
env:
AWS_PROFILE: ''
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_DEFAULT_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_DEFAULT_ACCOUNT_ID }}

- name: Output deployment info
if: success()
run: |
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/pr-preview-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,13 @@ jobs:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_DEFAULT_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_DEFAULT_ACCOUNT_ID }}

- name: Run database migrations
run: cd packages/core && npx sst shell --stage ${{ env.STAGE_NAME }} drizzle-kit push
env:
AWS_PROFILE: ''
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_DEFAULT_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_DEFAULT_ACCOUNT_ID }}

- name: Get preview URLs
id: get_urls
run: |
Expand Down
6 changes: 6 additions & 0 deletions infra/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ export const personalEmail = !IS_DEPLOYED_STAGE
sender: 'harrison@structa.so',
});

export const anotherPersonalEmail = !IS_DEPLOYED_STAGE
? null
: new sst.aws.Email('anotherPersonalEmail', {
sender: 'hester@structa.so',
});

// Local email preview server (react-email)
export const reactEmail = new sst.x.DevCommand('EmailServer', {
dev: {
Expand Down
4 changes: 3 additions & 1 deletion packages/backend/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { AuthRoute } from "./routes/auth";
import { HealthRoute } from "./routes/health";
import { StorageRoute } from "./routes/storage";
import { UserRoute } from "./routes/user";
import { WaitlistRoute } from "./routes/waitlist";

export class VisibleError extends Error {
constructor(
Expand Down Expand Up @@ -69,7 +70,8 @@ const routes = app
.route("/auth", AuthRoute)
.route("/user", UserRoute)
.route("/storage", StorageRoute)
.route("/health", HealthRoute);
.route("/health", HealthRoute)
.route("/waitlist", WaitlistRoute);

export const handler = handle(routes);
export type RoutesType = typeof routes;
41 changes: 41 additions & 0 deletions packages/backend/src/api/routes/waitlist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { zValidator } from "@hono/zod-validator";
import { createContactInLoops, MAILING_LISTS } from "@structa/core/marketing";
import { Hono } from "hono";
import { z } from "zod";

const WaitlistSchema = z.object({
email: z.string().email(),
name: z.string().optional(),
});

export const WaitlistRoute = new Hono().post(
"/",
zValidator("json", WaitlistSchema),
async (c) => {
const { email, name } = c.req.valid("json");

const nameParts = (name || "").trim().split(" ");
const firstName = nameParts[0] || undefined;
const lastName = nameParts.slice(1).join(" ") || undefined;

const result = await createContactInLoops({
email,
userId: email, // Use email as userId for unauthenticated waitlist signups
firstName,
lastName,
properties: {
source: "waitlist",
createdAt: new Date().toISOString(),
},
mailingLists: {
[MAILING_LISTS.WAITLIST]: true,
},
});

if (!result.success) {
return c.json({ error: result.error }, 400);
}

return c.json({ success: true }, 200);
},
);
30 changes: 16 additions & 14 deletions packages/core/src/drizzle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,27 @@
* This verifies that the connection is properly configured for AWS Lambda.
*/

import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

// Mock the neon function to return a SQL query function
const mockSqlFunction = vi.fn();
const mockNeon = vi.hoisted(() => vi.fn(() => mockSqlFunction));

// Mock @neondatabase/serverless
vi.mock('@neondatabase/serverless', () => ({
vi.mock("@neondatabase/serverless", () => ({
neon: mockNeon,
}));

// Mock SST Resource
vi.mock('sst', () => ({
vi.mock("sst", () => ({
Resource: {
Database: {
url: 'postgresql://test:test@localhost:5432/test',
url: "postgresql://test:test@localhost:5432/test",
},
},
}));

describe('Database Connection', () => {
describe("Database Connection", () => {
beforeEach(() => {
vi.clearAllMocks();
});
Expand All @@ -34,26 +34,28 @@ describe('Database Connection', () => {
vi.restoreAllMocks();
});

it('should call neon with the database URL', async () => {
it("should call neon with the database URL", async () => {
// Clear module cache to get fresh import
vi.resetModules();

// Import the module to trigger the connection setup
await import('./drizzle');
await import("./drizzle");

expect(mockNeon).toHaveBeenCalledWith('postgresql://test:test@localhost:5432/test');
expect(mockNeon).toHaveBeenCalledWith(
"postgresql://test:test@localhost:5432/test",
);
});

it('should export a db instance with expected methods', async () => {
it("should export a db instance with expected methods", async () => {
// Clear module cache to get fresh import
vi.resetModules();

const { db } = await import('./drizzle');
const { db } = await import("./drizzle");

expect(db).toBeDefined();
expect(db).toHaveProperty('select');
expect(db).toHaveProperty('insert');
expect(db).toHaveProperty('update');
expect(db).toHaveProperty('delete');
expect(db).toHaveProperty("select");
expect(db).toHaveProperty("insert");
expect(db).toHaveProperty("update");
expect(db).toHaveProperty("delete");
});
});
Loading